Skip to content

Commit d8830c2

Browse files
test(cli): drive the two new --json members and align the noise-budget scaffold with the workspace protocol (nightly tiers) (#22699)
Fixes #22622 Clause-②: no Two nightly-tier CLI test files change and nothing else: no accepted input, key, export or error code moves, and nothing publishes. Two independent reds on the nightly tiers, both in `domain:cli`. **Both are fixed inside the nightly tests**, following the seat's review 6099406666 on #22622. Nothing else changes: - the family-roster check; - the noise budget and every assertion in that file; - the `create-objectstack` template, the version-time stamp (`scripts/sync-template-versions.mjs`) and their tests, which are byte-for-byte as `main` has them. ## (a) Both reds, reproduced before any fix Base `9646991283`, `OS_TEST_TIERS=nightly`, `--project integration`, through the verify lock: `Tests 2 failed | 51 passed (53)`. - `json-stdout-purity.e2e.test.ts`, `is exactly the set listed here`: `expected [ 'meta resync', …(17) ] to deeply equal [ 'meta resync', …(15) ]`, with `+ "migrate organization-ownership"` and `+ "migrate security-catalog-overlays"`. `migrate organization-ownership` came from PR #22643, after the nightly that filed the card. The members were taken from `discoverFamily()`'s output. - `serve-boot-diagnostics-noise-budget.e2e.test.ts`, `highlights the dead button once…`: `expected 0 to be greater than 0` at `:176`. The boot printed `✗ package 'com.example.support-desk' targets protocol ^17 (engines.protocol) but this runtime is protocol 18.0.0. This is a major-version break. Run: objectstack migrate meta --from 17`. That is a refusal, so no *Boot diagnostics* block is printed at all. After, on `3d63ed7db5`: `Test Files 2 passed (2)`, `Tests 59 passed (59)`. That is 53, plus 3 cases for each of the two new members. ## Failure 1: the two new `--json` members are driven `FAMILY` gains both members, each in its bare form. The CLI was first run against the purity fixture to see each one's face: - `migrate organization-ownership` shows its refusal face. Over a database with none of the platform tables it prints `{"error":"plan_refused","reason":"not-an-objectstack-database",…}` and exits 1. The JSON is emitted after `stack.shutdown()`, and no plan file is written. - `migrate security-catalog-overlays` shows its read-only preview: `listed: 0`, exit 0. There is no `sys_metadata` hydration and nothing is deleted. Both print all three boot diagnostics on stderr. The discovery and the `toEqual` roster check are untouched. ## Failure 2: the noise-budget e2e aligns the pair it boots ### (b) What the version-time stamp writes Measured read-only at `9646991283`: - `changeset status --output` (`pre.json` is `{"mode":"pre","tag":"next"}`) gives `@objectstack/spec major 17.7.0 -> 18.0.0-next.0` and `create-objectstack major 17.7.0 -> 18.0.0-next.0` (the `fixed` group). - `loadScaffolderVersion()` on `18.0.0-next.0` gives `{"version":"18.0.0-next.0","major":"18","range":"^18.0.0"}`. So the version pass stamps `engines: { protocol: '^18' }` into the released template. **Triage's "every project that v18's `create-objectstack` scaffolds would boot with the protocol-gap warning" is false.** No released v18 scaffold carries the gap. PR #22215's changeset says the same ("the template keeps `'^17'` until the version pass stamps it"). The red is `main`'s pre-mode window: packages at `17.7.0`, `PROTOCOL_VERSION` at `18.0.0`. Only an in-repo pairing sees it: a published-line scaffold booted on the workspace runtime with nothing installed. ### Round 1 changed the product side, and CI falsified that Round 1 (`bff640d98`) stamped the template's `engines.protocol` from `PROTOCOL_VERSION`'s major. On that head, `Scaffold with repo dist` (`scaffold-e2e.yml`, job 1) went red: "package 'com.example.e2e-app' targets protocol ^18 (engines.protocol) but this runtime is protocol 17.0.0". That gate guards the path a user takes. It scaffolds with the repo-built scaffolder and installs `@objectstack/*` from the registry (`^17.0.0`, so protocol 17). On that path a template's range must match the protocol of the packages it installs, which is the package major. The sync script and the `template-consistency` ratchet already enforce exactly that on `main`. Commit `60fa01c7a7` removes round 1's product-side change: - `templates/blank/objectstack.config.ts`, `template-consistency.test.ts`, `template-version-stamps.test.ts` and `scripts/sync-template-versions.mjs` are back at their `main` blobs (`6308f29e`, `d32c819c`, `48ba146e`, `f7704db8`, all equal at base and `origin/main`); - the `create-objectstack` changeset is deleted. ### This round: the setup aligns the pair it boots This follows the seat's verification-strategy ruling in 6099406666. After scaffolding, and before the fixture files are written, `alignProtocolRange()` rewrites the scaffold's `engines.protocol` to `'^' + PROTOCOL_MAJOR`. `PROTOCOL_MAJOR` is read from `@objectstack/spec/kernel`, the workspace spec this file boots, the same import other CLI e2e files use. - **Only the major's digits are rewritten.** When the majors agree, the scaffold is left byte-for-byte as written, and the helper does not write the file at all. - Measured on the template: at `'^17'` (the window) the hash moves `684e38ed0d1a` → `5d6e6468255e`, and only the `engines` line differs. - At `'^18'` (majors agree) the hash stays `5d6e6468255e` → `5d6e6468255e`, byte-identical. - **A missing stamp throws in `beforeAll`.** A template that stops declaring the key fails this file loudly instead of skipping the alignment. - **Why a direct rewrite of the one key, and not `objectstack migrate meta --from 17 --write`.** The `--write` command is the step the refusal prescribes, and it was measured first. On a scaffold under `packages/cli/node_modules/` it exited 0 with `not written [outside-project]` and left the config unchanged. The codemod refuses any file whose real path has a `node_modules` segment (`authored-source-codemod.ts`, the `outside-project` refusal). This project has to live under this package's `node_modules` so that its imports resolve to workspace copies. - **What stays the same:** the noise budget, all three assertions, and the ticket object and actions. Once the version pass lands, the window closes and the alignment becomes a no-op. ## Ablations Both ran through `scripts/ablation-replace.mjs` in wrap mode on committed trees, under the verify lock. Each restore was proven by blob equal to HEAD and an empty `git diff HEAD`. Neither subject resolves through `dist/`: the FAMILY and the setup live in the test files. | Mutation | Anchor | Result | |---|---|---| | `alignProtocolRange(join(dir, 'objectstack.config.ts'));` deleted from the setup | x1 → x0, blob `058fbecec7dc` → `bfedb0522aa5` | noise-budget red with the original refusal: `✗ package 'com.example.support-desk' targets protocol ^17 … this runtime is protocol 18.0.0 …`, `expected 0 to be greater than 0`, 1 failed and 2 passed. After the restore to blob `058fbecec7dc`: 3 passed. | | `'migrate security-catalog-overlays': [],` deleted from `FAMILY` (round 1, same blob as now) | x1 → x0, blob `698034ba9bcb` → `5dfe8780897a` | roster pin red: `expected [ 'meta resync', …(17) ] to deeply equal [ 'meta resync', …(16) ]`, `+ "migrate security-catalog-overlays"` | ## Tests (on `3d63ed7db5`) - **Nightly tier** (`OS_TEST_TIERS=nightly`, `--project integration`, verify lock): the two card files, 59 passed. - **`create-objectstack`:** suite 17 files and 254 tests passed. `pnpm check:template-version-sync` is green with 40 assertions, the same count as on `main`. Both are unchanged, because this diff does not touch the package or the script. - **`@objectstack/cli`:** - unit layer: 279 files and 4126 tests passed; - `typecheck` (`tsc --noEmit` plus `check:test-typecheck`) is OK, and `tsconfig.test.json`'s `--listFiles` includes both edited files. - **Changeset:** `node scripts/check-empty-changeset.mjs --base origin/main` exits 0. It reports no empty-frontmatter changeset added and no merge-base changeset modified or deleted. - **Gates:** `dispatch-gates --commands` (no paths) derived 50 commands from the 2-file change set. `--ran` reports `50 derived, 50 run, 0 NOT-MEASURED, 0 UNRUN`, a derived zero: every command recorded `exit 0`. `check:type-check-debt` and `check:dual-build-cjs-loads` ran last, after every locked suite. - `dispatch-gates` noted that the tree is behind `origin/main`: 6 derived-from files changed there (`ci.yml`, `check-shard-attestation.mjs` and the fleet-write relay scripts). - No conflict exists, so per the round's direction there was no merge. CI judges the merge ref. - **Lint, proven narrowing:** 1. Population is read from ESLint's own config: both changed files return results with no "File ignored". 2. File count is from `--format json`: 2 results, 0 errors, 0 warnings. 3. Invariance: `eslint.config.mjs` enables no type-aware linting, and this diff touches neither the config nor a baseline it reads. ## Changeset Nothing publishes: `@objectstack/cli` ships `dist`, `README.md` and `CHANGELOG.md`, and this diff is two `test/` files. By the repo's rule this PR takes the `skip-changeset` label. An empty-frontmatter changeset would be the risky path that `check:empty-changeset` refuses. The seat applies the label. ## Acceptance notes - `objectstack migrate meta --write` on a project whose path has a `node_modules` segment says `the literal is in …/objectstack.config.ts, outside the project at …`, although the file is inside the project. The real cause is the `node_modules` segment rule. Only a test layout reaches it, so this is a wording polish, not a finding. No carrier. - Version Packages PR #21988 was last refreshed on 2026-10-08T06:19Z, before pre mode was entered (`a87d8be29`). This is release-lane state, recorded as a reading, ⛔ not acted on. --- _Generated by [Claude Code](https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1bb1da1 commit d8830c2

2 files changed

Lines changed: 53 additions & 1 deletion

File tree

‎packages/cli/test/json-stdout-purity.e2e.test.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,22 @@ const FAMILY: Record<string, string[]> = {
100100
'migrate files-to-references': [],
101101
'migrate meta': ['--stored'],
102102
'migrate multi-value-columns': [],
103+
// Its bare form is its only mode — a read-only plan (`deferSchemaDdl` +
104+
// `readOnlyProbe`). The fixture's database holds none of the platform tables
105+
// the ceremony inventories, so the face driven here is its REFUSAL face
106+
// (`plan_refused`, `emitJson(…, 1, { compact: true })`), emitted after the
107+
// stack is shut down. A refused plan writes no plan file, so the fixture
108+
// directory gains nothing.
109+
'migrate organization-ownership': [],
103110
'migrate plan': [],
104111
'migrate recorded-by': [],
105112
'migrate resume': [],
113+
// A preview is its DEFAULT and the only form driven here: without `--apply`
114+
// it composes the deployment the way `os serve` does (the host config's
115+
// plugins and the auth-gated security plugin) and boots read-only with no
116+
// `sys_metadata` hydration, so it deletes nothing. No table exists in the
117+
// fixture, so the face driven here is its empty listing.
118+
'migrate security-catalog-overlays': [],
106119
'migrate summary-nulls': [],
107120
// One object per run, so it needs a name: the fixture's own `jp_ticket`,
108121
// which the composed host config registers. No database file exists, so the

‎packages/cli/test/serve-boot-diagnostics-noise-budget.e2e.test.ts‎

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@
3030

3131
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
3232
import { execFile } from 'node:child_process';
33-
import { appendFileSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
33+
import { appendFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3434
import { join, resolve } from 'node:path';
3535
import { fileURLToPath } from 'node:url';
36+
import { PROTOCOL_MAJOR } from '@objectstack/spec/kernel';
3637
import { childEnv, randomPort, runServe, type ServeRun } from './helpers/serve-process.js';
3738

3839
const HERE = resolve(fileURLToPath(import.meta.url), '..');
@@ -122,6 +123,43 @@ let root: string;
122123
let scaffold: { code: number; output: string };
123124
let boot: ServeRun | undefined;
124125

126+
/**
127+
* Align the scaffold's `engines.protocol` with the protocol of the runtime this
128+
* file boots: the WORKSPACE `@objectstack/spec`, whose `PROTOCOL_MAJOR` this
129+
* reads.
130+
*
131+
* The template stamps the protocol of the packages a real project INSTALLS,
132+
* which is the published line (`create-objectstack`'s own major). That is
133+
* right for users: the Scaffold E2E gate installs from the registry and refuses
134+
* any other range. This file installs nothing — the project resolves the
135+
* workspace copies — so in a Changesets pre-mode window, where
136+
* `PROTOCOL_VERSION` moves ahead of the version pass (packages at 17.x beside
137+
* protocol 18), the pair it boots is one no user has: a published-line scaffold
138+
* on a next-major runtime. The handshake refuses that pair before a single boot
139+
* diagnostic is printed, and this file goes red on the setup rather than the
140+
* budget (review 6099406666 on #22622). So the setup aligns the pair it boots.
141+
*
142+
* Only the major's digits are rewritten. Outside such a window the template
143+
* already stamps `'^PROTOCOL_MAJOR'`, and the file is left byte-for-byte as
144+
* scaffolded.
145+
*
146+
* A direct rewrite of this one key, not `objectstack migrate meta --from N
147+
* --write`, the step the refusal prescribes: that codemod refuses to edit any
148+
* file under a `node_modules` path segment, and this project lives under this
149+
* package's `node_modules` so that its imports resolve. Measured on a scaffold
150+
* there: exit 0, "not written [outside-project]", config unchanged.
151+
*/
152+
function alignProtocolRange(configPath: string): void {
153+
const source = readFileSync(configPath, 'utf8');
154+
const stamp = /engines:\s*\{\s*protocol:\s*'\^(\d+)'\s*\}/;
155+
if (!stamp.test(source)) {
156+
throw new Error(`${configPath} declares no engines.protocol stamp, so the pair this file boots cannot be aligned`);
157+
}
158+
const aligned = source.replace(stamp, (literal, major: string) =>
159+
literal.replace(`'^${major}'`, `'^${PROTOCOL_MAJOR}'`));
160+
if (aligned !== source) writeFileSync(configPath, aligned);
161+
}
162+
125163
function scaffoldProject(cwd: string): Promise<{ code: number; output: string }> {
126164
return new Promise((resolveRun) => {
127165
execFile(
@@ -141,6 +179,7 @@ beforeAll(async () => {
141179
scaffold = await scaffoldProject(root);
142180
if (scaffold.code !== 0) return;
143181
const dir = join(root, PROJECT);
182+
alignProtocolRange(join(dir, 'objectstack.config.ts'));
144183
writeFileSync(join(dir, 'src', 'objects', 'ticket.object.ts'), TICKET_OBJECT);
145184
appendFileSync(join(dir, 'src', 'objects', 'index.ts'), "export { Ticket } from './ticket.object.js';\n");
146185
writeFileSync(join(dir, 'src', 'actions', 'ticket.actions.ts'), TICKET_ACTIONS);

0 commit comments

Comments
 (0)