Skip to content

refactor(stories): cut the Storybook surfaces that measure their own scaffolding - #1849

Merged
Astro-Han merged 9 commits into
mainfrom
fix/storybook-fidelity-drift
Aug 1, 2026
Merged

refactor(stories): cut the Storybook surfaces that measure their own scaffolding#1849
Astro-Han merged 9 commits into
mainfrom
fix/storybook-fidelity-drift

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Storybook cleanups, each a case where a story asserted something the product does not do.

The chat transcript had two hosts. Product/Chat Surface mounted ChatView and Composer inside a 960×760 centred card with a border and a radius — a frame the app does not have. In production the transcript lives in the detail pane that app-shell.stories.tsx composes from the real AstryxAppShell, so any geometry measured against the old story was measuring its own scaffolding, which is what FIDELITY.md's "the frame matters" section exists to prevent. Two of its six stories were also shell states in that wrong frame (StreamingResponse vs StreamingTurn; the two empty chats vs EmptyHome). The states the shell lacked — the reasoning-and-tool-call transcript, the session context layer, and the automation-triggered turn #1853 added while this branch was open — move onto ComposedShell, and the parallel file goes with its duplicate session/message/composer fixtures. SessionContextNarrow faked a narrow column with a 360px div; the manifest renders the surviving story at compact and floor instead.

ComposedShell now derives what the renderer derives. External review found the migration still short of the bar this PR set. The detail pane retyped AppShellDetailPanel's classes and dropped the two wrappers below it, so .mainColumn never applied and the composer measured its own box; the composer omitted onModelChange, which renders ChatModelSwitcher disabled; and NativeConversation lost the footer actions the deleted file hardcoded. It now mounts the real AppShellDetailPanel with the production wrappers and calls deriveAppShellTurnViewModel, deriveBranchBanner and deriveSessionRevisionNavigation. Deriving is the point: SessionContextLayer previously hand-wrote a banner and a revision counter for a session catalog containing neither a parent nor a revision family, and asserted fromAbortedTurn, which app-shell.tsx documents it deliberately never passes. The story now supplies the relatives and gets the banner only if the real rules still produce one.

A second review round found the first round's own fixtures unreachable. The revision family added above did not satisfy isValidRevisionLineage (packages/storage/src/session-store.ts) — it set two of the five fields the store writes together, used revisionIndex 1 where the store starts at 2, and made the root its own revision; the sidebar also listed three rows where projectRevisionLinkedSessionTree collapses a family to one. The lineage is now built the way reviseBeforeTurn writes it, and the sidebar runs through that projection. WaitingForPermission passed disabled: true to a composer the app never disables. EmptyChat was dropped as a duplicate of EmptyHome, but composer.tsx picks ChatModelSwitcher or NewChatModelPicker on activeSession, so the no-session composer is a different component — restored as NewChatComposer.

Three guards passed for reasons unrelated to what they check. sessionContext was not in REQUIRED_PRODUCT_SURFACES, so deleting its manifest entry silently removed its checks; that check then read querySelector, which returns OverflowList's hidden measurement clone, and the countdown-spacing assertion passed vacuously when its loop ran zero times. check-story-annotations.mjs skipped export followed by a line break, so an unannotated story could ship while the check reported that every story names its path. All four now fail in the direction they claim, with a regression test for the last.

play functions do not execute in this Storybook. #1853 added a computed line-height assertion to a story play; a deliberately throwing play still ships green through the smoke script, so that contract has never run. The marker's real contract moved to packages/ui/src/__tests__/host-origin-presentation.test.tsx, widened from Goal alone to all three Host origins — three separate branches in chat-turn.tsx with separate copy, of which only one was covered. FIDELITY.md records the trap.

ask-user-question was a review board, not a screen. Two hand-built columns at 760px and 390px, a fake transcript above them, and 60 lines of story-only CSS — the only CSS file under stories/. The prompt already caps itself at --maka-chat-measure and its production slot is display: contents, so both widths were invented. It now mounts inside the production wrappers, with the narrow column expressed as real viewports. Its annotation says plainly that the ChatComposerRegion takeover is out of scope and uncovered, rather than claiming a host it does not mount.

Plan reminders held five of the manifest's surfaces while every neighbouring page held one. The attention state is a single blocked row, which a scanning page only proves it surfaces when the row has healthy neighbours, so it joins the configured list — the way ExtensionsMcpConfigured already pairs one healthy server with one failed one. Keep-awake renders nothing at all, so it is a unit contract rather than a story: plan-reminder-panel.test.tsx now asserts aria-checked on the settings menu item in both directions, which the previous test did not do.

icons.stories.tsx kept its own list of the eight IM channels, so it was satisfied by any subset of the BOT_BRAND registry BotBrandLogo actually reads and a newly supported channel would silently never render. It now reads the keys, matching how LucideIcons already tracks the icon barrel.

FIDELITY.md loses three paragraphs that re-derived check-story-annotations.mjs's design from that script's own header, and a trailing H1 describing one story's variants that those variants already carry as // Real path:.

Net: 863 deletions against 638 insertions, one fewer story file, and the catalog goes from 65 stories to 59.

Verification

  • npm --workspace @maka/desktop run build-storybook + run smoke:storybook — passed, 57 manifest checks and 47 catalog renders.
  • Every new or repaired guard was verified against a negative case: removing relatedSessions fails session-context-layer at wide, compact and floor; deleting the manifest entry fails with missing required surface: sessionContext; a wrong line-height constant and then an unconditional throw in a play both shipped green, which is how the inert-play finding was established.
  • npm --workspace @maka/desktop run typecheck — clean across preload, main, renderer and storybook projects.
  • node --test packages/ui/dist/__tests__/plan-reminder-panel.test.js and .../host-origin-presentation.test.js — 6/6 and 3/3.
  • node scripts/check-story-annotations.mjs and node --test scripts/check-story-annotations.test.mjs — clean, 8/8.
  • npm run lint, npm run format:check, node scripts/check-dead-css.mjs, npx knip on both workspaces — clean.

No product source changed: the diff is stories, the fidelity doc, the smoke manifest and its script, and two UI tests.

Review focus

The claim worth checking by reading rather than running is whether each // Real path: sentence is true — the CI check can only prove the sentence exists. SessionContextLayer is the one to read closest: it asserts that a branched session can also be revision 2 of 3, and that the banner correctly omits 从中断前.

@Astro-Han
Astro-Han force-pushed the fix/storybook-fidelity-drift branch 2 times, most recently from 388972f to cb7876a Compare August 1, 2026 16:09
@Astro-Han Astro-Han closed this Aug 1, 2026
@Astro-Han Astro-Han reopened this Aug 1, 2026
@Astro-Han
Astro-Han force-pushed the fix/storybook-fidelity-drift branch from 6bb4aa9 to 1ef0137 Compare August 1, 2026 17:08
The catalog kept its own list of the eight IM channels, so the story was
satisfied by any subset of the registry BotBrandLogo actually reads — a
newly supported channel would simply never render. Read the keys instead,
matching how LucideIcons already tracks the icon barrel, and drop the empty
omit-list and the always-true component guard it filtered through.
The story built its own two-column board with a hand-written fake transcript
and 60 lines of story-only CSS, then hardcoded 760px and 390px chat columns.
The prompt already caps itself at --maka-chat-measure and its production slot
is display:contents, so those widths were the story's invention rather than
the product's — the retyped chain FIDELITY.md warns about, and the transcript
above it was a screen no user reaches.

Mount the prompt the way its sibling sandbox-boundary-prompt story does, and
give the manifest the narrow column back as compact and floor viewports,
where it is the real thing instead of a fixed div.
Plan reminders held five of the manifest's surfaces while every neighbouring
page held one, and two of the five paid for it without showing anything new.

The attention state is a single blocked row, which is exactly the kind of
thing a scanning page only proves it surfaces when the row has healthy
neighbours — so it joins the configured list, the way ExtensionsMcpConfigured
already pairs one healthy server with one failed one.

Keep-awake renders nothing at all: plan-reminder-panel.test.tsx asserts the
enabled page stays visually identical and keeps the state inside the settings
menu, so the story smoked pixels identical to the configured one and its play
function re-tested a prop the unit test already covers.

Also drop the "~130 stories" figure from the catalog-pass comment; the count
has been wrong since #1783 cut it, and a live number there earns nothing.
Product/Chat Surface mounted ChatView and Composer inside a 960x760 centred
card with a border and a radius — a frame the product does not have. In the
app the transcript lives in the full-height maka-panel-detail pane that
app-shell.stories.tsx already composes from the real AstryxAppShell, so every
geometry comparison against the old story was measuring its own scaffolding.
Two of its six stories were also the shell stories' states in that wrong
frame: StreamingResponse against StreamingTurn, and the two empty chats
against EmptyHome, which is the only empty home the app renders.

Move the states the shell did not have — the reasoning-and-tool-call
transcript and the session context layer — onto ComposedShell, and delete the
parallel file with its second copy of the session, message and composer
fixtures. Widening the session overlay to Partial<SessionSummary> keeps the
one-active-session rule while letting a story set the name and labels the
context layer reads.

SessionContextNarrow is not carried over: it faked a narrow column with a
360px div, and the manifest now renders the same story at compact and floor.
The panel renders identically whether keep-awake is on or off, so the
settings checkbox is the only thing carrying the state — and the existing
test only checked that the trigger exists. Deleting the story that opened
the menu therefore left aria-checked unverified. Astryx renders the menu
in static markup, so the contract lands in the same suite rather than
needing a browser.
Review found the migrated stories still short of what this PR claimed. The
detail pane retyped AppShellDetailPanel's classes and dropped the two wrappers
below it, so .mainColumn never applied and the composer measured its own box;
the composer omitted onModelChange, which renders ChatModelSwitcher disabled;
and NativeConversation lost the footer actions the old file hardcoded.

Mount the real AppShellDetailPanel with the production wrappers, and derive
what the renderer derives: deriveAppShellTurnViewModel for footer actions,
deriveBranchBanner and deriveSessionRevisionNavigation for lineage. Deriving
is the point — SessionContextLayer previously hand-wrote a banner and a
revision counter for a session catalog containing neither a parent nor a
revision family, and asserted fromAbortedTurn, which app-shell.tsx documents
it deliberately never passes. The story now supplies the relatives and gets
the banner only if the real rules still produce one.

A story that renders no lineage while claiming to show it is the failure this
should catch, so sessionContext gets a surface check for the breadcrumbs, the
revision counter and horizontal overflow; removing the fixture makes it fail
at all three viewports.

Excluding id from the session overlay closes the one field that would have
desynchronized the sidebar from the chat rather than projecting onto it.
…duce

Second review round found the lineage fixture from the first one was itself
impossible. The store's isValidRevisionLineage accepts the five revision
fields together or not at all and rejects any index below 2; the fixture set
revisionRootSessionId and revisionIndex alone, and made the root its own
revision. reviseBeforeTurn shows the real shape: the root carries no revision
fields and each revision carries all five, keeping the branch lineage it was
revised from. The family is now built that way, and the sidebar runs through
projectRevisionLinkedSessionTree, because production collapses a family to one
row and the story was about to show three.

WaitingForPermission forced disabled: true, but app-shell.tsx never passes
disabled to ChatComposerRegion — the textarea stays usable while a tool waits,
so the story was inventing a locked input.

Restore the no-active-session composer as NewChatComposer. Deleting EmptyChat
took the only coverage of Composer's other branch with it: without an active
session it renders NewChatModelPicker instead of ChatModelSwitcher and drops
the thinking selector. EmptyHome's comment claimed that state differed only in
the sidebar, which was wrong.

The ask-user-question decorator now reproduces the two wrappers around the
composer slot. The prompt's root carries the composer class and .mainColumn
zeroes its top padding, so the bare canvas rendered it 8px low; measured at
0px in the built story afterwards.

Guards, all three verified by making them fail:
- sessionContext joins REQUIRED_PRODUCT_SURFACES, so deleting its manifest
  entry now fails the validator instead of silently retiring its check.
- The check reads every OverflowList copy rather than the first, which is the
  hidden measurement clone, and asserts goal and revision are visible or in
  the overflow menu — not merely present in the DOM.
- The countdown spacing loop ran zero times if countdowns disappeared; it now
  requires one when a scheduled row exists.
- check-story-annotations skipped `export` followed by a newline, so a story
  in that shape passed unread while the check reported success.
#1853 asserted the automation-origin marker's line height inside a story
`play` function. Play functions never execute in this Storybook — no test
addon is configured, and a deliberately throwing `play` still ships green
through scripts/storybook-visual-smoke.mjs — so that contract never ran.

Keep the automation-triggered turn as visual coverage by folding it into
the transcript scaffold, and move the contract into the seam that runs:
the existing origin-presentation test, widened from Goal alone to all
three Host origins, each of which is a separate branch with its own copy.
…ing the checker

Two edits to the fidelity convention:

- Add the rule the commit before this one discovered: `play` is inert here,
  so behavioural and computed-style contracts belong in a packages/ui test
  or the smoke script's checks, not in a story.
- Cut three paragraphs that re-derive check-story-annotations.mjs's design
  — its fail-closed rationale, #1724's history, and why a source scanner
  only approximates the story set. All of it already lives in that script's
  header, which is where someone editing the guard will read it. The doc
  keeps the rule authors need: annotate every story, and write it as
  `export const Name: Story = …`.
- Drop the trailing `# Sandbox boundary prompt` section. It described one
  story's two variants, in an H1 that broke the document's structure, and
  those variants carry the same description as their own `// Real path:`.
@Astro-Han
Astro-Han force-pushed the fix/storybook-fidelity-drift branch from 1ef0137 to 87338e3 Compare August 1, 2026 17:22
@Astro-Han
Astro-Han merged commit a532f16 into main Aug 1, 2026
5 checks passed
@Astro-Han
Astro-Han deleted the fix/storybook-fidelity-drift branch August 1, 2026 17:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant