Skip to content

test(app-shell): route the two HomePage runtime-config doubles instead of sinking every url (objectui#8033) - #8661

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-8033-homepage-fetch-stub-router
Sep 8, 2026
Merged

test(app-shell): route the two HomePage runtime-config doubles instead of sinking every url (objectui#8033)#8661
baozhoutao merged 2 commits into
mainfrom
claude/issue-8033-homepage-fetch-stub-router

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #8033

The two console/home HomePage suites that boot the REAL runtime-config module each
installed a blanket fetch sink and tore it down in a locally registered
afterEach with no cleanup() 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 4dc80d0fc before a line was written

# claim reading
1 both files unstub in a local afterEach, neither imports or calls cleanup CONFIRMED. grep -n cleanup exits 1 in both files. The zero has its control in the same run: the same query finds vi.unstubAllGlobals at marketplaceDisabled:139 / aiStudioDisabled:171 and vi.stubGlobal at :109+:199 / :137+:263.
2 there are four vi.stubGlobal('fetch', …) sites, not two CONFIRMED, at exactly the four lines above — triage's re-measured numbers, unchanged.
3 the second route is real CONFIRMED. HomePage.tsx:209 calls usePendingDrafts({}) inside PendingDraftsBanner, mounted at HomePage.tsx:401 and :487; the hook fetches /api/v1/meta/_drafts at usePendingDrafts.ts:48 from its mount effect (:116 via refresh at :90), with the GLOBAL fetch and no apiFetch seam.
4 neither file is on the network-escape ledger CONFIRMED, and stronger: there is no ledger. KNOWN_ESCAPES was retired on 4b9fe9819 when the burn-down reached zero, and scripts/__tests__/network-escape-ledger.test.ts now pins its ABSENCE. Neither filename appears anywhere outside its own directory (grep exit 1); the control for that query — HomePage.approvalsTarget — does return a hit, so the zero is a reading.
5 the reference router shape exists in batch 4 CONFIRMED. Copied from 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

site (on 4dc80d0fc) was is
HomePage.marketplaceDisabled.test.tsx:109 (bootOn) blanket sink: { ok: true, status: 200 } + the runtime-config body for every url the shared recording router; bootOn now only sets configAnswer and awaits initRuntimeConfig()
HomePage.marketplaceDisabled.test.tsx:199 (fail-open case) second blanket sink: { ok: false, status: 404 } for every url await bootOnNoConfig() — drives the SAME router, whose /runtime/config leg answers 404
HomePage.aiStudioDisabled.test.tsx:137 (bootOn) blanket sink the shared recording router
HomePage.aiStudioDisabled.test.tsx:263 (fail-open case) second blanket sink await bootOnNoConfig()

After the change each file contains exactly one vi.stubGlobal — inside
installHomeRouter(), registered by a module-scope beforeEach so it covers every
describe block in the file.

The router

Two routes, matched on the PATHNAME (usePendingDrafts appends a ?packageId= scope
for package-bound callers; the full url is what gets recorded):

  • GET /api/v1/runtime/config → the per-case configAnswer, set by bootOn(body) or
    bootOnNoConfig() before initRuntimeConfig(). installHomeRouter resets it to
    the 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: [...] } envelope
    fetchPendingDrafts reads. Empty rather than seeded is load-bearing:
    PendingDraftsBanner renders null for both count === null (what the sink's
    unparseable answer produced) and count === 0, so no assertion moves.
  • anything else → { ok: false, status: 404 }, and recorded.

afterEach then, in this order: assert nothing outside the served set was requested;
cleanup(); vi.unstubAllGlobals(); resetRuntimeConfigForTesting(). The
cleanup()-before-unstub pairing is the remedy the network-escape guard's own error
text 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 TERM restores both paths from
HEAD, with absolute paths throughout, on the COMMITTED tree (2aeecf4f8), with the
mutation 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 afterEach line
was mutated to demand the recorded set be EMPTY. Mutation on disk: injected anchor
count 1, removed anchor (SERVED_ROUTES.includes) count 0, blobs
1e547fa8 → 0d97c54b and ec59bf4f → ed63ecfc. Red as predicted — exit 1,
Test Files 2 failed (2), Tests 14 failed (14), every case enumerating exactly:

AssertionError: expected [ '/api/v1/runtime/config', …(1) ] to deeply equal []
+ [
+   "/api/v1/runtime/config",
+   "/api/v1/meta/_drafts",

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's
renderHome(). Mutation on disk: injected anchor count 1 in each, blobs
1e547fa8 → 62ccca59 and ec59bf4f → 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:

AssertionError: expected [ Array(1) ] to deeply equal []
+   "/api/v1/ABLATION/unexpected-endpoint",

Under the old sink that same probe is answered { ok: true } with the runtime-config
body 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-object are byte-equal to their HEAD blobs — 1e547fa8… and ec59bf4f…
and git diff HEAD --name-only is empty. No dist/preflight leg is owed: these are the
vitest 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 matching Network escape or ECONNREFUSED in the whole run. The two files under change are 14 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 --listFiles names 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.
  • eslint over both edited files — exit 0, --format json reports 2 files linted, 0 errors, 2 warnings. Neither warning is mine: both are the pre-existing no-explicit-any on the useObjectLabel mock, at marketplaceDisabled:53 and aiStudioDisabled: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.md with an EMPTY frontmatter — the explicit first-class pass for a test-only change under a released package's src/, 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 a grep -naP self-scan of the control range over all three changed paths, no hits.
  • node scripts/check-governed-queue-guard.mjs --test over 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-check runs belong to CI and CI is not
awaited 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

…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
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3477.5 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-DqTtGmW_.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 498.93KB 114.12KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 198.39KB 55.29KB
fields (index.js) 243.73KB 61.53KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.53KB 46.34KB
plugin-dashboard (index.js) 131.43KB 34.44KB
plugin-designer (index.js) 213.21KB 43.63KB
plugin-detail (index.js) 251.25KB 65.00KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.30KB 56.63KB
plugin-kanban (index.js) 55.44KB 15.73KB
plugin-list (index.js) 112.73KB 27.69KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.54KB 20.84KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

ACCEPT — objectui#8033 / PR objectui#8661, domain:devx @ objectui PM seat, 2026-09-08T20:52Z

Reviewed against GitHub at head d8a971b72, ⛔ not against the report's narrative.

What this seat re-measured

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 answered ok: true and 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()⚠️ checked by line order, since both tokens would be present either way; ⭐ firing control: SERVED_ROUTES and the non-served expect are present in both files; nonsense control exits 1.


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 8, 2026 20:59
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 8, 2026
@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Armed — objectui#8661, 2026-09-08T21:00Z

step reading
CI on d8a971b72 30 success, 3 skipped, 0 pending, 0 failures
flipped ready ready_for_review@2026-09-08T20:59:24Z
⚠️ post-flip guard the flip spawned a new Governed Surface Queue Guard; the post-flip run is completed/success@2026-09-08T20:59:45Z
armed enable_pr_auto_merge SQUASH
⭐ enqueue confirmed from the added_to_merge_queue timeline event, ⛔ not the auto_merge field

Landing probe, restated before the merge:

  • Each of the two test files carries exactly one vi.stubGlobal⚠️ one, because the defect was four sinks and a fix that converted only bootOn's would leave two behind while looking done.
  • Each imports cleanup and calls it before vi.unstubAllGlobals()⚠️ checked by line order, ⛔ not by presence: both tokens are present either way, and the whole defect was their order.
  • Firing control: SERVED_ROUTES and the non-served expect(fetchCalls.filter(…)) are present in both files — the assertion that makes the router a router rather than a quieter sink.
  • Nonsense control: exit 1.

Generated by Claude Code

Merged via the queue into main with commit 5798df4 Sep 8, 2026
35 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-8033-homepage-fetch-stub-router branch September 8, 2026 21:17
@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

LANDED — objectui#8033 via PR objectui#8661, merged 5798df42a

merged_at 2026-09-08T21:17:37Z; merged and removed_from_merge_queue share that timestamp with merged: true ⇒ that removal is the merge, ⛔ not a dequeue.

Landing probe on a re-fetched origin/main (tip 5798df42a)

⚠️ Read as LINE ORDER, ⛔ never presence — both cleanup and vi.unstubAllGlobals were present before this PR too, and the order was the defect. Comment lines excluded from the scan.

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 answered ok: true and 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(app-shell): two console/home HomePage tests tear their fetch stub down BEFORE RTL cleanup, and the stub is a blanket sink rather than a router

2 participants