test(app-shell): route the two HomePage runtime-config doubles instead of sinking every url (objectui#8033) - #8661
Conversation
…d of sinking every url (objectui#8033)
`HomePage.marketplaceDisabled.test.tsx` and `HomePage.aiStudioDisabled.test.tsx`
each installed a blanket `fetch` sink — one `vi.fn` returning the runtime-config
body for EVERY url — and tore it down in a locally registered `afterEach` that
called `vi.unstubAllGlobals()` with no `cleanup()` in front of it. Two defects,
landed together.
Ordering: Vitest runs `afterEach` hooks in reverse registration order, so a
teardown written in a test file runs BEFORE the root setup's RTL `cleanup()`.
Unstubbing there restores the real `fetch` while the tree is still mounted, so a
read that `cleanup()`'s act-flush triggers reaches a live socket. Both files now
`cleanup()` before `vi.unstubAllGlobals()`, the pairing the network-escape
guard's own `Fix:` text names and that objectui#7307's batches installed in 16
files (objectui#7439).
Routing: `HomePage` reaches two endpoints under these renders —
`GET /api/v1/runtime/config`, which the sink existed for, and
`GET /api/v1/meta/_drafts`, which `usePendingDrafts` issues from its mount
effect with the global `fetch` (`usePendingDrafts.ts:48`). The sink answered the
`_drafts` reader with the runtime-config body; it found no `drafts` key and
yielded `[]`, so nothing failed and nothing could. Both files now install one
recording router at module scope that serves exactly those two routes, records
every url it is handed, and asserts in `afterEach` that nothing outside the
served set was requested. Neither file is on the network-escape burn-down list —
it reached zero and was retired — so that assertion is the change's own
evidence.
All four stub sites are converted, not two: the per-file `bootOn` sink and the
inline `status: 404` sink in each file's fail-open case, which now drives the
same router through `bootOnNoConfig()`. `_drafts` answers an empty ledger in the
`{ drafts: [...] }` envelope, so `PendingDraftsBanner` still renders `null` and
no assertion moves — 14 tests passed before and after.
Router shape copied from `HomePage.approvalsTarget.test.tsx` (objectui#7307
batch 4, PR #8032).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
Test-only: both files touched are `*.test.tsx` under `packages/app-shell/src`, so `scripts/check-changeset-presence.mjs` requires a declaration; empty frontmatter is the explicit exemption for a change that publishes nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
ACCEPT — objectui#8033 / PR objectui#8661,
|
| claim | this seat's reading | verdict |
|---|---|---|
| ⭐ all four stub sites converted | per file the patch removes 2 vi.stubGlobal and adds 1 ⇒ four sinks collapsed into two routers, one per file |
✅ |
| ⛔ both defects together | both files now import cleanup and call cleanup() inside the added teardown block |
✅ |
the afterEach assertion exists |
expect(fetchCalls.filter((url) => !SERVED_ROUTES.includes(routeOf(url)))).toEqual(…) in both files |
✅ |
| scope | 3 files: the two test files (+102 −5 each) and one changeset. ⛔ Nothing else touched | ✅ |
draft, Fixes #8033 |
✅ | ✅ |
⭐ Precondition 4 came back STRONGER than the card and triage stated it
Both said "neither file is on the network-escape ledger." True — ⭐ and there is no ledger at all: KNOWN_ESCAPES was retired on 4b9fe9819 when objectui#7307 reached zero, and scripts/__tests__/network-escape-ledger.test.ts now pins its absence. This seat confirms the constant survives only inside that pinning test (plus one unrelated plugin-detail fixture).
⇒ ⭐ the "no gate covers these two files" reasoning is not weaker than stated, it is more absolute — which is precisely why the dispatch required the PR to carry its own evidence.
⭐ Two failure legs, not one — and the second is the one that matters
- Leg A (non-vacuity): demanded the recorded set be empty ⇒ 14/14 red, each case enumerating exactly
['/api/v1/runtime/config', '/api/v1/meta/_drafts']. ⇒ the router really records, and it really sees both routes. - Leg B (escape probe): injected
void fetch('/api/v1/ABLATION/unexpected-endpoint')⇒ 14/14 red, each naming the unexpected URL. ⭐ Under the old sink that same probe is answeredok: trueand stays green. ⇒ that is the defect reproduced and closed, in one measurement.
Both legs proved on disk by anchor count and blob hash, restored under a trap with absolute paths, restoration proven by blob equality against HEAD — ⛔ not by an exit code.
⭐ A red the dev refused to read as a red
tsc -p tsconfig.test.json first came back exit 2 with TS2307 on @object-ui/*. ⛔ It did not report that as a failing gate: it identified a missing prerequisite, built the dependency closure, and re-ran to exit 0 — then proved the check was non-vacuous with --listFiles, which names each edited file exactly once. ⭐ Prerequisite not met is a failed read, not a reading — the same rule this seat applies to its own probes.
One design note, recorded because the dev raised it rather than hid it
The afterEach asserts before cleanup(), matching batch 4's 16-file precedent verbatim. Asserting after would additionally catch an escape raised by the act-flush — ⭐ but inventing a third shape in this family is the disease this lane keeps carding, and the root guard's own afterEach still runs after this file's teardown and reds any real socket in that window. ⇒ ⛔ correct call, and correctly surfaced instead of silently chosen.
Next
ci-wait on d8a971b72, then flip, post-flip guard, arm SQUASH, enqueue from the timeline event.
Landing probe, scoped now: on a re-fetched origin/main, each file carries exactly one vi.stubGlobal (⛔ not two), imports cleanup, and calls it before vi.unstubAllGlobals() — SERVED_ROUTES and the non-served expect are present in both files; nonsense control exits 1.
Generated by Claude Code
Armed — objectui#8661, 2026-09-08T21:00Z
Landing probe, restated before the merge:
Generated by Claude Code |
LANDED — objectui#8033 via PR objectui#8661, merged
|
marketplaceDisabled |
aiStudioDisabled |
|
|---|---|---|
vi.stubGlobal sites |
1 (:163) |
1 (:191) |
cleanup() |
:237 |
:269 |
vi.unstubAllGlobals() |
:238 |
:270 |
| ⭐ order: cleanup BEFORE unstub | ✅ | ✅ |
imports cleanup |
✅ | ✅ |
firing control — SERVED_ROUTES |
2 refs | 2 refs |
Nonsense control: ✅ exit 1.
⭐ One vi.stubGlobal per file is the count that matters: there were four sinks across the two files, and a fix that converted only bootOn's would have left two behind while looking finished. Four collapsed into two routers, one per file.
What is now true that was not
Teardown order. Vitest runs afterEach hooks in reverse registration order, so a teardown written in the test file ran before the root setup's RTL cleanup() — restoring the real fetch while the tree was still mounted, so any read that cleanup()'s act-flush triggered reached a real socket. From 5798df42a both files call cleanup() first, matching the pairing objectui#7307's batches installed in 16 files.
The sink is gone. bootOn() used to answer every URL with the runtime-config body. ⭐ A sink has no record of what it was handed and no assertion that it served only what it meant to — so a future new escape from HomePage would have been silently answered instead of going red. Now a recording router serves exactly /api/v1/runtime/config and /api/v1/meta/_drafts, records everything else, and an afterEach asserts nothing outside that set was requested.
⭐ The evidence, because there is still no gate over these two files
Precondition 4 came back stronger than the card stated: not only are these files not on the network-escape ledger — ⭐ there is no ledger. KNOWN_ESCAPES was retired when objectui#7307 reached zero, and a test now pins its absence. ⇒ nothing external can prove this change, which is why the PR had to carry its own proof:
- Leg A (non-vacuity) — demand the recorded set be empty ⇒ 14/14 red, each case enumerating exactly the two served routes. ⇒ the router really records, and really sees both.
- Leg B (escape probe) — inject an unexpected
fetch⇒ 14/14 red, each naming the URL. ⭐ Under the old sink that same probe is answeredok: trueand stays green. ⇒ the defect reproduced and closed in one measurement.
Both proven on disk by anchor count and blob hash, restored under a trap, restoration proven by blob equality against HEAD — ⛔ not by an exit code.
One design note kept on the record
The afterEach asserts before cleanup(), matching batch 4's 16-file precedent verbatim. Asserting after would additionally catch an escape raised by the act-flush — ⭐ but inventing a third shape in this family is the disease this lane keeps carding, and the root guard's own afterEach still runs after this file's teardown and reds any real socket in that window.
Generated by Claude Code
Fixes #8033
The two
console/homeHomePage suites that boot the REAL runtime-config module eachinstalled a blanket
fetchsink and tore it down in a locally registeredafterEachwith nocleanup()in front of it. Both defects land here, together —ordering without routing leaves the sink, routing without ordering leaves the socket
window.
Preconditions, re-measured on
4dc80d0fcbefore a line was writtenafterEach, neither imports or callscleanupgrep -n cleanupexits 1 in both files. The zero has its control in the same run: the same query findsvi.unstubAllGlobalsatmarketplaceDisabled:139/aiStudioDisabled:171andvi.stubGlobalat:109+:199/:137+:263.vi.stubGlobal('fetch', …)sites, not twoHomePage.tsx:209callsusePendingDrafts({})insidePendingDraftsBanner, mounted atHomePage.tsx:401and:487; the hook fetches/api/v1/meta/_draftsatusePendingDrafts.ts:48from its mount effect (:116viarefreshat:90), with the GLOBALfetchand noapiFetchseam.KNOWN_ESCAPESwas retired on4b9fe9819when the burn-down reached zero, andscripts/__tests__/network-escape-ledger.test.tsnow pins its ABSENCE. Neither filename appears anywhere outside its own directory (grepexit 1); the control for that query —HomePage.approvalsTarget— does return a hit, so the zero is a reading.packages/app-shell/src/console/home/__tests__/HomePage.approvalsTarget.test.tsx(objectui#7307 batch 4, PR #8032). No third shape was invented.The four stub sites and what each became
4dc80d0fc)HomePage.marketplaceDisabled.test.tsx:109(bootOn){ ok: true, status: 200 }+ the runtime-config body for every urlbootOnnow only setsconfigAnswerand awaitsinitRuntimeConfig()HomePage.marketplaceDisabled.test.tsx:199(fail-open case){ ok: false, status: 404 }for every urlawait bootOnNoConfig()— drives the SAME router, whose/runtime/configleg answers 404HomePage.aiStudioDisabled.test.tsx:137(bootOn)HomePage.aiStudioDisabled.test.tsx:263(fail-open case)await bootOnNoConfig()After the change each file contains exactly one
vi.stubGlobal— insideinstallHomeRouter(), registered by a module-scopebeforeEachso it covers everydescribeblock in the file.The router
Two routes, matched on the PATHNAME (
usePendingDraftsappends a?packageId=scopefor package-bound callers; the full url is what gets recorded):
GET /api/v1/runtime/config→ the per-caseconfigAnswer, set bybootOn(body)orbootOnNoConfig()beforeinitRuntimeConfig().installHomeRouterresets it tothe 404 a runtime predating the endpoint gives, so a case that never boots cannot
silently inherit the previous case's payload.
GET /api/v1/meta/_drafts→ a known-EMPTY ledger in the{ drafts: [...] }envelopefetchPendingDraftsreads. Empty rather than seeded is load-bearing:PendingDraftsBannerrendersnullfor bothcount === null(what the sink'sunparseable answer produced) and
count === 0, so no assertion moves.{ ok: false, status: 404 }, and recorded.afterEachthen, in this order: assert nothing outside the served set was requested;cleanup();vi.unstubAllGlobals();resetRuntimeConfigForTesting(). Thecleanup()-before-unstub pairing is the remedy the network-escape guard's own errortext spells out, and the ordering objectui#7439 landed — installed in 16 files by
objectui#7307's batches. No gate covers these two files, so that assertion is this
PR's own evidence.
The assertion can fail — demonstrated, restore proven by blob hash
Both legs ran from one script whose
trap ... EXIT INT TERMrestores both paths fromHEAD, with absolute paths throughout, on the COMMITTED tree (
2aeecf4f8), with themutation proven on disk by anchor counts and blob hashes rather than by an exit code.
Leg A — non-vacuity: the router really is handed both routes. The
afterEachlinewas mutated to demand the recorded set be EMPTY. Mutation on disk: injected anchor
count 1, removed anchor (
SERVED_ROUTES.includes) count 0, blobs1e547fa8 → 0d97c54bandec59bf4f → ed63ecfc. Red as predicted — exit 1,Test Files 2 failed (2),Tests 14 failed (14), every case enumerating exactly:So the served set is exactly the set requested, in all 14 cases, and neither route is
decoration.
Leg B — the escape probe: an unexpected endpoint reds.
void fetch('/api/v1/ABLATION/unexpected-endpoint')was injected into each file'srenderHome(). Mutation on disk: injected anchor count 1 in each, blobs1e547fa8 → 62ccca59andec59bf4f → 39de5a39. Predicted direction before running:red, naming the unexpected url. OBSERVED red — exit 1,
Test Files 2 failed (2),Tests 14 failed (14), every case:Under the old sink that same probe is answered
{ ok: true }with the runtime-configbody and the suites stay green. That is the whole difference between a sink and a
router, and it is what the card is about.
Restore proven by observation, not by an exit code. After each leg, both paths'
git hash-objectare byte-equal to theirHEADblobs —1e547fa8…andec59bf4f…—and
git diff HEAD --name-onlyis empty. No dist/preflight leg is owed: these are thevitest projects' own test files, transformed from source.
Checks — exit codes captured by redirect-then-capture, never through a pipe
All on the final head
d8a971b72, working tree clean.pnpm exec vitest run packages/app-shell/src/console/home/ scripts/__tests__/network-escape-ledger.test.ts— exit 0,Test Files 13 passed (13),Tests 75 passed (75), and zero lines matchingNetwork escapeorECONNREFUSEDin the whole run. The two files under change are14 passed, the same 14 as before the change: the router adds no case and removes none.pnpm --filter @object-ui/app-shell run type-check— exit 0 (tsc --noEmit && tsc -p tsconfig.test.json). Proven non-vacuous rather than assumed:tsc -p tsconfig.test.json --listFilesnames each edited file exactly once. Its first reading on the freshly installed tree was TS2307 on@object-ui/*— a missing prerequisite, not a red gate;pnpm --filter '@object-ui/app-shell^...' run build(exit 0) is what it was owed, and it is green on the built tree.eslintover both edited files — exit 0,--format jsonreports 2 files linted, 0 errors, 2 warnings. Neither warning is mine: both are the pre-existingno-explicit-anyon theuseObjectLabelmock, atmarketplaceDisabled:53andaiStudioDisabled:74, unmoved lines strictly before every insertion point in this diff.node scripts/check-changeset-presence.mjs— exit 1 before, exit 0 after: "2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)". The declaration is.changeset/8033-homepage-fetch-router.mdwith an EMPTY frontmatter — the explicit first-class pass for a test-only change under a released package'ssrc/, per AGENTS.md §9. Not a label: objectui declares, it does not exempt.node scripts/check-control-bytes.mjs— exit 0 (6851 tracked text files), plus agrep -naPself-scan of the control range over all three changed paths, no hits.node scripts/check-governed-queue-guard.mjs --testover all changed paths — exit 0, "NOT GOVERNED — 3 path(s) checked against 5 governed surface(s)". This PR nonetheless stays in draft: the dispatch fences it, no auto-merge, no merge.The repo-wide
turbo run lint/turbo run type-checkruns belong to CI and CI is notawaited here — the report is delivered at draft-PR time per the dispatch contract.
Scope
Test files only; no product source is touched, and no assertion in either file changed.
Nothing was skipped, quarantined or silenced. Neither file is on any ledger, so no
ledger arithmetic is owed.
Generated by Claude Code