Skip to content

test(hono): resolve @objectstack/plugin-hono-server to source in the unit suite - #13339

Merged
os-trump merged 2 commits into
mainfrom
claude/issue-12767-hono-test-source-alias
Aug 30, 2026
Merged

test(hono): resolve @objectstack/plugin-hono-server to source in the unit suite#13339
os-trump merged 2 commits into
mainfrom
claude/issue-12767-hono-test-source-alias

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes#12767

What this changes

packages/adapters/hono/src/index.ts imports createOriginMatcher, hasWildcardPattern, DEFAULT_CORS_ALLOW_HEADERS and DEFAULT_CORS_EXPOSE_HEADERS from @objectstack/plugin-hono-server as values, at module scope. packages/adapters/hono/vitest.config.ts aliased only @objectstack/runtime, so that specifier resolved through the dependency's exports to its dist/ — this package's 74 unit verdicts were a function of another package's build state.

The alias, before and after

Before (object form, one entry):

resolve: {alias: {'@objectstack/runtime': path.resolve(__dirname,'src/__mocks__/runtime.ts'),},},

After (array form; comments trimmed here, the file carries them):

resolve: {alias: [{find: /^@objectstack\/plugin-hono-server$/,replacement: path.resolve(__dirname,'../../plugins/plugin-hono-server/src/index.ts'),},{find: '@objectstack/runtime',replacement: path.resolve(__dirname,'src/__mocks__/runtime.ts'),},],},

The array form is forced by the gate's rule 5, not a style choice: an object-form key matches by PREFIX, so a bare key whose replacement is a FILE also swallows that package's subpaths and resolves them through a file extension — ENOTDIR at run time, in a config that reads as correct. An anchored regex find cannot do that.

The pre-existing @objectstack/runtime entry is carried over unchanged in reach: a bare string find still matches by prefix in the array form, so the kernel mock keeps exactly the surface it had. @objectstack/runtime publishes only . in its exports, so there is no subpath here for that prefix to swallow.

Positive control — the suite really does load source now

Re-running the gate is not verification, and a passing suite does not say which file it loaded. So the alias was measured by mutation. Same mutation, same un-rebuilt dist/, once on each side of the change:

In packages/plugins/plugin-hono-server/src/pattern-matcher.ts, inside createOriginMatcher, if (!requestOrigin) return null; was replaced by if (requestOrigin) return null; — the matcher then rejects every origin. Source only; plugin-hono-server was not rebuilt, so its dist/ kept the correct behaviour throughout.

legplugin-hono-server SOURCEdist rebuiltpnpm --filter @objectstack/hono test
before the aliasmatcher guttednoTest Files 2 passed (2) · Tests 74 passed (74)
after the aliasmatcher guttednoTest Files 1 failed, 1 passed (2) · Tests 4 failed, 70 passed (74)

The four failures after the alias are exactly the cases whose subject that function is:

FAIL src/hono.test.ts > CORS wildcard origin patterns > matches subdomain wildcard (https://*.example.com) for real subdomains
FAIL src/hono.test.ts > CORS wildcard origin patterns > matches port wildcard (http://localhost:*) for any localhost port
FAIL src/hono.test.ts > CORS wildcard origin patterns > matches the correct pattern from a comma-separated wildcard list
FAIL src/hono.test.ts > CORS wildcard origin patterns > responds to preflight OPTIONS with matched wildcard origin
AssertionError: expected null to be 'https://app.example.com' // Object.is equality

That green row is the hazard stated as a measurement: with the specifier resolving to dist/, the whole CORS-wildcard block passes over a createOriginMatcher that has been destroyed in the checkout.

Both mutation legs were proven on disk, never by an exit code: anchor text count 1 to 0 and marker count 0 to 1, plus git hash-object differing from git rev-parse HEAD:PATH. Both restores were proven the same way — git checkout HEAD -- ABSOLUTE_PATH, marker count back to 0, disk hash equal to the HEAD blob, git diff HEAD empty for that path. A first attempt at the mutation was caught by that same count check as a no-op (a perl s/// whose replacement text contained the delimiter) and was re-run, so no reading was taken on an unmutated tree.

Suite result

pnpm --filter @objectstack/hono test at 8ea75a241, alias in place, tree clean:

 Test Files 2 passed (2)
Tests 74 passed (74)
Duration 5.09s (transform 7.72s, setup 0ms, import 9.67s, tests 111ms, environment 0ms)

No breakage was surfaced by the switch to source — nothing was skipped, no it.fails, no revert to dist, no widened ledger. Cost of loading the dependency's source graph, before vs after: wall 4.65s to 5.09s, transform 6.82s to 7.72s, tests themselves unchanged (113ms to 111ms). The load is paid at module top by static imports, not inside a clocked window, so the timeout failure mode that rule exists for does not apply here.

Ledger — it shrank, by exactly one member

KNOWN_UNALIASED_TEST_IMPORTS audits for set EQUALITY, so the alias made the entry stale and the gate named the narrowing itself:

✗ @objectstack/hono: registry entry is STALE — no longer unaliased: @objectstack/plugin-hono-server.
Narrow the entry to exactly: ["@objectstack/types"]

The only registry line that moves in the diff:

- '@objectstack/hono': ['@objectstack/plugin-hono-server', '@objectstack/types'],+ '@objectstack/hono': ['@objectstack/types'],

Repo-wide: 303 to 302 (package, unaliased dep) pairs, across an unchanged 61 packages. Nothing was added and no entry was widened — the registry is shrink-only and this PR only removes from it. The entry's note was rewritten in the same edit rather than left describing a remediation that is now landed.

@objectstack/types stays on the entry, correctly: src/index.ts still imports readEnvWithDeprecation, looksLikeInternalErrorLeak, INTERNAL_ERROR_MESSAGE and resolveThrownHttpError from it as values, through dist/. That is a remediation of its own and is already recorded, mechanically, by the row that remains.

check-type-source-resolution.mjs reads tsconfigpaths, not vitest.config.*, so its own entry for this package is untouched by this diff and its gate is green unchanged.

Gates

Derived from the real change set with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (2 paths, merge base 3f7f8f590), not recalled. Run at 8ea75a241; each verdict below is the gate's own printed line, exit codes captured before any pipe.

Green: check:test-source-alias (OK — 72 packages with tests scanned; 61 registered as still resolving a workspace dep through dist/; 45 published subpath(s) resolved through every alias table), check:type-source-resolution (OK — 94 tsc program(s) across 77 packages scanned; 54 registered), check:agent-test-spelling, check:bash32-floor, check:cli-command-ids, check:cross-package-test-inputs, check:entry-guard, check:logger-receiver-detach, check:page-declaration-shape, check:parse-guard, check:pnpm-filter-targets, check:published-files, check:slot-lookup, check:watch-hint-literal, check-ci-filter-parity, check-comment-mask-adoption, check-cross-package-test-inputs, check-keyed-text-bounds, check-plugin-teardown-shape, check-shard-attestation, check-undeclared-dep-imports, check-affected-docs, check-drift-comment, plus both convention-triggered families this diff incurs by editing a gate script: scripts/pm/bare-root-worklist.mjs --self-test and check:pm-dispatch-gates.

NOT MEASURED (recorded with the gate's own refusal text, never folded into the green list):

  • check:dual-build-cjs-loads — exit 3, PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/. … Run pnpm build first. ⛔ This is NOT a pass: nothing was measured. It needs a repo-wide build, which CI owns.
  • scripts/check-test-completeness.mjs — exit 3, PREREQUISITE NOT MET — this gate grades a saved turbo run test log, and no log was named. … running the family locally, record this gate as NOT MEASURED.

Lint was narrowed to the changed files rather than run repo-wide, declared as a narrowing: eslint --no-inline-config --format json on both paths reports 2 files linted, 0 errors, 0 warnings, 0 suppressed (the count read from the JSON, not from a summary line); this repo runs one eslint.config.mjs which never enables type-aware linting for ANY file (no parserOptions.project, no typed @typescript-eslint rules, stated and positive-controlled in that config's own header), so this diff cannot move the verdict of any file it does not touch. The repo-wide pnpm lint run belongs to CI.

Changeset

None, deliberately. This diff publishes nothing from any package: vitest.config.ts is not in @objectstack/hono's files (dist, README.md, CHANGELOG.md), and scripts/check-test-source-alias.mjs belongs to the private root package. There is no user-visible change — the published behaviour of both packages is byte-identical. skip-changeset applied.

Generated by Claude Code


Generated by Claude Code

…unit suite
`packages/adapters/hono/src/index.ts` imports createOriginMatcher,
hasWildcardPattern, DEFAULT_CORS_ALLOW_HEADERS and DEFAULT_CORS_EXPOSE_HEADERS
from @objectstack/plugin-hono-server as VALUES at module scope, and the suite's
vitest config aliased only @objectstack/runtime — so that specifier resolved
through the dependency's `exports` to its `dist/`, making this package's unit
verdicts a function of another package's build state.
The alias is added in ARRAY form with an anchored regex `find`; the existing
@objectstack/runtime entry is carried over unchanged in reach (a bare string
`find` still matches by prefix).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k
…ill unaliased
`check-test-source-alias` audits `KNOWN_UNALIASED_TEST_IMPORTS` for set
EQUALITY, so the alias added in the previous commit makes the entry stale and
the gate names the exact narrowing. Repo-wide the ledger goes 303 -> 302
(package, unaliased dep) pairs across an unchanged 61 packages; the only
registry line that moves is hono's, and it only loses a member.
`@objectstack/types` stays on the entry: hono's `src/index.ts` still imports
four values from it through `dist/`. That is a separate remediation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️1 changed file(s) yielded no anchor (packages/adapters/hono/vitest.config.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/adapters/hono/vitest.config.ts) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 1 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 a286411dfeb99bcedfd496f058e86d0651cb5906packageMentionDocs.

@os-trumpos-trump added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 30, 2026 — with Claude
@os-trumpClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ PM review — ACCEPT once CI is green; ⛔ not while it is running (#12767)

Undrafting now; arm follows on a complete green read. ⛔ No rework owed. Clause ②: does not attach — 2 files, nothing under packages/spec/src/**, no REST door.

✅ The ledger fence held, verified from the diff

The binding fence was ⛔ never widen KNOWN_UNALIASED_TEST_IMPORTS — it is shrink-only, and #12770's ruling was explicitly narrow. Read from the patch itself:

- '@objectstack/hono': ['@objectstack/plugin-hono-server', '@objectstack/types'],
+ '@objectstack/hono': ['@objectstack/types'],

One member removed, nothing added, no other entry touched. Repo-wide 303 → 302 pairs across an unchanged 61 packages.

⭐ The positive control is the best part of this PR

I asked for proof the alias actually took effect, because a passing suite never says which file it loaded. What came back does better than that — it measures the hazard live and then closes it, on one instrument:

mutationdisthono suite
before the aliascreateOriginMatcher gutted in plugin-hono-server sourcenot rebuilt74 passed ← green over a destroyed matcher
after the aliasidentical mutationidentical stale dist4 failed — exactly the CORS wildcard cases

The four failures are precisely the cases whose subject that function is (expected null to be https://app.example.com). So the same experiment demonstrates the defect was real and that this change fixes it — a single control carrying both directions.

Both legs proven on disk (anchor 1→0, marker 0→1, git hash-objectHEAD:PATH), restored by absolute-path git checkout HEAD -- with git diff HEAD empty. ⭐ And the first mutation attempt was caught as a NO-OP by the count check (a perl s/// whose replacement contained the delimiter) and re-run — so no reading was taken on an unmutated tree. That is the discipline catching its own instrument.

⭐ The array-form conversion is not cosmetic, and the reasoning is worth keeping

The card's snippet would have been easy to paste. Instead the why was derived: the object form matches by PREFIX, so a bare key whose replacement is a file also swallows that package's subpaths into …/index.ts/<subpath> — ENOTDIR at run time, from a config that reads as correct.

⭐ And the trap on the other side was avoided: @objectstack/runtime was carried over as a bare string find, not converted to an anchored regex, so its reach is unchanged — with the note that @objectstack/runtime publishes only ., so there is no subpath for that prefix to swallow anyway. Narrowing that entry silently while "just converting to array form" is exactly the kind of collateral this could have shipped.

⭐ The control is recorded in the ledger comment itself, next to the row it justifies, so the next reader of that registry finds the evidence rather than a bare shrink.

Notes, none blocking

  • No failure was surfaced by the source switch — so the "⛔ do not paper over" fence was never tested. Nothing skipped, no it.fails, no revert to dist, no widened ledger. Suite is 74/74 at the final commit.
  • No changeset, justified: the diff publishes nothing — vitest.config.ts is not in hono's files list, and the gate script belongs to the private root package. skip-changeset applied and read back.
  • Two gates NOT MEASURED, recorded with their own refusal text and kept out of the green list: check:dual-build-cjs-loads (exit 3, needs a repo-wide build, CI-owned) and check-test-completeness (exit 3, needs a saved turbo log). ⛔ Correctly not counted as passes.
  • Lint is a declared narrowing rather than the repo-wide run other PRs in this lane did today: 2 files linted, 0 errors, count read from --format json, with the config-invariance argument that this repo's single eslint.config.mjs never enables type-aware linting for any file. That is the accepted narrowing form here, so it stands — but CI runs the full farm regardless, which is the real backstop.

✅ The unfiled finding — I agree, and it stays unfiled

@objectstack/types remains an unaliased artifact import of @objectstack/hono (src/index.ts imports readEnvWithDeprecation, looksLikeInternalErrorLeak, INTERNAL_ERROR_MESSAGE, resolveThrownHttpError from it as values). ⛔ Deliberately not filed, and that is the right call: the ledger row that remains IS the register of pending remediations, and opening a card for one of 302 registered pairs would duplicate the mechanism rather than add to it. It is also now documented inline in the ledger comment explaining why it stays. ⇒ no card; the register tracks it.


Generated by Claude Code

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

Labels

size/sskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@os-trump@claude