Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .changeset/host-resolution-control-fixture-name.md
Original file line number Diff line number Diff line change
@@ -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`.
163 changes: 157 additions & 6 deletions packages/verify/src/harness.host-resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'),
Expand All @@ -108,15 +164,16 @@ function writeApp(
name: 'hostres-fixture',
private: true,
type: 'module',
...(declare ? { dependencies: { '@objectstack/organizations': '*' } } : {}),
...(declare ? { dependencies: { [pkg]: '*' } } : {}),
},
null,
2,
),
'utf8',
);
if (opts.withOrganizations) {
const pkgDir = join(dir, 'node_modules', '@objectstack', 'organizations');
// The specifier is a scoped name, so it lands on disk as `<scope>/<name>`.
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
Expand All @@ -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' } },
Expand All @@ -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',
Expand All @@ -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,
});
});

Expand All @@ -175,6 +237,7 @@ afterAll(() => {
appInstalledButUndeclared,
appDeclaredNoLoadableEntry,
appDeclaredNotInstalled,
appFixtureInstalled,
]) {
if (dir) rmSync(dir, { recursive: true, force: true });
}
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -342,3 +414,82 @@ 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 `<store>/<pkg>/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 resolved: string | undefined;
let resolveError: string | undefined;
try {
resolved = createHostRequire(appDeclaredNotInstalled).resolve(ORGANIZATIONS_PKG);
} catch (e) {
resolveError = (e as Error).message;
}
// 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', () => {
// 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);
});
});
48 changes: 47 additions & 1 deletion packages/verify/src/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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` /
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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 {
Expand Down
Loading