Skip to content

fix(test-harness): one anchored rule for every @objectstack/spec subpath in nine vitest alias tables - #9703

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-9457-runtime-vitest-spec-subpath-alias
Aug 18, 2026
Merged

fix(test-harness): one anchored rule for every @objectstack/spec subpath in nine vitest alias tables#9703
os-zhuang merged 4 commits into
mainfrom
claude/issue-9457-runtime-vitest-spec-subpath-alias

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#9457

packages/runtime's vitest alias table enumerated twelve @objectstack/spec subpaths by hand. A string find matches by prefix, so with a FILE replacement the bare @objectstack/spec entry swallowed every published subpath the list had not reached and resolved it to …/spec/src/index.ts/(sub)ENOTDIR, at run time, from a config that reads as correct.

Reproduced on origin/main (ca2e020e4) before touching anything, from a throwaway test in packages/runtime that did nothing but await import('@objectstack/spec/cloud'):

Error: ENOTDIR: not a directory, open
'/home/user/objectstack-issue-9457/packages/spec/src/index.ts/cloud'

A Zone-2 assumption was falsified. The card names one missing subpath; the enumeration had gone stale in three places. cloud, integration and studio are all published subpath exports of @objectstack/spec with no entry in the table. integration is already reached by src/http-dispatcher.test.ts — as an import type, erased before resolution, so it sits one deleted keyword away from the same ENOTDIR.

The one-liner was the wrong deliverable

The hand-maintained population is the defect, and its harm is diagnostic: the error names whichever module performed the import (measured: MetadataPlugin._parseAndRegisterArtifact), never the alias table. So all nine enumerated configs are converted to the anchored-regex shape that packages/qa/downstream-contract (PR #8129), service-knowledge, service-settings and plugin-audit already carry:

{find: /^@objectstack\/spec\/([a-z-]+)$/,replacement: path.join(path.resolve(__dirname,'..'),'spec/src/$1/index.ts'),},{find: /^@objectstack\/spec$/,replacement: path.resolve(__dirname,'../spec/src/index.ts')},

Why PR #8128's shape transfers — checked, not assumed.platform-objects needs a hand-written entry ahead of its namespace rule because ./plugin is src/plugin.ts, a FILE, while every other subpath is a directory. @objectstack/spec's export map is uniform: all fifteen published namespaces are dist/(ns)/index.*, i.e. src/(ns)/index.ts in source, with no file-shaped subpath at all. The two non-namespace keys (./openapi.json, ./package.json) contain a dot, which ([a-z-]+) does not match, so they now fall through to node resolution and land on the real files instead of being mangled by the old prefix-matching entry.

Converting the object form to the array form is resolution-neutral by construction: Vite normalises an alias object into exactly this list, in this order, first match wins. Every specifier that resolved before resolves to the same file; the only specifiers whose behaviour changes are the ones that were failing.

The sweep — full population, and how it was enumerated

find . -name 'vitest.config.*' outside node_modules/.git36 configs, all classified. Counts are echoed rather than inferred from a silent filter, and the zero-hit half carries a control: of the 23 configs that mention no @objectstack/spec, two (packages/adapters/hono, packages/triggers/trigger-record-change) do alias other @objectstack/* packages, so the grep discriminates rather than missing quietly, and 25 of the 36 have an alias block at all.

countconfigs
alias @objectstack/specenumerated, converted here9runtime, metadata, drivers/driver-memory, drivers/driver-sql, plugins/embedder-openai, plugins/knowledge-memory, plugins/knowledge-ragflow, plugins/plugin-dev, plugins/plugin-hono-server
alias @objectstack/spec — already an anchored regex, unchanged4qa/downstream-contract, plugins/plugin-audit, services/service-knowledge, services/service-settings
do not alias @objectstack/spec23

packages/metadata is the one config that already listed cloud — which is precisely why the failure surfaced there and pointed away from the table that was actually wrong.

The type-axis twin was swept too, and needed nothing. Exactly one tsconfig.json in the repo enumerates spec subpaths in paths (packages/qa/downstream-contract), and it is already a star pattern ("@objectstack/spec/*": ["../../spec/src/*/index.ts"]), not a list. pnpm check:type-source-resolution is green.

The pin is over the rule, not over cloud

packages/runtime/src/spec-subpath-alias-coverage.pin.test.ts derives its population from @objectstack/spec's published exports map and checks three axes:

  1. Resolution really happens — every published namespace subpath is import()ed, so this is Vite's own resolution and not a re-implementation of it. This is the axis that fails with ENOTDIR.
  2. It lands on source, not dist — axis 1 alone would stay green with the alias table deleted outright, because the specifiers would then resolve through exports to packages/spec/dist. The discriminator is @objectstack/spec/conversions, which exists in the source tree and is deliberately absent from the exports map, so through exports it is ERR_PACKAGE_PATH_NOT_EXPORTED and can only arrive via a source alias. Same discriminator packages/qa/downstream-contract/test/source-resolution.pin.test.ts uses.
  3. The rule stays a rule — the config's own alias array is read and Vite's first-match-wins resolution simulated over it, asserting the winner produces …/spec/src/(namespace)/index.ts, plus a case that rejects any string find beginning with @objectstack/spec. That is what goes red on a revert to the enumerated shape, instead of staying green until somebody reaches an unlisted namespace.

The population is read through node's own resolution of the dependency (createRequire(...).resolve('@objectstack/spec/package.json')), never a repo-relative path climbing out of the package — that is the read pnpm check:cross-package-test-inputs exists to keep declared, and an installed dependency reached by its package specifier is covered by the ordinary dependency edge instead. That gate is green with no new declaration. Both import() specifiers are held in variables rather than written as literals, so a vitest-alias defect cannot surface as a tsc error or be masked by tsconfig.json; this file is inside the program check:type-check-debt re-measures.

Shown failing without the fix

Predictions were written down before each run, and the second run exists because the first one caught a defect in the pin itself.

Run 1 — predicted 4 fail / 2 pass; actual 4 fail / 2 pass, but one of the four failed for the wrong reason: imports every one of them hit vitest's 5000ms default timeout rather than reporting the resolution failure. Cold-transforming fifteen spec namespaces measures ~5.3s, so the case was flaky green-path when the file ran alone and — worse — reported Test timed out in 5000ms on the red path. That is the same points-at-the-wrong-thing diagnostic this card is about, reproduced inside its own pin. Fixed with an explicit 60s timeout, and the reason is written into the file rather than left as a bare number.

Run 2, on the final tree, predicted before running:

casepredictedactual
fixed tree, pin file alone6/6 pass6/6 pass
reverted, finds a populationpasspass
reverted, imports every one of themfail, naming exactly cloud / integration / studiofail, naming exactly those three
reverted, resolves a namespace exports does NOT publishfail (conversions is unlisted too)fail, ENOTDIR … /spec/src/index.ts/conversions
reverted, maps every published subpath …fail, first mismatch at cloudfail at cloud: expected …/spec/src/cloud/index.ts, received …/spec/src/index.ts/cloud
reverted, keeps the bare package entry on sourcepasspass
reverted, refuses a prefix-matching string entryfail, first at @objectstack/spec/aifail at @objectstack/spec/ai
AssertionError: published subpaths this vitest config cannot resolve: expected [ …(3) ] to deeply equal []
+ "@objectstack/spec/cloud: ENOTDIR: not a directory, open '…/packages/spec/src/index.ts/cloud'",
+ "@objectstack/spec/integration: ENOTDIR: not a directory, open '…/packages/spec/src/index.ts/integration'",
+ "@objectstack/spec/studio: ENOTDIR: not a directory, open '…/packages/spec/src/index.ts/studio'",

Reverted with git checkout origin/main -- packages/runtime/vitest.config.ts, restored with git checkout claude/issue-9457-runtime-vitest-spec-subpath-alias -- packages/runtime/vitest.config.ts; git status clean afterwards, confirmed in the same script.

Blast radius

This change alters how every test in ten packages resolves @objectstack/spec, so the suites were run rather than reasoned about. All of it at d9d7580a4, after the second origin/main merge, with the workspace closure built first (turbo run build --filter=./packages/* --filter=./packages/*/*, 70 tasks):

packageresult
@objectstack/runtime171 files, 2534 tests, all passing
@objectstack/driver-memory25 files, 760 tests
@objectstack/driver-sql101 files + 5 skipped, 1761 tests + 62 skipped
@objectstack/metadata31 files, 603 tests
@objectstack/embedder-openai1 file, 14 tests
@objectstack/knowledge-memory1 file, 8 tests
@objectstack/knowledge-ragflow1 file, 9 tests
@objectstack/plugin-dev5 files, 54 tests
@objectstack/plugin-hono-server18 files, 211 tests

pnpm --filter @objectstack/runtime typecheck exit 0. Gates run locally on this same head, all exit 0: check:test-source-alias, check:cross-package-test-inputs, check:type-source-resolution, check:nul-bytes, check:query-options-erasure, check:engine-double-contract, check:where-matcher, check:type-check-coverage, scripts/docs-audit/check-affected-docs.mjs, and the ratchet check:type-check-debt --re-measure"33 ledger entries re-measured, 1926 raw tsc errors total, none above its recorded number; surplus: none". That last one matters here: @objectstack/runtime's TEST_DEBT entry sits exactly at its measurement with no slack, so the new test file adding a single type error would have been red.

Changeset

skip-changeset. The diff is nine vitest.config.ts files and one *.test.ts. Neither is publishable package source under this repo's convention: tsup builds each package from its src entry point and ships dist/, so a vitest config is never compiled into any published artifact and a *.test.ts is in no package's entry graph. This PR releases nothing.

Out of scope, filed not fixed

#9674 — the same class one package over. Those same configs alias bare @objectstack/core and @objectstack/types with prefix-matching string entries, and both publish a FILE-shaped subpath (./logger, ./node). The #9457 shape deliberately does not transfer there (a ([a-z-]+) rule would send @objectstack/core/logger to src/logger/index.ts), and the fix has to move check:test-source-alias's registry with it, so it is its own card rather than a rider here.

Generated by Claude Code


Generated by Claude Code

zhuangjianguoand others added 4 commits August 18, 2026 14:42
…bpath in the vitest alias tables (#9457)
`packages/runtime`'s vitest alias table enumerated twelve `@objectstack/spec`
subpaths by hand and had gone stale in three places — `cloud`, `integration`
and `studio` are published subpath exports with no entry. A string `find`
matches by PREFIX, so with a FILE replacement the bare `@objectstack/spec`
entry swallowed each of them and resolved it to `…/spec/src/index.ts/<sub>`:
`ENOTDIR`, at run time, from a config that reads as correct.
The one-line alias is not the fix; the hand-maintained population is the
defect. All nine enumerated configs are converted to the anchored-regex shape
`packages/qa/downstream-contract` (PR #8129), `service-knowledge`,
`service-settings` and `plugin-audit` already carry — one rule for every
namespace, which cannot go stale as tests reach new subpaths.
`packages/runtime/src/spec-subpath-alias-coverage.pin.test.ts` pins the RULE:
it derives the subpath population from spec's published `exports` map, so a
namespace this rule stops covering fails there rather than surfacing as an
`ENOTDIR` inside whichever plugin performed the import.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WKSnqAaoqtW3QX7SSf1Vk
…a cold Vite transform (#9457)
Measured in both directions: the fifteen namespace imports take ~5.3s on a
cold transform, against vitest's 5000ms default, so the case reported
`Test timed out in 5000ms` instead of the resolution failure it exists to
name. That is the points-at-the-wrong-thing diagnostic this card is about,
reproduced inside its own pin.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WKSnqAaoqtW3QX7SSf1Vk
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 9 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 9 changed file(s) yielded no anchor (packages/drivers/driver-memory/vitest.config.ts, packages/drivers/driver-sql/vitest.config.ts, packages/metadata/vitest.config.ts, …) — pages documenting those are invisible to this run

Coarse fallback — 36 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 52182a69c51a2f9d49aeba91428c5607ce64bb6apackageMentionDocs.

@os-zhuangos-zhuang added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed tests labels Aug 18, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review August 18, 2026 16:06
@os-zhuang
os-zhuang enabled auto-merge August 18, 2026 16:07
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PM review — accepted, ready + auto-merge armed. And a scope worry of mine, checked and withdrawn.

domain:cli seat, session session_012WKSnqAaoqtW3QX7SSf1Vk. Dev returned status: done, no open questions. CI was in_progress at review; auto-merge cannot fire until all six required checks pass.

⛔ My own concern, raised and then withdrawn on the record

Seeing nine vitest.config.ts files across driver-memory, driver-sql, packages/metadata, embedder-openai, knowledge-* — packages belonging to domain:drivers, domain:metadata and domain:services — I flagged this as a possible cross-domain surface breach before reading back the card.

It is not, and the authorisation predates both me and the dev. The triage seat's grading comment set this scope explicitly:

"Dispatch scope: the one-line alias plus the card's own follow-through — … and sweep sibling vitest configs with the same enumerated list. The regex conversion is the actual fix for the class; the one-liner alone re-arms the same trap for the next subpath."

and my own claim declared it forward: "plus the sibling vitest configs the sweep names." These are test-harness configs, not package source; Clause-② is no. Nothing crossed a lane line. Recording the retraction rather than quietly dropping it, because an unwithdrawn suspicion in a review thread reads as a finding.

⭐ The detail worth keeping: the numbers matched and one of them was still wrong

Run 1 of the pin predicted 4 fail / 2 pass on the reverted config and observed exactly 4 fail / 2 pass. The dev did not stop there — it checked why each failed, and found one had died on vitest's 5000ms default timeout (fifteen cold Vite transforms measure ~5.3s) rather than naming the resolution failure.

That is this card's own defect reproduced inside its own pin: a failure that points at the wrong thing. The whole card exists because ENOTDIR pointed at the metadata plugin instead of the alias table. It fixed it with an explicit 60s timeout and a comment saying why, then re-predicted and matched run 2 exactly — with the reverted tree now naming cloud, integration and studio and their actual ENOTDIR paths.

⭐ A matching count is not a passing verification. This is the clearest instance of that all day, and the dev caught it on itself.

The fix is the class, not the instance

The card asked for one missing alias. The enumerated-list shape is the defect: a bare @objectstack/spec entry swallowed every published namespace, resolving @objectstack/spec/cloud to …/spec/src/index.ts/cloud. One anchored rule (/^@objectstack\/spec\/([a-z-]+)$/) now covers all fifteen namespaces and cannot go stale.

Checked rather than assumed: PR #8128's shape transfers because spec's export map is uniform — no FILE-shaped subpath of the platform-objects/plugin kind — so no exception entry is needed. The two dotted keys (openapi.json, package.json) fall outside [a-z-]+ and now reach node resolution instead of being mangled.

Sweep was exhaustive and classified, not just converted: 36 configs repo-wide — 9 converted, 4 already anchored, 23 with no spec alias. The type axis was swept too (exactly one tsconfig.json enumerates spec subpaths; already a star pattern).

Zone 2, falsified in the useful direction

The card's premise understated itself: three published subpaths were missing (cloud, integration, studio), not one — and integration is already imported by src/http-dispatcher.test.ts as an import type, erased before resolution. One deleted keyword from the same failure. That is a latent trap the card did not know it had.

Two more things handled correctly

  • skip-changeset applied and READ BACK (['size/l','skip-changeset']). Nothing publishable changes and check-empty-changeset.mjs rejects an empty one — the conditional rule this lane got wrong twice today and wrote down.
  • A harness/dispatch conflict, surfaced rather than resolved silently: the harness default asks for a model identifier in the commit trailer; the dispatch forbids any model identifier in a pushed artifact. It followed the dispatch. That is correct — the no-model-identifier rule binds on anything pushed to a repository, and Co-Authored-By: Claude plus the session trailer is the right form. Flagging the conflict instead of picking one silently is exactly right.

Out-of-scope finding #9674 filed unassigned, no pm:queue, with the reason the regex shape deliberately does not transfer there (@objectstack/core and @objectstack/types publish FILE-shaped subpaths). ⛔ Not graded by this seat.


Generated by Claude Code

@os-zhuang
os-zhuang added this pull request to the merge queueAug 18, 2026
Merged via the queue into main with commit 278340dAug 18, 2026
32 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-9457-runtime-vitest-spec-subpath-alias branch August 18, 2026 16:41
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

packages/runtime's vitest alias list omits @objectstack/spec/cloud, so any runtime test that loads a compiled artifact dies with ENOTDIR

3 participants

@os-zhuang@zhuangjianguo@claude