mirror of
https://github.com/nearai/ironclaw.git
synced 2026-09-03 08:06:01 +08:00
* chore(webui): integrate Storybook + design-system catalog (Epic phase 1) Storybook 10 (@storybook/react-vite, pnpm) wired to app.css + a light/dark data-theme toolbar; ~33 stories across Primitives / Components / Composites / Icons / Tokens; shared withRouter/withQueryClient decorators + react-query cache seeding; vitest split (pnpm test node-only, pnpm test:storybook in headless Chromium); @storybook/addon-mcp. Recreated as a clean linear branch off current main (crates/product path), superseding #7039 which had become an unmergeable stacked/merge-commit tangle. Verified: typecheck, lint:conventions, test:storybook (103), node test (1355), build-storybook — all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(webui): address design-system catalog review feedback Review follow-ups for Storybook + design-system catalog (Epic phase 1): - CI: run the Storybook story suite (headless Chromium) + build-storybook in the frontend code_style job so the browser suite can no longer regress silently behind the `test` / `test:storybook` split (High). - a11y: enforce `a11y: error` in test:storybook. Fix a nameless <Select> and an unresolved `aria-controls` target; scope only `color-contrast` as a documented Phase-3 exception (known `--v2-*` token-palette AA shortfalls). - Security: gate `@storybook/addon-mcp` behind `STORYBOOK_MCP=1`, so no unauthenticated `/mcp` endpoint ships in the committed config by default. - Deps: pin the new Storybook/Playwright packages to exact versions (match the existing convention); drop the unused `@vitest/coverage-v8`. - Stories: stub `fetch` for PairingWebCodePanel so the production caller gets deterministic pending-code / error responses and never hits a real backend; reuse the canonical `SelectMenuOption` type and exported `iconNames`; type the trace-credits fixture; sync the Modal `open` control; refresh token color values when the theme toolbar changes. - vite: stop the Storybook browser server from proxying `/assets` to an absent backend — serve the real logo from `public/` (fixes ECONNREFUSED / broken logo in the catalog). - docs: trim duplicated component API facts from the design-system README (one canonical home: component source + Storybook). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(ci): resolve the Storybook Playwright cache path + harden the fetch stub handoff CI (`fast-checks` -> `ws12_workflow_contracts`): - The Playwright browser-cache key spelled `crates/product/ironclaw_webui/ frontend/pnpm-lock.yaml` as a flat literal. `validate_webui_frontend_sites` sanctions that literal in exactly one shape (a `cache-dependency-path` line twinned with its nested-wildcard sibling), so a `hashFiles(...)` argument is the WS10 regression the pin exists to catch. Build the path from the already-resolved `WEBUI_FRONTEND_DIR` (`scripts/ci/crate-dir.sh`) via `format()` instead — `env` is available to `steps.<*>.with`. Review follow-ups: - `withStubbedFetch` shared one global "a stub is installed" guard across decorator instances. Switching stories renders the incoming decorator before React runs the outgoing one's cleanup, so the new story ran on the OLD story's routes and the outgoing cleanup then restored the real `fetch` underneath it — a live backend call from a story (e.g. minting a real pairing code). Each instance now owns its stub: install whenever `window.fetch` is not this instance's own, carry the real `fetch` forward through `__original` so stubs never chain-wrap, and restore only while still the owner. Regression test: `storybook-decorators.test.tsx` drives the swap through React (distinct component types, so it is a real unmount+mount) and asserts the second story is served its own routes, that no story request reaches the real `fetch`, that an unmatched URL still falls through, and that the last owner restores. Red against the previous implementation. - `MintError` covered the failed bootstrap status but never invoked `renew`. Add a `play` that waits for the alert, clicks `pairing-new-code`, and asserts the panel returns to alert + enabled retry — counting the stubbed mint POSTs so it pins the retry PATH, not just a re-render. Verified: `pnpm test:storybook` 104/104, `pnpm lint` (conventions + tsc), `pnpm test` adds 1 passing test with no new failures (16 pre-existing local failures are a Node 26 vs required 22.22 happy-dom mismatch, green on CI), `python3 scripts/ci/test_ws12_workflow_contracts.py` 94/94. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(webui): report Storybook theme-persist failures and make the fetch stub hermetic by default Follow-ups to the approach-audit run against6a380c8. Theme persistence swallowed its cause (`.storybook/preview.tsx`). The empty `catch` discarded every `localStorage.setItem` failure, leaving no diagnostic. The write stays non-fatal — `data-theme` on <html> is what app.css keys its token sets off, so a story is themed correctly whether or not the persist succeeds, and the persist can fail legitimately (sandboxed iframe, browser blocking storage in a third-party frame). What changes is that the cause is now reported via `console.warn` instead of dropped, so a real storage fault is visible rather than looking like a successful persist. The fetch stub let unmatched requests reach the network (`storybook-decorators.tsx`). Passing unmatched requests through to the real `fetch` meant a future story that forgot a route would silently acquire live backend access — exactly what the pairing stories document must never happen (a real pairing code minted from a shared/deployed catalog). Unmatched requests now reject with an error naming the method and URL and pointing at the fix; `{ passthrough: true }` is the explicit opt-in for a story that genuinely needs the network. Hermeticity is now a property of the harness rather than a convention each new story has to remember. Regression tests extend `storybook-decorators.test.tsx`: the existing story-swap test now asserts an unmatched request rejects and never reaches the real fetch, and a second test pins the opt-in — declared routes still win over the network, and only uncovered URLs fall through. Verified: `pnpm test:storybook` 104/104 (unchanged — no story was relying on network fall-through, so the stricter default breaks nothing), `pnpm lint` (conventions + tsc), `pnpm test` 1341 passing / +1 test with no new failures (the 16 local failures are the pre-existing Node 26 vs required 22.22 happy-dom mismatch, green on CI). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(webui): install the Storybook fetch stub on commit and match a Request's own method Two follow-ups from the CodeRabbit re-review ofb8238341. Install moved out of the render phase. The stub was assigned to `window.fetch` during render, but a render can be abandoned — interrupted, suspended, or thrown out — without ever committing, and an abandoned render schedules no cleanup. That stranded a stub on `window.fetch` with nothing left to remove it. The install now runs in a layout effect, and `<Story />` is withheld until the stub is live so a story that fetches on mount still cannot observe the real `fetch` (the reason the install sat in render to begin with). The layout effect lands the second render synchronously before paint, so there is no flicker, and cleanup ordering now falls out naturally: the outgoing story's cleanup runs before the incoming story's install, with the ownership check retained. `Request.method` is now honored. `fetch(new Request(url, { method: "POST" }))` carries its method on the Request rather than on `init`, so reading only `init` classified it as a GET — matching the wrong route, or none. Resolution order is `init.method`, then `Request.method`, then `GET`. Regression tests: an uncommitted render (`renderToStaticMarkup`, which runs the render phase and stops) must leave `window.fetch` untouched; and a POST `Request` must select the POST route while a bare `Request` still selects the GET one. Both verified red against the previous implementation. Verified: `pnpm test:storybook` 104/104 — withholding the story for one commit breaks no play function; `pnpm lint` (conventions + tsc); `pnpm test` 1343 passing / +2 tests, with the same 16 pre-existing local failures from the Node 26 vs required 22.22 happy-dom mismatch (green on CI). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(webui): drop a stray sed backup committed alongside the fetch-stub fix `storybook-decorators.tsx.tmp` was a `sed -i.tmp` backup left over from verifying that the new regression tests fail against the previous implementation. It was byte-identical to `storybook-decorators.tsx` and got swept into0502c1aeby a `git add -A`. Removing it — the canonical home for the decorator is `storybook-decorators.tsx` alone. No behavior change: nothing imported the duplicate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(webui): drop concrete extension names from the design-system stories The merge queue ejected this PR three times. Both required checks — `Code Style` (via `Reborn CLI smoke tests`) and `Tests (Reborn)` (via the `architecture-misc` crate bucket) — failed on the same gate: `reborn_generic_code_names_no_concrete_extension` (`ironclaw_architecture_tests::reborn_extension_specificity`). That gate scans the WebUI frontend `src` tree for `.ts`/`.tsx` naming a concrete extension, vendor, or vendor host, deriving the forbidden vocabulary from the package manifests under `crates/extensions/packages/` and `tests/fixtures/extensions/`. Four story fixtures tripped it: - `pairing-web-code-panel.stories.tsx` — "telegram" - `flow-list.stories.tsx` — "slack", "telegram" - `input.stories.tsx` — "acme" (from the `acme-messenger` test fixture package) None is pre-existing debt, so the fix is the code, not the allowlist (raising `WS0_EXTENSION_SPECIFICITY_ALLOWLIST_BASELINE` is a reviewed decision reserved for existing debt, and the ratchet only shrinks). The names were arbitrary demo strings: the pairing panel is extension-agnostic and its stub routes key off whatever id the story picks, so it now uses a fictional "example-chat" with an `example.test` deep link; the FlowList copy says "a chat app or email"; and the Input placeholder is "Research workspace". Why CI never caught this on the PR: the change is frontend-only, so `has_code=false` and the Rust jobs are path-filtered out — every PR run shows them as `skipping`. The merge queue runs the full suite against PR-merged-into- main, which is the first place the gate executes. `is_test_source_path` exempts `.test.`/`.spec.` and `test-utils`/`test_support` paths but not `.stories.`, so story files are scanned as product code. Widening that exemption would be a gate change needing its own review, so this fixes the fixtures instead. Verified: no Rust toolchain is available on this machine, so the gate's term derivation and scan were reproduced in Python (`derive_forbidden_terms` + `scan_dir` semantics, deliberately over-approximating the term set) — 603 terms derived, zero hits in the three files. `pnpm test:storybook` 104/104 and `pnpm lint` (conventions + tsc) stay green. Not addressed here: `Reborn user-sandbox Docker tests` also failed in the third queue attempt (`sweeper_removes_managed_egress_bundle_after_worker_disappears`) but passed in the second, and is unrelated to this PR — an intermittent real-Docker egress-bundle sweeper test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>