MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-3283

A story whose deliverable was a PAGE planned no instrument that OPENS it until its last card — a client-boundary 500 lived through five green subtask PRs

Done
Description

Found during motir run MOTIR-3232 (2026-08-20), writing the story's browser E2E (MOTIR-3243).

What the plan did

Story MOTIR-3232 rebuilds /plans — a PAGE. It fanned out into eleven children: eight that change the page's code and its services, one vitest gate, and two E2E cards placed last, blocked by the other nine. That is the ordinary and, card-for-card, defensible shape: an E2E of a surface genuinely needs the surface.

What it cost

MOTIR-3241 (the tab strip) declared the page's URL parser — ten lines, pure string → string, no hooks — inside the 'use client' component that pushes the URL, and the page, a Server Component, imported it. 'use client' marks a module, not a declaration, so every export of it is a client REFERENCE: the server got a proxy, calling it threw, and /plans answered 500 on every request.

It then survived, undetected, through five more subtask PRs on the same branch, because nothing in the story's plan opens the page:

  • tsc --noEmit — clean. The types are real.
  • next build — clean. The boundary is only crossed at render.
  • The tab strip's own twelve-test component suite — green, and one of its tests exercises that exact function over six malformed inputs. A vitest import has no RSC boundary in play, so there the helper is just a helper.
  • Every subtask PR's full CI — green.

The first instrument that could see it was MOTIR-3243's Playwright walk, whose opening assertion (the tab strip is visible) found an error page. Had the E2E card been dropped, deferred, or scoped to a different surface, the story would have merged a permanently-500ing page with eleven green cards behind it.

The rule this suggests

A story whose deliverable is a rendered surface owes at least one instrument that OPENS that surface, and owes it EARLY — not only as its last card. Not because the unit tests are wrong, but because a whole class of defect lives in the server/client seam and is invisible to every one of them; the cost of finding it at the end is that N cards have shipped on top of it.

Cheap shapes worth considering rather than a second full E2E:

  • the story's FIRST code card also lands a smoke that loads the route and asserts one landmark, so every later card's CI opens the page;
  • or the plan's E2E card is ordered against the first card that renders the surface rather than against all of them;
  • or a repo-level guard: for every module WITHOUT 'use client', flag named imports from a module that HAS it whose name starts lowercase. That is a static check, it runs in seconds, and it found exactly this one instance and (after the fix) none. It would not have needed a browser at all.

The third is the narrowest and might belong in motir-core rather than in the planner; the first two are planning rules. Deciding which is this card's work — this is a report with a proposal, not a settled rule.

Evidence

  • The server's own line: ⨯ Error: Attempted to call planStatusFromParam() from the server but planStatusFromParam is on the client.digest: '628730192', on GET /plans, in a production build (next build && next start), acceptance lane.
  • What the browser showed: heading "This page couldn't load", paragraph: A server error occurred. Reload to try again.
  • Seen on branch parent/MOTIR-3232-plans-surface-refined in motir-core, at the commit before MOTIR-3243's. Not a statement about main — the defect never reached it, because the story's own E2E card caught it first, which is the one thing that went right.
  • Fixed on MOTIR-3243's pull request by moving the parser to lib/planning/planStatusFilter.ts (no directive), imported by both sides. Lesson logged as notes.html #353 (motir-meta PR #299).