Skip to content

test(console-starter): record unresolvable relative imports in the alias closure - #5484

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-5386-alias-closure-relative-miss
Aug 21, 2026
Merged

test(console-starter): record unresolvable relative imports in the alias closure#5484
os-support-ai merged 2 commits into
mainfrom
claude/issue-5386-alias-closure-relative-miss

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes #5386

computeClosure() in examples/console-starter/test/vite-alias-closure.test.ts walks two kinds of specifier. The bare-specifier branch pushes a miss onto unresolvable. The relative branch dropped one with no record, so expect(closure.unresolvable).toEqual([]) was not a weak assertion for relative imports — it was a structurally empty one, unable to fail no matter how many the walk failed to follow.

This is the reporting half. The resolution half landed in objectui#5357 and is already on main.

Re-measured on current main, not taken from the card

Measured with the walker's own logic against origin/main at 490f482:

reading current main with the resolver ablated to its pre-objectui#5357 behaviour
filesWalked 1245 402
packages reached 29 22
relative specifiers dropped 0 275
closure.unresolvable reports [] []

The bottom row is the defect. Ablating the resolver reproduces the objectui#4538 / objectui#5214 regression class on today's tree: 275 specifiers vanish, 28% of the walk is lost, and the assertion that exists to report exactly that still reads []. Only the filesWalked floor could ever see it, and the card's history shows it had slack to absorb two green landings.

Newly-visible specifiers on this branch: zero, and the assertion stays green

Making the branch record misses surfaces 0 previously-invisible specifiers, and expect(closure.unresolvable).toEqual([]) stays green. No exemption, allowlist or suppression was added to achieve that — the count is genuinely zero because objectui#5357's resolver repair is already on main, so every one of the 1914 .js and 1018 extensionless relative specifiers in the closure resolves.

A zero is only a reading if the method can still report a non-zero, so both directions are counter-probed below rather than asserted.

The asset-specifier boundary, and where the line is drawn

ts.preProcessFile reports every import specifier, including ./styles.css and ./logo.svg, which resolveModule cannot resolve by design — its candidate list is JS and TS only. Recording those identically would manufacture failures that are not defects.

The line is drawn on intent, not on whether resolution happened to succeed:

  • module — no extension, or one of the JS/TS emitted extensions (MODULE_EXTENSIONS is derived from RESOLVABLE so the two cannot drift, plus .mts / .cts). Recorded in unresolvable, which is asserted empty.
  • not a module — any other extension, and any specifier carrying a ?query or #hash (a Vite resource specifier such as ?raw, ?url, ?inline). Recorded in a separate nonModuleSkipped list, which is never asserted empty.

Drawing it on intent rather than on outcome matters here. The closure contains two .css imports today, and they resolve only because resolveModule returns any path that exists on disk verbatim — a stylesheet that is virtual, generated, or shipped only in dist would land in the miss branch the moment it appeared. Assets are skipped explicitly into a list rather than by falling off the end of a branch, so the skip stays auditable and this branch cannot go quiet by accident again.

Reverse verification

Every leg predicted its direction before running. This file is executed from source by vitest and imports no package dist, so no rebuild separates the edit from the measurement.

Leg A — re-blind the relative branch (restore the bare continue).
Predicted: the two counter-probe fixtures go red; the real-tree assertions stay green, because a healthy tree has zero misses.
Observed, exactly: 2 failed | 7 passed, with expected [] to have a length of 2 and expected [] to have a length of 5 — and reaches the workspace graph it is meant to cover passed. That asymmetry is the card's whole point: the production assertion cannot detect its own blinding. Only the fixtures can.

Leg B — ablate the resolver's .js to source fallback.
Predicted: 3 red / 6 green, and the real-tree test fails on its floor assertion, which is written first and short-circuits the list.
Observed, exactly: 3 failed | 6 passed, expected 402 to be greater than 500.

Leg B-prime — same ablation, floor neutralized so the list assertion's own message is exposed.
Predicted: the list now names roughly 275 specifiers where pre-fix it stayed [].
Observed: AssertionError: expected [ …(275) ] to deeply equal [], each entry naming the specifier and its importer:

"./AuthProvider.js (unresolvable relative import, imported by packages/auth/src/index.ts)"
"./useAuth.js (unresolvable relative import, imported by packages/auth/src/index.ts)"

The gate now reports the regression as itself instead of as a number that drifted toward a floor.

Restore — both mutations reverted via git checkout, git status and git diff HEAD both empty (byte-identical to the commit), zero ablation markers left in the file, and the suite re-run green at 9/9.

Pinning the class by fixture

Four fixtures drive the real computeClosure over throwaway trees built in mkdtemp, so a future edit cannot silently re-blind the branch:

  • ./Foo.js walked through to the Foo.tsx on disk, subtree intact — the case that produced every row of the table above.
  • planted unresolvable modules of both spellings (./missing-module.js and extensionless ./gone) named in unresolvable — the counter-probe for the zero.
  • five assets kept out of unresolvable while still accounted for in nonModuleSkipped.
  • the classifier's boundary spellings, including . and .. as extensionless directory imports.

computeClosure() gained an optional entry-directory parameter to make this possible; the production call site is unchanged.

Gates, all on final head 14c1b5e

gate result
vitest run --project unit examples/console-starter/test/vite-alias-closure.test.ts 9 passed (5 pre-existing + 4 new)
pnpm --filter @object-ui/example-console-starter type-check exit 0, both tsc invocations echoed including tsconfig.test.json
pnpm --filter @object-ui/example-console-starter lint exit 0
check:control-bytes OK, 4532 tracked files
changeset:check OK
check:esm-specifiers, check:phantom-deps, lint:coverage, type-check:coverage all exit 0

The dependency closure was built first (pnpm --filter '@object-ui/example-console-starter^...' build) — without it type-check fails on unbuilt @object-ui/* declarations in src/, which is unrelated to this diff.

Changeset is empty-frontmatter: no package src/ is touched and the example is private, so this publishes nothing.

No test is skipped, disabled or quarantined. Scope held to the one declared file plus the changeset.

Generated by Claude Code


Generated by Claude Code

claude added 2 commits August 21, 2026 02:12
…ias closure

`computeClosure()`'s walk had two branches. The bare-specifier branch pushed a
miss onto `unresolvable`; the relative branch dropped one with no record, so
`expect(closure.unresolvable).toEqual([])` was structurally empty for that whole
specifier class — it could not fail no matter how many relative imports the walk
failed to follow.

That is what let objectui#4538's and objectui#5214's conversions to explicit
extensions truncate this walk while landing green: the `filesWalked` floor was
the only signal, and it had enough slack to hide two pull requests' worth of
drift before app-shell pushed the count under it.

Record the relative miss the same way, bounded to specifiers that are meant to
be modules — no extension, or a JS/TS emitted extension. Assets (`.css`, images,
fonts, `.json`) and Vite resource queries cannot be resolved by design, so they
are skipped explicitly into `nonModuleSkipped` rather than reported as defects.

Pinned by fixtures over the real walker: `./Foo.js` reaching `Foo.tsx`, planted
misses of both module spellings being named, and assets staying out of
`unresolvable` while remaining accounted for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
…s-only

Empty frontmatter: no package src/ is touched, so this publishes nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
@os-support-ai
os-support-ai marked this pull request as ready for review August 21, 2026 02:54
@os-support-ai
os-support-ai added this pull request to the merge queue Aug 21, 2026
Merged via the queue into main with commit 3c06c75 Aug 21, 2026
22 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-5386-alias-closure-relative-miss branch August 21, 2026 02:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants