refactor(stories): cut the Storybook surfaces that measure their own scaffolding - #1849
Merged
Conversation
Astro-Han
force-pushed
the
fix/storybook-fidelity-drift
branch
2 times, most recently
from
August 1, 2026 16:09
388972f to
cb7876a
Compare
Astro-Han
force-pushed
the
fix/storybook-fidelity-drift
branch
from
August 1, 2026 17:08
6bb4aa9 to
1ef0137
Compare
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
force-pushed
the
fix/storybook-fidelity-drift
branch
from
August 1, 2026 17:22
1ef0137 to
87338e3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Storybook cleanups, each a case where a story asserted something the product does not do.
The chat transcript had two hosts.
Product/Chat SurfacemountedChatViewandComposerinside 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 thatapp-shell.stories.tsxcomposes from the realAstryxAppShell, 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 (StreamingResponsevsStreamingTurn; the two empty chats vsEmptyHome). 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 ontoComposedShell, and the parallel file goes with its duplicate session/message/composer fixtures.SessionContextNarrowfaked a narrow column with a 360px div; the manifest renders the surviving story at compact and floor instead.ComposedShellnow derives what the renderer derives. External review found the migration still short of the bar this PR set. The detail pane retypedAppShellDetailPanel's classes and dropped the two wrappers below it, so.mainColumnnever applied and the composer measured its own box; the composer omittedonModelChange, which rendersChatModelSwitcherdisabled; andNativeConversationlost the footer actions the deleted file hardcoded. It now mounts the realAppShellDetailPanelwith the production wrappers and callsderiveAppShellTurnViewModel,deriveBranchBannerandderiveSessionRevisionNavigation. Deriving is the point:SessionContextLayerpreviously hand-wrote a banner and a revision counter for a session catalog containing neither a parent nor a revision family, and assertedfromAbortedTurn, whichapp-shell.tsxdocuments 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, usedrevisionIndex1 where the store starts at 2, and made the root its own revision; the sidebar also listed three rows whereprojectRevisionLinkedSessionTreecollapses a family to one. The lineage is now built the wayreviseBeforeTurnwrites it, and the sidebar runs through that projection.WaitingForPermissionpasseddisabled: trueto a composer the app never disables.EmptyChatwas dropped as a duplicate ofEmptyHome, butcomposer.tsxpicksChatModelSwitcherorNewChatModelPickeronactiveSession, so the no-session composer is a different component — restored asNewChatComposer.Three guards passed for reasons unrelated to what they check.
sessionContextwas not inREQUIRED_PRODUCT_SURFACES, so deleting its manifest entry silently removed its checks; that check then readquerySelector, which returnsOverflowList's hidden measurement clone, and the countdown-spacing assertion passed vacuously when its loop ran zero times.check-story-annotations.mjsskippedexportfollowed 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.playfunctions do not execute in this Storybook. #1853 added a computed line-height assertion to a storyplay; a deliberately throwingplaystill ships green through the smoke script, so that contract has never run. The marker's real contract moved topackages/ui/src/__tests__/host-origin-presentation.test.tsx, widened from Goal alone to all three Host origins — three separate branches inchat-turn.tsxwith separate copy, of which only one was covered. FIDELITY.md records the trap.ask-user-questionwas 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 understories/. The prompt already caps itself at--maka-chat-measureand its production slot isdisplay: 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 theChatComposerRegiontakeover 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
ExtensionsMcpConfiguredalready 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.tsxnow assertsaria-checkedon the settings menu item in both directions, which the previous test did not do.icons.stories.tsxkept its own list of the eight IM channels, so it was satisfied by any subset of theBOT_BRANDregistryBotBrandLogoactually reads and a newly supported channel would silently never render. It now reads the keys, matching howLucideIconsalready 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.relatedSessionsfailssession-context-layerat wide, compact and floor; deleting the manifest entry fails withmissing required surface: sessionContext; a wrong line-height constant and then an unconditionalthrowin aplayboth shipped green, which is how the inert-playfinding 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.jsand.../host-origin-presentation.test.js— 6/6 and 3/3.node scripts/check-story-annotations.mjsandnode --test scripts/check-story-annotations.test.mjs— clean, 8/8.npm run lint,npm run format:check,node scripts/check-dead-css.mjs,npx knipon 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.SessionContextLayeris the one to read closest: it asserts that a branched session can also be revision 2 of 3, and that the banner correctly omits 从中断前.