From 20e44a112176747dc1154c2390ff6df3a952233d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 13 Sep 2026 08:39:01 +0000 Subject: [PATCH 1/2] fix(verify): give the declared-unresolvable control a fixture subject the workspace can never supply MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The host-resolution CONTROL builds a temp host app that DECLARES the enterprise multi-org package and deliberately does not install it, then pins the `declared-but-not-installed` remedy wording. Since ADR-0132 made that package a tracked workspace member, pnpm's hoisted store carries it and vitest's own `pnpm exec` bin shim exports a NODE_PATH that reaches the store — so on any tree with a full local build the CJS-shaped host resolve SUCCEEDED, the organization wall came up, and boot was refused several steps later by the membership-policy gate whose message even says "This is NOT ... a missing package". The control never reached the wording it exists to pin: red on a built tree, green on CI, and proving nothing in either state. `bootStack` now takes the subject as an option defaulting to the real package, so the one case whose content is an ABSENCE hands in a `@fixture/*` name the workspace can never contain, and a PREMISE block proves that absence instead of assuming it. The other four cases stay on the real subject and keep the default pinned behaviourally. Same repair #16539 and #16552 landed for packages/qa/dogfood and packages/types. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c --- .../host-resolution-control-fixture-name.md | 22 +++ .../src/harness.host-resolution.test.ts | 153 +++++++++++++++++- packages/verify/src/harness.ts | 48 +++++- 3 files changed, 216 insertions(+), 7 deletions(-) create mode 100644 .changeset/host-resolution-control-fixture-name.md diff --git a/.changeset/host-resolution-control-fixture-name.md b/.changeset/host-resolution-control-fixture-name.md new file mode 100644 index 00000000000..29e8e6ef948 --- /dev/null +++ b/.changeset/host-resolution-control-fixture-name.md @@ -0,0 +1,22 @@ +--- +'@objectstack/verify': minor +--- + +verify: let `bootStack` be told which package `multiTenant: true` resolves, so the +`declared-unresolvable` control can name a subject the workspace can never supply + +`BootOptions` gains an optional `organizationsPackage`. It defaults to +`@objectstack/organizations` and production callers never pass it — the +operator-facing error still names that package literally, because in every +production boot it is the subject. Only the specifier moves. + +Why it exists: a fixture whose whole content is "this host root DECLARED the +package and does not have it" cannot state the second half with a name the +workspace owns. Since ADR-0132 the multi-org runtime is a tracked workspace +package, pnpm's hoisted store carries it, and a `pnpm exec`-launched runner +exports a `NODE_PATH` that reaches that store — so such a fixture resolved the +package out of the ambient workspace the moment it had been built, and its +verdict became a function of an unrelated package's build state rather than of +its own directory. The harness's own host-resolution control now hands in a +`@fixture/*` name and proves the absence instead of assuming it, the repair +already landed for `packages/qa/dogfood` and `packages/types`. diff --git a/packages/verify/src/harness.host-resolution.test.ts b/packages/verify/src/harness.host-resolution.test.ts index e4ea9ed7eb9..1aba512cb31 100644 --- a/packages/verify/src/harness.host-resolution.test.ts +++ b/packages/verify/src/harness.host-resolution.test.ts @@ -27,13 +27,53 @@ * that is the whole point), registering the same `org-scoping` service and * posture entitlement the real one does. What is under test here is * RESOLUTION, not the enterprise semantics. + * + * ── #17911 — why ONE case below drives a `@fixture/*` subject ──────────────── + * + * The CONTROL's whole content is "this host root DECLARED it and does not have + * it". Until ADR-0132 / #16215 that came free: `@objectstack/organizations` was + * cloud-private, so a temp host that declared it and did not install it was + * unresolvable by construction. It is a tracked workspace package now, pnpm's + * hoisted store carries it, and vitest's own `pnpm exec` bin shim exports a + * `NODE_PATH` that reaches that store — so the package RESOLVED, the wall came + * up, and boot was refused several steps later by the membership-policy gate + * whose message even says "This is NOT … a missing package". The CONTROL never + * reached the wording it exists to pin: red on any tree with a full local + * build, green on CI, and proving nothing in either state. + * + * So that one case hands in a name this workspace can never contain and the + * PREMISE block below PROVES the absence instead of assuming it — the repair + * #16539 (dogfood) and #16552 (`packages/types/src/node.test.ts`) landed, for + * the reason those cards state verbatim: no workspace name is safe from + * becoming one. + * + * ⛔ The other four cases deliberately stay on the REAL subject, and that is + * what keeps `ORGANIZATIONS_PKG` pinned as `bootStack`'s default: each of them + * is decided by something the ambient workspace cannot supply — an app-local + * `node_modules` copy, which wins over `NODE_PATH`, or the UNDECLARED arm, + * whose fallback is this module's own ESM `import()` and Node's ESM resolver + * does not consult `NODE_PATH` at all. Measured in both build states (#17911). */ import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { bootStack } from './harness.js'; +import { createHostRequire } from '@objectstack/types/node'; +import { bootStack, ORGANIZATIONS_PKG } from './harness.js'; + +/** + * The subject the `declared-unresolvable` CONTROL is built around: modelled on + * the real enterprise package, fixture-only in NAME. + * + * ⚠️ #17911 — it must stay a `@fixture/*` name. A name this workspace owns + * cannot state "this host root does not have it", because the hoisted store and + * the launcher's `NODE_PATH` answer that question instead of the fixture; and no + * workspace name is safe from becoming one (`@objectstack/organizations` was + * cloud-private when this case was written). Only a name the workspace can never + * contain is, and the PREMISE cases at the bottom prove this one still is not. + */ +const FIXTURE_ORGANIZATIONS = '@fixture/host-organizations'; /** * Stand-in for `@objectstack/organizations`. Mirrors the real plugin's @@ -92,14 +132,30 @@ let appDeclaredNoLoadableEntry: string; * untouched by #14270, so this case must render byte-identically before and * after — that is what makes the third arm's flip a measurement rather than a * rewrite that moved everything. + * + * ⚠️ #17911 — "NOT installed" is a property of THIS directory, and only a + * subject the workspace can never supply keeps it one. See + * {@link FIXTURE_ORGANIZATIONS}. */ let appDeclaredNotInstalled: string; +/** + * #17911 PREMISE leg 1 only — a host that really does install the fixture + * subject. Without it, a subject that exists NOWHERE (a typo, a deleted scope) + * would satisfy the absence leg and the CONTROL would be vacuous. + */ +let appFixtureInstalled: string; function writeApp( prefix: string, - opts: { withOrganizations: boolean; declare?: boolean; typesOnly?: boolean }, + opts: { withOrganizations: boolean; declare?: boolean; typesOnly?: boolean; pkg?: string }, ): string { const declare = opts.declare ?? opts.withOrganizations; + // #17911 — the subject this host declares and/or installs. Defaults to the + // real package, which is what four of the five cases need; the CONTROL hands + // in `FIXTURE_ORGANIZATIONS` because its verdict is an ABSENCE and only a name + // the workspace can never supply keeps that absence a property of this + // directory. + const pkg = opts.pkg ?? ORGANIZATIONS_PKG; const dir = mkdtempSync(join(tmpdir(), prefix)); writeFileSync( join(dir, 'package.json'), @@ -108,7 +164,7 @@ function writeApp( name: 'hostres-fixture', private: true, type: 'module', - ...(declare ? { dependencies: { '@objectstack/organizations': '*' } } : {}), + ...(declare ? { dependencies: { [pkg]: '*' } } : {}), }, null, 2, @@ -116,7 +172,8 @@ function writeApp( 'utf8', ); if (opts.withOrganizations) { - const pkgDir = join(dir, 'node_modules', '@objectstack', 'organizations'); + // The specifier is a scoped name, so it lands on disk as `/`. + const pkgDir = join(dir, 'node_modules', ...pkg.split('/')); mkdirSync(pkgDir, { recursive: true }); if (opts.typesOnly) { // A publish whose `exports` names a `types` target and nothing else: no @@ -126,7 +183,7 @@ function writeApp( writeFileSync( join(pkgDir, 'package.json'), JSON.stringify({ - name: '@objectstack/organizations', + name: pkg, version: '0.0.0-fixture', type: 'module', exports: { '.': { types: './index.d.ts' } }, @@ -138,7 +195,7 @@ function writeApp( writeFileSync( join(pkgDir, 'package.json'), JSON.stringify({ - name: '@objectstack/organizations', + name: pkg, version: '0.0.0-fixture', type: 'module', main: 'index.js', @@ -165,6 +222,11 @@ beforeAll(() => { appDeclaredNotInstalled = writeApp('os-verify-org-host-not-installed-', { withOrganizations: false, declare: true, + pkg: FIXTURE_ORGANIZATIONS, + }); + appFixtureInstalled = writeApp('os-verify-org-host-fixture-ok-', { + withOrganizations: true, + pkg: FIXTURE_ORGANIZATIONS, }); }); @@ -175,6 +237,7 @@ afterAll(() => { appInstalledButUndeclared, appDeclaredNoLoadableEntry, appDeclaredNotInstalled, + appFixtureInstalled, ]) { if (dir) rmSync(dir, { recursive: true, force: true }); } @@ -278,6 +341,14 @@ describe('bootStack multiTenant — host-app package resolution (#4700)', () => // Not a new behaviour, a CONTROL. #14270 rewrote this branch's SHAPE // (two-way → three-way); this arm's text has to come out byte-identical, // or the third kind's fix moved something that was already right. + // ⚠️ #17911 — `organizationsPackage` is why this case can still SAY + // "not installed". On the real subject the host's declaration was + // honoured, the hoisted store answered the resolve, the wall came up and + // boot died several steps later at the membership-policy gate — the + // assertion below was never reached, in either direction. The subject is + // a `@fixture/*` name the workspace can never contain; the PREMISE block + // proves it is absent, and `bootStack`'s DEFAULT subject stays pinned by + // the four cases above, which pass no option at all. // ⚠️ try/catch rather than `.then(onFulfilled, onRejected)`: `./harness` // is imported without its `.js` extension, so under NodeNext the // specifier does not resolve and every symbol it names is `any` — which @@ -290,6 +361,7 @@ describe('bootStack multiTenant — host-app package resolution (#4700)', () => const stack = await bootStack(app as never, { multiTenant: true, hostRoot: appDeclaredNotInstalled, + organizationsPackage: FIXTURE_ORGANIZATIONS, }); await stack.stop(); } catch (e) { @@ -342,3 +414,72 @@ describe('bootStack multiTenant — host-app package resolution (#4700)', () => BOOT_TIMEOUT, ); }); + +/** + * #17911 — the premises the CONTROL rests on, asserted instead of assumed. + * + * Both resolution legs are load-bearing and they fail in opposite directions. + * Without leg 1 a subject that exists nowhere at all satisfies leg 2 and the + * CONTROL is vacuous; without leg 2 the fixture stops deciding what the host + * root has, and the CONTROL silently starts measuring the ambient workspace — + * which is exactly how this case broke. + */ +describe('PREMISE — the CONTROL subject is host-only (#17911)', () => { + it('resolves from a host app that installs it', () => { + // Leg 1. A host that really installs the fixture can see it, so + // "unresolvable" elsewhere is a statement about the resolver's anchor and + // not about a typo or a scope nobody publishes. + const fromHost = createHostRequire(appFixtureInstalled).resolve(FIXTURE_ORGANIZATIONS); + expect(fromHost).toContain(appFixtureInstalled); + }); + + it('is absent from every ambient store the runner exposes', () => { + // Leg 2, and the guard that would have caught this card. Asserted on the + // BARE SPECIFIER, ⛔ never on `/Cannot find module/` alone: a package the + // runner CAN see whose entry file merely is not on disk throws + // MODULE_NOT_FOUND too, naming `//dist/index.js` instead of the + // specifier. That second throw is precisely what kept this CONTROL green + // while the property went unguarded — `@objectstack/organizations` was + // reachable through the pnpm bin shim's NODE_PATH and simply unbuilt on + // CI's task graph. Read as a bare-specifier failure, the premise can no + // longer be satisfied by an unbuilt workspace package, in either build + // state. + expect(() => createHostRequire(appDeclaredNotInstalled).resolve(FIXTURE_ORGANIZATIONS)).toThrow( + new RegExp(`Cannot find module '${FIXTURE_ORGANIZATIONS}'`), + ); + }); + + it('and the REAL package is reachable from that same anchor — the asymmetry is the point', () => { + // The other half of leg 2, and the one that makes this file's history + // legible: from the very same host root, the real subject DOES resolve, + // through the hoisted store the launcher's NODE_PATH exposes. That is not a + // defect to fix here (ADR-0132 put the package in this workspace on + // purpose); it is the reason the CONTROL may not be built on that name. + // + // ⚠️ Build-state dependent BY CONSTRUCTION, so it asserts reachability of + // the package DIRECTORY rather than a loadable entry: `require.resolve` + // throws MODULE_NOT_FOUND for an unbuilt package too, and pinning "it + // resolves" would make this case itself a verdict about whether a sibling + // package had been built — the defect it documents. + let resolveError: string | undefined; + try { + createHostRequire(appDeclaredNotInstalled).resolve(ORGANIZATIONS_PKG); + } catch (e) { + resolveError = (e as Error).message; + } + // Either it resolved (built tree), or it failed naming a path inside the + // store rather than the bare specifier (unbuilt tree). What must NEVER + // happen is the bare-specifier failure leg 2 asserts for the fixture. + expect(resolveError).not.toMatch(new RegExp(`Cannot find module '${ORGANIZATIONS_PKG}'`)); + }); + + it('and bootStack still binds the ENTERPRISE package as its default subject', () => { + // What the fixture subject must NOT quietly become: the default. Production + // callers pass no `organizationsPackage`, and the one they get is the real + // package — behaviourally pinned by the four cases above, which pass no + // option and are decided by the real name. + expect(ORGANIZATIONS_PKG).toBe('@objectstack/organizations'); + expect(FIXTURE_ORGANIZATIONS).not.toBe(ORGANIZATIONS_PKG); + expect(FIXTURE_ORGANIZATIONS.startsWith('@fixture/')).toBe(true); + }); +}); diff --git a/packages/verify/src/harness.ts b/packages/verify/src/harness.ts index adc271bd853..7610839f97f 100644 --- a/packages/verify/src/harness.ts +++ b/packages/verify/src/harness.ts @@ -85,6 +85,18 @@ const DEFAULT_ADMIN_EMAIL = 'admin@objectos.ai'; const DEFAULT_ADMIN_PASSWORD = 'admin123'; const DEFAULT_AUTH_SECRET = 'objectstack-verify-secret'; +/** + * The enterprise multi-org runtime this harness mounts under `multiTenant: true` + * — the one and only subject `bootStack` resolves from the host app, and the + * default of {@link BootOptions.organizationsPackage}. + * + * Exported for the host-resolution suite's premise case, which pins this value + * so a test seam can never quietly become the production subject. ⛔ Deliberately + * NOT re-exported from `./index.ts`: it is not part of this package's published + * API. + */ +export const ORGANIZATIONS_PKG = '@objectstack/organizations'; + /** * A booted stack: the HTTP surface (`api` / `raw` / `signIn` / `signUp` / * `apiAs`) plus the in-process handle (`hooks` / `validate` / `flows` / @@ -347,6 +359,36 @@ export interface BootOptions { * array order. Default `[]`. */ extraPlugins?: unknown[]; + /** + * The specifier `multiTenant: true` resolves from the host app. Defaults to + * the real subject, {@link ORGANIZATIONS_PKG}; ⛔ production callers never + * pass it. + * + * ## Why it exists (#17911, the same repair #16539 / #16552 landed) + * + * Every verdict this boot path reaches is a statement about what a host root + * HAS and, just as load-bearing, what it has NOT got. Until ADR-0132 / #16215 + * the second half came free: `@objectstack/organizations` was cloud-private, + * so a temp host that declared it and did not install it was unresolvable by + * construction. It is a tracked workspace package now; pnpm's hoisted store + * carries it and every `pnpm exec`-launched runner (vitest's bin shim + * included) exports a `NODE_PATH` that reaches that store. From then on a + * "declared, not installed" fixture's verdict was a function of whether an + * unrelated package had been BUILT — green on CI, whose test graph never + * builds it, red on any tree that had run a full local build. + * + * The visible half of that is a false red. The half that matters is the quiet + * one: a fixture whose subject is reachable is no longer deciding what the + * host root has, and nothing says so. So a case that needs the absence to be + * a property of ITS OWN directory hands in a name this workspace can never + * contain (`@fixture/*`) and proves the absence rather than assuming it — and + * no workspace name is safe from becoming one. + * + * ⛔ It does NOT rename the package in the operator-facing sentence: the error + * this boot throws names {@link ORGANIZATIONS_PKG} literally, because in every + * production boot that is the subject. Only the specifier moves. + */ + organizationsPackage?: string; } /** @@ -548,7 +590,11 @@ export async function bootStack( // package declares it — and it is what stops the next app-supplied // package added to this path from silently missing `packages/verify`'s own // dependencies. - const organizationsPkg = '@objectstack/organizations'; + // #17911: the subject is a parameter with the real package as its default, + // so a fixture case whose whole content is "this host root does NOT have + // it" can hand in a name the workspace can never supply. Production callers + // pass nothing and get `ORGANIZATIONS_PKG` — see BootOptions.organizationsPackage. + const organizationsPkg = opts.organizationsPackage ?? ORGANIZATIONS_PKG; const hostRoot = opts.hostRoot ?? process.cwd(); let mod: any; try { From 250e8741336b1ef3f5d593350d5809ab242a12bf Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 13 Sep 2026 08:43:18 +0000 Subject: [PATCH 2/2] test(verify): make the premise leg assert in both legal build states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `expect(undefined).not.toMatch(...)` is a TypeError, not a pass, so the leg asserting that the REAL subject is reachable from the fixture's anchor went red on exactly the tree it describes — a built one. Each of the two legal shapes now asserts its own discriminating fact: a resolve that succeeded did not come out of the host root, and a resolve that failed did not fail on the bare specifier. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c --- .../src/harness.host-resolution.test.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/verify/src/harness.host-resolution.test.ts b/packages/verify/src/harness.host-resolution.test.ts index 1aba512cb31..35c18efc95f 100644 --- a/packages/verify/src/harness.host-resolution.test.ts +++ b/packages/verify/src/harness.host-resolution.test.ts @@ -461,16 +461,26 @@ describe('PREMISE — the CONTROL subject is host-only (#17911)', () => { // throws MODULE_NOT_FOUND for an unbuilt package too, and pinning "it // resolves" would make this case itself a verdict about whether a sibling // package had been built — the defect it documents. + let resolved: string | undefined; let resolveError: string | undefined; try { - createHostRequire(appDeclaredNotInstalled).resolve(ORGANIZATIONS_PKG); + resolved = createHostRequire(appDeclaredNotInstalled).resolve(ORGANIZATIONS_PKG); } catch (e) { resolveError = (e as Error).message; } - // Either it resolved (built tree), or it failed naming a path inside the - // store rather than the bare specifier (unbuilt tree). What must NEVER - // happen is the bare-specifier failure leg 2 asserts for the fixture. - expect(resolveError).not.toMatch(new RegExp(`Cannot find module '${ORGANIZATIONS_PKG}'`)); + // Two legal shapes, and each asserts its own discriminating fact — ⛔ not + // one assertion that a `undefined` would satisfy vacuously. + if (resolved !== undefined) { + // Built tree: it resolved, and NOT out of this host root — which is the + // whole mechanism, stated as an assertion. + expect(resolved).not.toContain(appDeclaredNotInstalled); + } else { + // Unbuilt tree: the package DIRECTORY is still reachable, so the throw + // names a path inside the store. What must never happen is the + // bare-specifier failure leg 2 asserts for the fixture — that is the one + // reading that would make the two subjects interchangeable. + expect(resolveError).not.toMatch(new RegExp(`Cannot find module '${ORGANIZATIONS_PKG}'`)); + } }); it('and bootStack still binds the ENTERPRISE package as its default subject', () => {