Skip to content

test(types): pin the host-only resolution defect on a name the workspace can never contain - #16723

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-16552-node-test-host-only-pin
Sep 8, 2026
Merged

test(types): pin the host-only resolution defect on a name the workspace can never contain#16723
os-project-manager merged 1 commit into
mainfrom
claude/issue-16552-node-test-host-only-pin

Conversation

@claude

@claude claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #16552

One file: packages/types/src/node.test.ts. The pin's example changed; what the pin asserts did not.

The defect, reproduced

The case host-app package resolution (cloud#1013, #4700) > the framework package's own resolution cannot see a host-only package — the defect used @objectstack/organizations as its example of a host-only package. #16215 (c677cda816) made that package a workspace member, so pnpm's hoisted store now carries it and a framework-anchored require.resolve finds it.

Resolution measured directly from packages/types, with the pnpm bin shim's NODE_PATH pointed at the hoisted store — the same two tree states, one command apart:

tree UNBUILT (organizations/dist absent — every CI shard that runs this file)
  THREW    @objectstack/organizations -> MODULE_NOT_FOUND
           Cannot find module '.../node_modules/.pnpm/node_modules/@objectstack/organizations/dist/index.js'

tree BUILT (after turbo run build --filter=@objectstack/organizations, 28 tasks)
  RESOLVED @objectstack/organizations -> .../packages/plugins/organizations/dist/index.js

Read the unbuilt throw closely: resolution found the package and failed only because its entry file was not on disk. That is what held the pin green in CI — build order, not the property. At test level, on the built tree, before this change:

FAIL src/node.test.ts > ... > the framework package's own resolution cannot see a host-only package — the defect
AssertionError: expected [Function] to throw an error
Tests  1 failed | 69 passed (70)

The repair

The example is now @fixture/host-only — the fixture-only scope this file already uses for @fixture/hoisted-only, @fixture/throws-on-load and @fixture/declared-but-missing. Measured absent from the workspace: no tracked package.json declares an @fixture/* name, and the scope is absent from both node_modules/@fixture and the hoisted store.

The case now proves its own premise rather than assuming it, which is the shape triage preferred, because a test that proves its own premise cannot silently decay into a vacuous assertion:

  1. the name resolves from the host app — without this leg, a name that exists nowhere at all satisfies leg 2 and the case pins nothing;
  2. and from the framework package it is absent — asserted on the bare specifier, not on /Cannot find module/ alone.

Leg 2's tightening is what removes the build-order dependence. A package the framework can see, whose entry file merely is not on disk, also throws MODULE_NOT_FOUND — but names the missing entry path, not the specifier. That throw can no longer satisfy this pin, in either build state.

The identifier was renamed ORGANIZATIONS to HOST_ONLY at all six sites: a constant named after a real package is exactly what let the example stop being host-only without anyone noticing. The fixture package still exports the real plugin's shape (OrganizationsPlugin / com.objectstack.organizations), because the callers' cases assert that shape; only the NAME is fixture-only.

Same verdict on BOTH tree states (acceptance criterion)

The fixed assertion, unmodified at HEAD (git diff HEAD empty for both runs), run twice — once with packages/plugins/organizations/dist present, once with it parked aside. The environment genuinely differs between the two runs, shown by a framework-anchored resolve of the old example taken immediately before each:

Tree state 1 — organizations WITH dist

framework-anchored resolve: RESOLVED @objectstack/organizations -> .../packages/plugins/organizations/dist/index.js
vitest-exit=0
Test Files  1 passed (1)
     Tests  70 passed (70)

Tree state 2 — organizations WITHOUT dist

framework-anchored resolve: THREW @objectstack/organizations -> MODULE_NOT_FOUND
                            Cannot find module '.../node_modules/.pnpm/node_modules/@objectstack/organizations/dist/index.js'
vitest-exit=0
Test Files  1 passed (1)
     Tests  70 passed (70)

Same verdict, both trees. organizations/dist was restored afterwards and the tree left unmodified. The old pin could not produce this pair: it was green in state 2 and red in state 1, which is the whole card.

The ablation below is the other half of the same square — it shows the reverted example is red in BOTH states too, so the new assertion is build-order-independent in both directions rather than merely green twice.

Ablation — the guard is proved to fail, in both tree states

Run from the committed state, each leg mutated on disk (proof printed), then restored via git checkout HEAD -- path with git diff HEAD empty and the worktree blob equal to the HEAD blob (394683344fbdf8dd985a905882e930a4938e66f9) after every leg.

leg mutation tree result
A example reverted to @objectstack/organizations BUILT RED — expected [Function] to throw an error, 1 failed / 69 passed
B same revert UNBUILT (organizations/dist parked aside) REDexpected [Function] to throw error matching /Cannot find module '@ob…/organizations' but got 'Cannot find module \'/home/user/objec…', 1 failed / 69 passed
C the host-only name installed nowhere BUILT RED — 3 failed / 67 passed

Leg B is the one that matters: the old pin was green in exactly that state. That green was the card. Leg C shows the positive leg is load-bearing — the assertion cannot be satisfied vacuously by a name that exists nowhere.

Verification

All readings taken on a tree with the @objectstack/organizations build closure present (28 turbo tasks), at 613a2b0ca.

  • pnpm --filter @objectstack/types testTest Files 19 passed (19), Tests 573 passed (573)
  • pnpm --filter @objectstack/types test:repoTest Files 1 passed (1), Tests 7 passed (7)
  • pnpm --filter @objectstack/types typecheck — exit 0; tsc --noEmit --listFiles names src/node.test.ts, so this reading really does cover the edited file
  • Gates derived from the actual diff with scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 45 families, all 45 run, reconciled with --ran"45 derived famil(ies) accounted for — 45 run, 0 NOT-MEASURED". 43 exit 0. Two exit 3 = PREREQUISITE NOT MET, both because this tree is not fully built, quoting each gate: check:dual-build-cjs-loads"this gate reads built output, and some package has no dist/"; check:type-check-debt"--re-measure cannot run: 5 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk". Neither is a failure and neither is a pass: NOT MEASURED, left to CI.
  • pnpm lint run repo-wide, not narrowed: exit 0 over 6337 files, 0 errors, 0 warnings, the edited file present in eslint's own reported population.

skip-changeset

Measured, not assumed. @objectstack/types publishes files: ["dist", "README.md", "CHANGELOG.md"]. The subject symbols (HOST_ONLY, @fixture/host-only) have zero hits across dist/ and README.md; positive control createHostRequire hits 4 files under dist/. A test file moves no published surface, so the skip-changeset label is applied rather than a changeset written.

Scope

#16539 is not addressed here, and this PR was deliberately not widened to it. Read as instructed, and the two cards cannot share one fixture-only scope: in this file the name is a pure fixture stand-in that the test itself writes to disk, whereas in packages/qa/dogfood/test/enterprise-organizations.ts the name is the probe's real subject — ORGANIZATIONS_PKG is exported production code, probeOrganizations() runs at module scope for the live dogfood gates, and the control asserts the operator-facing remedy text that names that real package. Renaming it there would delete the thing being probed. That card needs a hermetic fixture root or a parameterised probe subject — a design change in the probe, not a rename.

Clause-②: no — re-declared from the delivered diff: one test file, no published surface moves.

🤖 Generated with Claude Code

https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8


Generated by Claude Code

…ace can never contain

The host-only example was `@objectstack/organizations`, a real cloud-private
package when the pin was written. Once that package became a workspace member,
pnpm's hoisted store carried it and the framework-anchored resolve succeeded on
any built tree — so the assertion held in CI only because the shard running it
had not built that package. Build order, not the property it asserts.

The example is now `@fixture/host-only`, a scope this workspace cannot contain,
and the case proves its own premise instead of assuming it: it first resolves
the name FROM THE HOST APP (without which a name existing nowhere would satisfy
the assertion vacuously), then requires the framework-side failure to name the
BARE SPECIFIER. A reachable-but-unbuilt package throws MODULE_NOT_FOUND naming
its missing entry file instead, so that throw can no longer satisfy this pin in
either build state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 8, 2026
@github-actions

github-actions Bot commented Sep 8, 2026

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 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 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 — 0 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 be92d468043c1b9e100e6e0f2c2fc9216a5aa850packageMentionDocs.

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

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants