Skip to content

Commit 87027c5

Browse files
test(cli): budget the os dev / os serve / package spawn files, drop the tsx launcher from two (#28601)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XHtyVkHSaxV7iEzXvwKNU1u)_ Part of #23181 (slice 25, the `os dev` / `os serve` / package family). The card stays open for the other slices. Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:`packages/cli/test` 里 6 个 `os dev` / `os serve` / 包相关的集成测试文件,各加一行第 2 行 `// test-budget:` 头(80 / 50 / 40 / 40 / 30 / 30 s);其中 2 个文件(package-restart-acceptance、package-owned-command-readers)把 spawn 方式从 `node_modules/.bin/tsx bin/run-dev.js` 改成 `process.execPath bin/run-dev.js`,同一个入口、同一份 `src/`,每次 spawn 少起一个 node 进程,本机 A/B 各快约 34%。 - **为什么改**:卡 #23181 的测试提速职责:超过 10 s 的测试文件要么提速,要么按裁决 6181973363 B 声明预算。这 6 个文件里每次 boot/spawn 都是该文件要钉住的主体(重启、多次 boot 抓竞态、每条腿一个输入),没有重复的(命令、参数、夹具)三元组可合并,所以地板是真实工作,走 budget 头。 - **风险与回滚**:只改测试文件,不发布任何东西(cli 的 `files[]` 只有 dist、README、CHANGELOG);断言一条没删、没放宽,超时、层、项目都没动;回滚即 revert。 - **先看**:`packages/cli/test/package-restart-acceptance.integration.test.ts`(spawn 改为 `process.execPath`);`packages/cli/test/package-owned-command-readers.test.ts`(同上);其余 4 个文件只加了头。 - **两分钟验证**:`pnpm --filter @objectstack/cli exec vitest run --project integration test/package-owned-command-readers.test.ts` → 8/8 通过。 - **需要你做的**:无,席位落地。 ## What changes Slice 23's offender table (`pm-orders/server/evidence/23181-s23/offenders-after-s23.md` in the project files) reads these six files over 10 s with no `// test-budget:` header. 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`: | file | 0735 | 7347 | 4739 | 9006 | 4267 | header | |:--|--:|--:|--:|--:|--:|--:| | `dev-boot-output-order.integration.test.ts` | 59.0 | **59.4** | 41.9 | 53.6 | 47.2 | 80 | | `package-restart-acceptance.integration.test.ts` | 19.7 | 27.2 | 21.9 | 34.4 | **35.1** | 50 | | `package-owned-command-readers.test.ts` | 31.1 | 19.3 | 23.6 | 30.2 | **31.3** | 40 | | `dev-organization-ownership-stale-column.integration.test.ts` | 28.9 | **29.0** | 18.1 | 25.2 | 21.1 | 40 | | `dev-standalone-self-heal.integration.test.ts` | 12.5 | 16.7 | 13.5 | **22.9** | 22.6 | 30 | | `serve-runtime-state-project-key.test.ts` | **19.7** | 19.0 | 12.0 | 17.5 | 14.8 | 30 | **Cause, per file.** Every file already runs its boots or spawns once, in a `beforeAll` (or, for the runtime-state file, in the cases that read them). I counted them against distinct (command, flags, fixture) triples: | file | tests | boots / spawns | distinct triples | cause | fix | |:--|--:|:--|--:|:--|:--| | `dev-boot-output-order` | 3 | 5 boots of the built `os dev` (supervisor + `serve` child) under a real pty | 5 (fresh database each) | the boots are the subject: the order defect was a race one boot can miss; each boot also holds 2 s after the block and ends with a typed Ctrl+C | header 80 | | `package-restart-acceptance` | 5 | 2 boots of the source `os serve --dev` on one database file | 2 | the restart is the subject; each spawn went through the tsx binary, whose launcher (`tsx/dist/cli.mjs`) starts a second node process for the same entry | `process.execPath` + header 50 | | `package-owned-command-readers` | 8 | 6 source-CLI spawns, two alive at once | 6 (doctor, `diff --json`, `migrate meta --json` over the two-package config and its one-package control) | one spawn per distinct pair; same tsx launcher cost | `process.execPath` + header 40 | | `dev-organization-ownership-stale-column` | 5 | 2 boots of the built `os dev` on one database file (the second runs the upgrade ceremony) | 2 | the fresh boot creates the tables the stale shape is staged on; the second boot is the subject | header 40 | | `dev-standalone-self-heal` | 4 | 3 boots of the built CLI on one database file | 3 (fresh `os dev`, restart over the staged index, production `os serve`) | each boot is a leg the file pins | header 30 | | `serve-runtime-state-project-key` | 12 | 3 child processes publishing the state file | 3 (project A, project B, a second boot of A) | each child loads the real writer from `src/commands/serve.ts` through tsx, about 4 s of module load apiece; the file's own import of `serve.ts` is about 16 s locally. Distinct pids and a clean exit per child are the subject | header 30 | **The tsx launcher.** `node_modules/.bin/tsx X` runs `tsx/dist/cli.mjs` in one node process, which then spawns a second node process for `X` (checked: under `tsx -e`, `process.ppid` is a `node …/tsx/dist/cli.mjs` process). `bin/run-dev.js` runs straight under `process.execPath`, as slice 12 already does for other spawn files (`serve-no-auth-secret-refusal`, `dev-standalone-self-heal`, `union-fold-command-parity`): the same entry, the same `src/`, the same `NODE_ENV` that `run-dev.js` pins. Measured without the test manifest, `node bin/run-dev.js diff --help` took 5.1 to 5.2 s against 8.9 to 10.2 s through the tsx binary. I tried the same move on `serve-runtime-state-project-key` (`node --import tsx/dist/loader.mjs CHILD`) and reverted it: about 4.0 to 4.3 s per child either way, because the cost there is loading `serve.ts`, not the launcher. Making the child lighter would mean moving the state writer out of `serve.ts`, a source change outside this slice. **Headers** (ruling 6181973363 letter B, class `spawns the real producer once per single-cause fixture`): N = the slowest of the five CI readings × 1.25, rounded up to 10 s. The two files whose spawn changed keep their pre-change CI reading; a shard-timings refresh resizes them. All six parse with `readTestBudget` (`scripts/measure-test-shard-timings.mjs`): 80, 50, 40, 40, 30, 30, each at line 2. Not changed: no assertion, case name, timeout, tier or project. No test helper (`test/helpers/**` is untouched). No source file. ## Tests - **A/B** (one box, one lock hold, base copied beside head as `zzbase-*`, interleaved, N=3, `--project integration --maxWorkers=1`, the test manifest global setup on as in CI's queue tier; vitest's `tests` figure, and the whole `Duration` in brackets; copies deleted before the gate run): | file | base (s) | head (s) | change | |:--|:--|:--|--:| | `package-restart-acceptance` | 23.51, 23.06, 23.24 (32.98, 32.57, 32.91) | 15.19, 15.55, 15.34 (24.65, 24.98, 24.72) | −34% (mean 23.27 → 15.36) | | `package-owned-command-readers` | 16.97, 17.18, 16.88 (29.32, 29.94, 29.59) | 11.02, 11.25, 11.46 (23.09, 24.06, 24.15) | −34% (mean 17.01 → 11.24) | Every run exit 0; restart-acceptance 5/5 and owned-command-readers 8/8 on both sides, same names. - **Header-only files**, run once on head in the same lock hold: `dev-boot-output-order` 3/3 (tests 46.02 s), `dev-organization-ownership-stale-column` 5/5 (21.79 s), `dev-standalone-self-heal` 4/4 (17.17 s), `serve-runtime-state-project-key` 12/12 (17.86 s, import 16.53 s). - No ablation: no tested behaviour changed (test-only). - `pnpm --filter @objectstack/cli typecheck` (tsc + `check:test-typecheck`) exit 0 on `9a33975d`, with `NODE_OPTIONS=--max-old-space-size=4096`. ## Acceptance notes - **`skip-changeset`.** Test-only. `@objectstack/cli`'s `files[]` is `dist`, `README.md`, `CHANGELOG.md`, and its build does not emit `test/**` into `dist/`, so no `files[]` content changes. - `serve-runtime-state-project-key` and `package-owned-command-readers` sit in `vitest-tiers.ts`'s `integration` tier already (they spawn); nothing moved. ## Verification All readings on head `9a33975d` (my commit `eb79e981` plus one merge of `origin/main` after slice 23, #28581, landed; that merge touches none of these six files). - **Derived gate families:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` → 48 families, each run with its exit code captured before any pipe, all exit 0. On the first round (before the merge) two were container prerequisites, re-read after meeting them: `check-plugin-teardown-shape --self-test` exit 3 (shallow clone) → `git fetch --depth=1` of its pinned fixture commit → exit 0; `check:dual-build-cjs-loads` exit 3 (no full build) → `pnpm exec turbo run build --concurrency=2` (118/118) → exit 0. The whole set then re-ran on `9a33975d`. `--ran` reconciliation: `✓ dispatch-gates --ran: 48 derived famil(ies) accounted for — 48 run, 0 NOT-MEASURED (a DERIVED zero — all 48 recorded an exit code and none of them is 3)`. List with codes: `pm-orders/server/evidence/23181-s25/gates-ran-9a33975d.txt` in the project files. - **Hand gates:** `pnpm exec eslint --no-inline-config` over the six 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. - **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_01FEkQ5peeAMkJqa2CHQj7ia Co-authored-by: Claude <noreply@anthropic.com>
1 parent 519089e commit 87027c5

6 files changed

Lines changed: 13 additions & 4 deletions

‎packages/cli/test/dev-boot-output-order.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: five sequential boots of the built `os dev` (a supervisor plus its `serve` child) under a real pty, each on a fresh database, held 2 s past the MCP block and stopped by a typed Ctrl+C; the boots are the subject, because the order defect was a race one boot can miss; slowest CI reading 59.4 s of the five scheduled runs 41199450735…41271604267, x 1.25, rounded up
23

34
/**
45
* #22410 — the first screen of `os dev` prints in ONE fixed order, each block

‎packages/cli/test/dev-organization-ownership-stale-column.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: 40 — spawns the real producer once per single-cause fixture: two sequential boots of the built `os dev` on one database file (a fresh boot that creates the tables the stale shape is staged on, then the refused boot that runs the upgrade ceremony and starts), each stopped by SIGTERM; slowest CI reading 29.0 s of the five scheduled runs 41199450735…41271604267, x 1.25, rounded up
23

34
/**
45
* #26615 — the card's third target, run against the BUILT CLI: a development

‎packages/cli/test/dev-standalone-self-heal.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: three sequential boots of the built CLI on one database file (a fresh `os dev`, an `os dev` restart over the staged legacy index, a production `os serve` over the restaged index), each a leg the file pins, stopped by SIGTERM; slowest CI reading 22.9 s of the five scheduled runs 41199450735…41271604267, x 1.25, rounded up
23

34
/**
45
* #21733 — the card's acceptance, run against the BUILT CLI: a plain `os dev`

‎packages/cli/test/package-owned-command-readers.test.ts‎

Lines changed: 5 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: 40 — spawns the real producer once per single-cause fixture: six spawns of the source CLI, one per distinct (command, shape) pair (doctor, diff --json, migrate meta --json, each over the two-package config and its one-package control), two alive at once; slowest CI reading 31.3 s of the five scheduled runs 41199450735…41271604267 (spawned through the tsx binary then), x 1.25, rounded up
23

34
/**
45
* #22288 — `os doctor`, `os diff` and `os migrate meta` judged a multi-package
@@ -35,7 +36,7 @@ import { tmpdir } from 'node:os';
3536
import { delimiter, join } from 'node:path';
3637
import { FILE_REFERENCES_MIGRATION_ID } from '@objectstack/spec/system';
3738

38-
import { CLI, TSX, childEnv } from './helpers/serve-process.js';
39+
import { CLI, childEnv } from './helpers/serve-process.js';
3940
import { linkSpec } from './helpers/define-stack-fixture.js';
4041

4142
interface Run { code: number; stdout: string; stderr: string }
@@ -76,8 +77,10 @@ let stubBinDir = '';
7677

7778
function runCli(args: string[], cwd: string): Promise<Run> {
7879
return new Promise((resolvePromise) => {
80+
// `process.execPath`, not the tsx binary: the same `bin/run-dev.js` entry and
81+
// the same `src/`, without the second node process tsx's launcher starts.
7982
execFile(
80-
TSX,
83+
process.execPath,
8184
[CLI, ...args],
8285
// Artifact-less on purpose: the config is the only input these commands read.
8386
// The stub `pnpm` leads `PATH` for every child; only `doctor` calls it.

‎packages/cli/test/package-restart-acceptance.integration.test.ts‎

Lines changed: 4 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: 50 — spawns the real producer once per single-cause fixture: two sequential boots of the source `os serve --dev` on one database file, the restart being the subject, each stopped by SIGTERM; slowest CI reading 35.1 s of the five scheduled runs 41199450735…41271604267 (spawned through the tsx binary then), x 1.25, rounded up
23

34
/**
45
* #17676 ruling A' item 5 — the card's acceptance, run as HTTP against the real
@@ -63,7 +64,6 @@ import {
6364
CLI,
6465
E2E_SECRET_KEY,
6566
KERNEL_ASSIGNED_PORT,
66-
TSX,
6767
childEnv,
6868
probeThroughChild,
6969
readKernelAssignedPort,
@@ -110,7 +110,9 @@ const dirs: string[] = [];
110110
/** Boot `os serve --dev` on `db` and keep it running until {@link stop}. */
111111
function bootServe(dir: string, db: string): Promise<LiveServe> {
112112
return new Promise((resolveBoot, rejectBoot) => {
113-
const child = spawn(TSX, [CLI, 'serve', 'objectstack.config.ts', '-p', KERNEL_ASSIGNED_PORT, '--dev'], {
113+
// `process.execPath`, not the tsx binary: the same `bin/run-dev.js` entry and
114+
// the same `src/`, without the second node process tsx's launcher starts.
115+
const child = spawn(process.execPath, [CLI, 'serve', 'objectstack.config.ts', '-p', KERNEL_ASSIGNED_PORT, '--dev'], {
114116
cwd: dir,
115117
stdio: ['pipe', 'pipe', 'pipe'],
116118
// `childEnv`, never a bare `...process.env` — see its header.

‎packages/cli/test/serve-runtime-state-project-key.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: three sequential child processes (project A, project B, a second boot of A), each loading the real `os serve` state writer from `src/commands/serve.ts` through tsx, about 4 s of module load apiece; distinct pids and a clean exit per child are the subject; slowest CI reading 19.7 s of the five scheduled runs 41199450735…41271604267, x 1.25, rounded up
23

34
/**
45
* #15733 — the runtime state file is keyed by the PROJECT, not by the

0 commit comments

Comments
 (0)