diff --git a/packages/cli/test/json-stdout-purity.e2e.test.ts b/packages/cli/test/json-stdout-purity.e2e.test.ts index cc932b0671f..698034ba9bc 100644 --- a/packages/cli/test/json-stdout-purity.e2e.test.ts +++ b/packages/cli/test/json-stdout-purity.e2e.test.ts @@ -100,9 +100,22 @@ const FAMILY: Record = { 'migrate files-to-references': [], 'migrate meta': ['--stored'], 'migrate multi-value-columns': [], + // Its bare form is its only mode — a read-only plan (`deferSchemaDdl` + + // `readOnlyProbe`). The fixture's database holds none of the platform tables + // the ceremony inventories, so the face driven here is its REFUSAL face + // (`plan_refused`, `emitJson(…, 1, { compact: true })`), emitted after the + // stack is shut down. A refused plan writes no plan file, so the fixture + // directory gains nothing. + 'migrate organization-ownership': [], 'migrate plan': [], 'migrate recorded-by': [], 'migrate resume': [], + // A preview is its DEFAULT and the only form driven here: without `--apply` + // it composes the deployment the way `os serve` does (the host config's + // plugins and the auth-gated security plugin) and boots read-only with no + // `sys_metadata` hydration, so it deletes nothing. No table exists in the + // fixture, so the face driven here is its empty listing. + 'migrate security-catalog-overlays': [], 'migrate summary-nulls': [], // One object per run, so it needs a name: the fixture's own `jp_ticket`, // which the composed host config registers. No database file exists, so the diff --git a/packages/cli/test/serve-boot-diagnostics-noise-budget.e2e.test.ts b/packages/cli/test/serve-boot-diagnostics-noise-budget.e2e.test.ts index 623cd29386f..058fbecec7d 100644 --- a/packages/cli/test/serve-boot-diagnostics-noise-budget.e2e.test.ts +++ b/packages/cli/test/serve-boot-diagnostics-noise-budget.e2e.test.ts @@ -30,9 +30,10 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { execFile } from 'node:child_process'; -import { appendFileSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { appendFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { PROTOCOL_MAJOR } from '@objectstack/spec/kernel'; import { childEnv, randomPort, runServe, type ServeRun } from './helpers/serve-process.js'; const HERE = resolve(fileURLToPath(import.meta.url), '..'); @@ -122,6 +123,43 @@ let root: string; let scaffold: { code: number; output: string }; let boot: ServeRun | undefined; +/** + * Align the scaffold's `engines.protocol` with the protocol of the runtime this + * file boots: the WORKSPACE `@objectstack/spec`, whose `PROTOCOL_MAJOR` this + * reads. + * + * The template stamps the protocol of the packages a real project INSTALLS, + * which is the published line (`create-objectstack`'s own major). That is + * right for users: the Scaffold E2E gate installs from the registry and refuses + * any other range. This file installs nothing — the project resolves the + * workspace copies — so in a Changesets pre-mode window, where + * `PROTOCOL_VERSION` moves ahead of the version pass (packages at 17.x beside + * protocol 18), the pair it boots is one no user has: a published-line scaffold + * on a next-major runtime. The handshake refuses that pair before a single boot + * diagnostic is printed, and this file goes red on the setup rather than the + * budget (review 6099406666 on #22622). So the setup aligns the pair it boots. + * + * Only the major's digits are rewritten. Outside such a window the template + * already stamps `'^PROTOCOL_MAJOR'`, and the file is left byte-for-byte as + * scaffolded. + * + * A direct rewrite of this one key, not `objectstack migrate meta --from N + * --write`, the step the refusal prescribes: that codemod refuses to edit any + * file under a `node_modules` path segment, and this project lives under this + * package's `node_modules` so that its imports resolve. Measured on a scaffold + * there: exit 0, "not written [outside-project]", config unchanged. + */ +function alignProtocolRange(configPath: string): void { + const source = readFileSync(configPath, 'utf8'); + const stamp = /engines:\s*\{\s*protocol:\s*'\^(\d+)'\s*\}/; + if (!stamp.test(source)) { + throw new Error(`${configPath} declares no engines.protocol stamp, so the pair this file boots cannot be aligned`); + } + const aligned = source.replace(stamp, (literal, major: string) => + literal.replace(`'^${major}'`, `'^${PROTOCOL_MAJOR}'`)); + if (aligned !== source) writeFileSync(configPath, aligned); +} + function scaffoldProject(cwd: string): Promise<{ code: number; output: string }> { return new Promise((resolveRun) => { execFile( @@ -141,6 +179,7 @@ beforeAll(async () => { scaffold = await scaffoldProject(root); if (scaffold.code !== 0) return; const dir = join(root, PROJECT); + alignProtocolRange(join(dir, 'objectstack.config.ts')); writeFileSync(join(dir, 'src', 'objects', 'ticket.object.ts'), TICKET_OBJECT); appendFileSync(join(dir, 'src', 'objects', 'index.ts'), "export { Ticket } from './ticket.object.js';\n"); writeFileSync(join(dir, 'src', 'actions', 'ticket.actions.ts'), TICKET_ACTIONS);