Skip to content

Commit 882fcdb

Browse files
test(cli): speed up the os migrate test family and budget its spawn floors (#28610)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XXJFU9vDsux28Wt28Go65Se)_ Part of #23181 (slice 24, the `os migrate` family). The card stays open: slices 25 to 28 take other files from slice 23's offender table. Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:`packages/cli` 里 8 个 `os migrate` 测试文件。`apply.remote-posture` 的 stderr 桩改为回调写入回调,每次命令运行不再白等 2 s;两个 `os migrate meta` 文件改用本进程的 node 跑 `bin/run-dev.js`,不再经 tsx 二进制;6 个地板是真实 spawn/boot 的文件各加一行第 2 行 `// test-budget:` 头。 - **为什么改**:卡 #23181 的测试提速职责:超过 10 s 的测试文件要么提速,要么按裁决 6181973363 B 声明预算。 - **风险与回滚**:只改测试文件,不发布任何东西(cli 的 `files[]` 只有 dist、README、CHANGELOG);没有删、没有放宽任何断言,没有改超时、层级或项目;回滚即 revert。 - **先看**:`packages/cli/src/commands/migrate/apply.remote-posture.test.ts`(stderr 桩,约 141 行);`packages/cli/test/migrate-meta-default-range.test.ts`(spawn 入口);其余只加了头。 - **两分钟验证**:`pnpm --filter @objectstack/cli exec vitest run src/commands/migrate/apply.remote-posture.test.ts` → 12/12 通过,本地约 12 s(改前约 34 s)。 - **需要你做的**:无,席位落地。 ## What changes The eight files are slice 23's offender rows for the `os migrate` family. Readings are the default reporter's per-file module line in the Test Core shard logs of the five scheduled `ci.yml` runs `41199450735`, `41217827347`, `41237094739`, `41255489006`, `41271604267` (table: `pm-orders/server/evidence/23181-s23/offenders-after-s23.md` in the project files). **Cause, per file.** I counted spawns, boots and distinct (command, flags, fixture) triples in each file, then timed it locally. | file | CI max (s) | tests | spawns / boots | distinct triples | cause | fix | |:--|--:|--:|:--|--:|:--|:--| | `migrate/plan.boot-parity.integration` | 60.6 | 3 | 6 source-CLI spawns (a serving provision-and-exit boot plus `migrate plan --json`, per app shape) | 6 | one spawn per distinct triple, about 5.5 s each locally | header 80 | | `test/migrate-meta-default-range` | 46.3 | 8 (1 skipped) | 4 `migrate meta` spawns, already run once each and shared | 4 | each spawn went through the `tsx` binary (about 7 s locally) | spawn on `process.execPath`; header 60 | | `migrate/plan.nullable-tenant-columns.integration` | 35.5 | 1 | 1 serving boot, 3 `migrate plan` spawns | 4 | one spawn per distinct triple (the store changes between plans) | header 50 | | `migrate/resume.recorded-by.integration` | 34.6 | 9 | 4 crash children (each boots the data stack), 2 `os serve` boots, 7 in-process `migrate resume` boots, all in one `beforeAll` | 13 | each crash is a distinct chunk plan, each serve and resume runs on its own copy | header 50 | | `migrate/apply.remote-posture` | 25.7 | 12 | 16 in-process command runs over a stubbed stack, no database | 16 | **a real wait:** the stderr spy never called its write callback, so `exitOneShotCommand` waited out its 2 s drain timeout on every run | spy calls back; header 40 | | `utils/schema-migrate.one-shot-family.integration` | 17.7 | 129 | 1 served boot, then one in-process command boot per case, each on its own copy | 129 | distinct (command, mode, state) per case | header 30 | | `migrate/plan.deferred-reads.integration` | 16.2 | 2 | 2 `migrate plan` spawns (human, `--json`) | 2 | one spawn per distinct triple | header 30 | | `test/migrate-meta-engine-guidance` | 12.0 | 3 | 1 `migrate meta` spawn, shared by every case | 1 | the one spawn went through the `tsx` binary | spawn on `process.execPath`; no header (about 4 s locally) | - **`apply.remote-posture`.** Both commands end in `exitOneShotCommand` (`src/utils/one-shot-exit.ts`), which writes an empty chunk to stdout and stderr and waits for each callback, up to 2 s. The file's stdout spy already called back; its stderr spy returned `true` and dropped the callback, so every run waited 2 s (most one-run cases read 2.02 to 2.05 s locally, each two-run case about 4.05 s). The stderr spy now calls back the same way the stdout spy does, and stderr output is still discarded. What is left is the beforeAll's import of the apply and plan graph and the first oclif config load (the first case reads about 4.3 s), so the file takes a `whole-package import` header from its slowest CI reading on the old shape. - **The two `migrate meta` files** now spawn `bin/run-dev.js` with `process.execPath`, as the rest of the family has since slice 12 (`plan.boot-parity`, `object-scope`, `unmapped-columns`). The entry, `src/` and the pinned `NODE_ENV=development` are the same; the `tsx` wrapper only added a second process and its own transform. The files stay in the `integration` tier by behaviour (`vitest-tiers.ts` still sees `child_process` plus `run-dev.js`). `default-range` keeps 4 distinct spawns, so it still takes a header from its CI reading on the tsx shape. - **Headers** (ruling 6181973363 letter B): N = the slowest of the five CI readings × 1.25, rounded up to 10 s. Class `spawns the real producer once per single-cause fixture` for the spawn and boot files, `whole-package import` for `apply.remote-posture`. All six parse with `readTestBudget` (`scripts/measure-test-shard-timings.mjs`) at line 2. Not changed: no assertion, case name, timeout, tier or project. No source file, and no shared test helper (`childEnv` from `test/helpers/serve-process.ts` is only called, as before). ## Tests - **A/B** (one box, one lock hold, base copied beside head as `zzbase.*`, interleaved and alternating order, N=3, `--maxWorkers=1`, default reporter file line; copies deleted before the gate run): | file | base (s) | head (s) | change | |:--|:--|:--|--:| | `apply.remote-posture` | 34.44, 34.63, 34.45 | 12.23, 11.82, 11.49 | −66% (mean 34.51 → 11.85) | | `migrate-meta-default-range` | 28.15, 28.28, 27.69 | 16.45, 16.50, 17.38 | −40% (mean 28.04 → 16.78) | | `migrate-meta-engine-guidance` | 6.81, 7.35, 7.23 | 3.98, 4.41, 4.08 | −42% (mean 7.13 → 4.16) | Every run exit 0, same test counts on both sides (12, 8 with 1 skipped, 3). - **Header-only files**, run once on base (header lines are comments): `plan.boot-parity` 3/3 (34.3 s), `plan.nullable-tenant-columns` 1/1 (23.2 s), `resume.recorded-by` 9/9 (27.2 s), `schema-migrate.one-shot-family` 129/129 (11.8 s), `plan.deferred-reads` 2/2 (10.3 s). - **All eight on head**, after merging `origin/main` (slice 23 landed as `617dcf43`): head `90445cc3`, one `vitest run --maxWorkers=1 --reporter=default` over the eight files, Test Files 8/8, Tests 166 passed and 1 skipped (the registry-selected `skipIf` case, skipped on base too). File lines: boot-parity 37.1 s, resume 24.4 s, nullable 24.5 s, default-range 18.3 s, one-shot-family 14.5 s, deferred-reads 12.7 s, engine-guidance 4.8 s, remote-posture 8.6 s (the gate sweep ran beside it on the same cores). - `pnpm --filter @objectstack/cli typecheck` (tsc plus `check:test-typecheck`) exit 0 on `e6ea56ec`, with `NODE_OPTIONS=--max-old-space-size=4096`. The merge brought no change to `packages/cli`'s test layer. - No ablation: no tested behaviour changed (test-only). ## Acceptance notes - **`skip-changeset`.** Test-only. `@objectstack/cli`'s `files[]` is `dist`, `README.md`, `CHANGELOG.md`, and its build emits no test file into `dist/`, so no `files[]` content changes. - **`migrate-meta-engine-guidance` takes no header.** After the entry change it reads about 4 s locally, and its slowest CI reading on the tsx shape was 12.0 s (base locally about 7 s), so it should land under 10 s. If the next scheduled reading still puts it over, its floor is one real spawn and it takes a header then. - **`apply.remote-posture` should now read well under its header.** The header uses the old-shape CI reading (the rule is CI readings, never local ones); a shard-timings refresh re-sizes it. ## Verification All gate readings on head `90445cc3` (the merge of `origin/main` into this branch's two commits), after `pnpm exec turbo run build --concurrency=2` (118/118 tasks). - **Derived gate families:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` → 52 families (the same 52 as before the merge), each run with its exit code captured before any pipe: 52 exit 0. `--ran` reconciliation: `✓ dispatch-gates --ran: 52 derived famil(ies) accounted for — 52 run, 0 NOT-MEASURED`. On the pre-merge head `e6ea56ec` three first reads were container prerequisites and were re-read after meeting them: `check-plugin-teardown-shape --self-test` exit 3 (shallow clone) → `git fetch --depth=1` of its control commit → exit 0; `check:dual-build-cjs-loads` and `check:i18n-coverage` exit 3 (no full build) → full build → both exit 0. - **Hand gates:** `pnpm exec eslint --no-inline-config` over the eight files exit 0 (`@objectstack/cli` has no `lint` script; the repo-wide `pnpm lint` is CI's); `node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD` exit 0 (3 commits). - **CI** families (Test Core with the cli integration tier, lint farm, type-check lanes) are read off this PR's check-runs by the seat. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FEkQ5pgVXsp3aShmRuEpgc --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f3d84b3 commit 882fcdb

8 files changed

Lines changed: 31 additions & 5 deletions

‎packages/cli/src/commands/migrate/apply.remote-posture.test.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
// test-budget: 40 — whole-package import: the beforeAll imports the apply and plan commands' module graph, and the first command run pays oclif's config load (every command module of the package, where no manifest is on disk); after that each of the 16 runs over a stubbed stack takes well under a second; slowest CI reading 25.7 s (run 41217827347, while each run still waited out the 2 s stderr drain this file no longer leaves unanswered) of the five scheduled CI runs 41199450735, 41217827347, 41237094739, 41255489006, 41271604267, x 1.25, rounded up
23

34
/**
45
* #23848 — `os migrate plan` and `os migrate apply` on a primary whose
@@ -138,7 +139,14 @@ async function run(
138139
if (cb) cb();
139140
return true;
140141
}) as typeof process.stdout.write);
141-
vi.spyOn(process.stderr, 'write').mockImplementation((() => true) as typeof process.stderr.write);
142+
// Discarded, but its callback still fires, as the stdout spy's does: both commands end in
143+
// `exitOneShotCommand`, which waits for each stream's drain callback and gives up only after
144+
// its 2 s timeout. A spy that never calls back cost every run here that 2 s (#23181 slice 24).
145+
vi.spyOn(process.stderr, 'write').mockImplementation(((_chunk: unknown, ...rest: unknown[]) => {
146+
const cb = rest.find((a) => typeof a === 'function') as (() => void) | undefined;
147+
if (cb) cb();
148+
return true;
149+
}) as typeof process.stderr.write);
142150
const capture = (...parts: unknown[]) => { said.push(parts.map(String).join(' ')); };
143151
vi.spyOn(console, 'log').mockImplementation(capture);
144152
vi.spyOn(console, 'warn').mockImplementation(capture);

‎packages/cli/src/commands/migrate/plan.boot-parity.integration.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
// test-budget: 80 — spawns the real producer once per single-cause fixture: three app shapes (a requires: app, a host config, a compiled artifact with no config), each one serving provision-and-exit boot and one os migrate plan --json over the database it left, six spawns of the source CLI, every one a distinct triple; slowest CI reading 60.6 s (run 41271604267) of the five scheduled CI runs 41199450735, 41217827347, 41237094739, 41255489006, 41271604267, x 1.25, rounded up
23

34
import { describe, it, expect, afterAll } from 'vitest';
45
import { spawnSync } from 'node:child_process';

‎packages/cli/src/commands/migrate/plan.deferred-reads.integration.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
// test-budget: 30 — spawns the real producer once per single-cause fixture: two spawns of the source CLI, os migrate plan on an absent database in its human and --json faces, each a distinct triple; slowest CI reading 16.2 s (run 41217827347) of the five scheduled CI runs 41199450735, 41217827347, 41237094739, 41255489006, 41271604267, x 1.25, rounded up
23

34
/**
45
* [#20821] `os migrate plan` against a database that does not exist yet prints

‎packages/cli/src/commands/migrate/plan.nullable-tenant-columns.integration.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
// test-budget: 50 — spawns the real producer once per single-cause fixture: one serving provision-and-exit boot and three spawns of os migrate plan over it (--json on the fresh store, --json on the 18.0.0-shaped store, the human face on that store), every one a distinct triple; slowest CI reading 35.5 s (run 41271604267) of the five scheduled CI runs 41199450735, 41217827347, 41237094739, 41255489006, 41271604267, x 1.25, rounded up
23

34
import { describe, it, expect, afterAll } from 'vitest';
45
import { spawnSync } from 'node:child_process';

‎packages/cli/src/commands/migrate/resume.recorded-by.integration.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
// test-budget: 50 — spawns the real producer once per single-cause fixture: four crash children that each boot the data stack and are SIGKILLed inside a distinct chunk plan (default, killed after a committed chunk, chunk size 2, a renamed step), two os serve boots (over the interrupted run, and the fresh control) and seven in-process os migrate resume boots over their own copies, all in one beforeAll; slowest CI reading 34.6 s (run 41217827347) of the five scheduled CI runs 41199450735, 41217827347, 41237094739, 41255489006, 41271604267, x 1.25, rounded up
23

34
/**
45
* [#21498] An interrupted `os migrate recorded-by` run resumes to completion

‎packages/cli/src/utils/schema-migrate.one-shot-family.integration.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
// test-budget: 30 — spawns the real producer once per single-cause fixture: one served boot writes the template database, then its 129 cases boot in process, each over its own copy (every bootSchemaStack caller's modes on a present file, on an absent file and in a development key home, plus the lifecycle and key-home controls), each a distinct (command, mode, state); slowest CI reading 17.7 s (run 41255489006) of the five scheduled CI runs 41199450735, 41217827347, 41237094739, 41255489006, 41271604267, x 1.25, rounded up
23

34
/**
45
* [#21391] Every command that boots through `bootSchemaStack` keeps the two

‎packages/cli/test/migrate-meta-default-range.test.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
// test-budget: 60 — spawns the real producer once per single-cause fixture: four spawns of the source CLI's os migrate meta, each run once and shared by every case that reads it (the tombstone's --from N in its --json and human faces and the empty --from N --to N range, all on the retired-key stack; the oldest supported range on the canonical stack), every one a distinct triple; slowest CI reading 46.3 s (run 41271604267, on the tsx-binary spawn this file no longer takes) of the five scheduled CI runs 41199450735, 41217827347, 41237094739, 41255489006, 41271604267, x 1.25, rounded up
23

34
/**
45
* `os migrate meta` — the DEFAULT `--to`, and what an empty range is allowed to
@@ -83,8 +84,14 @@ import { childEnv } from './helpers/serve-process.js';
8384

8485
const execFileP = promisify(execFile);
8586
const HERE = resolve(fileURLToPath(import.meta.url), '..');
87+
/**
88+
* The source entry is spawned by this process's own `node`, as the `os migrate` family's other
89+
* spawners (`plan.boot-parity`, `object-scope`, `unmapped-columns`) spawn it. Not through the `tsx`
90+
* binary: the entry, its `src/` and its pinned `NODE_ENV=development` are the same either way, and
91+
* the wrapper only adds a second process and its own transform, ~3.4 s of every spawn (#23181
92+
* slice 12: `tsx bin/run-dev.js --version` 8.28-8.44 s, `node` 4.93-4.99 s).
93+
*/
8694
const CLI = resolve(HERE, '../bin/run-dev.js');
87-
const TSX = resolve(HERE, '../../../node_modules/.bin/tsx');
8895

8996
const RENAME_CONVERSION = 'dashboard-refresh-interval-to-refresh-interval-seconds';
9097

@@ -144,7 +151,7 @@ function runMeta(args: string[], cwd: string): Promise<Run> {
144151
const key = `${cwd}::${args.join(' ')}`;
145152
const hit = runs.get(key);
146153
if (hit) return hit;
147-
const started = execFileP(TSX, [CLI, 'migrate', 'meta', ...args], {
154+
const started = execFileP(process.execPath, [CLI, 'migrate', 'meta', ...args], {
148155
cwd,
149156
maxBuffer: 64 * 1024 * 1024,
150157
env: childEnv({ NO_COLOR: '1' }),

‎packages/cli/test/migrate-meta-engine-guidance.test.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,14 @@ import { childEnv } from './helpers/serve-process.js';
6666

6767
const execFileP = promisify(execFile);
6868
const HERE = resolve(fileURLToPath(import.meta.url), '..');
69+
/**
70+
* The source entry is spawned by this process's own `node`, as the `os migrate` family's other
71+
* spawners (`plan.boot-parity`, `object-scope`, `unmapped-columns`) spawn it. Not through the `tsx`
72+
* binary: the entry, its `src/` and its pinned `NODE_ENV=development` are the same either way, and
73+
* the wrapper only adds a second process and its own transform, ~3.4 s of every spawn (#23181
74+
* slice 12: `tsx bin/run-dev.js --version` 8.28-8.44 s, `node` 4.93-4.99 s).
75+
*/
6976
const CLI = resolve(HERE, '../bin/run-dev.js');
70-
const TSX = resolve(HERE, '../../../node_modules/.bin/tsx');
7177

7278
/** A tracker id as author-shown prose must not carry it: `#` and four or five digits. */
7379
const TRACKER_ID = /#\d{4,5}\b/;
@@ -381,7 +387,7 @@ beforeAll(async () => {
381387
writeFileSync(join(dir, 'objectstack.config.ts'), FAMILY_FIXTURE);
382388
const toMajor = Math.max(...FAMILY.map((e) => e.toMajor));
383389
const run = await execFileP(
384-
TSX,
390+
process.execPath,
385391
[CLI, 'migrate', 'meta', '--from', String(MIGRATION_SUPPORT_FLOOR), '--to', String(toMajor), '--all'],
386392
{ cwd: dir, maxBuffer: 16 * 1024 * 1024, env: childEnv({ NO_COLOR: '1' }) },
387393
);

0 commit comments

Comments
 (0)