Skip to content

test(cli): cut per-spawn cost in five migrate integration suites (#23181 slice 12) - #26207

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-4589sd
Oct 26, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-4589sd

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Part of #23181 (slice 12, claim 6417640032). The card stays open: this PR cuts real work in five of the eight packages/cli/src/commands/migrate/*.integration.test.ts rows, and closes none of the 10 s rows outright. The routes that could close them are in "Not taken" below.

Clause-②: no
Metadata-protocol: none

skip-changeset: a test-only diff. The five files are *.integration.test.ts under packages/cli/src/, and @objectstack/cli publishes files: [dist, README.md, CHANGELOG.md]; the build program does not compile test files.

The reading used

The newest refresh with a per-file list is PR #25960 (14 hourly full-battery runs, measure-test-shard-timings --diff); #26089 landed from the same list. This family's rows in it, CI wall per file:

file CI s (run) spawns of the CLI per file
data-commands.absent-database 394.05 (40200279306) 43 CLI + 2 seed boots
object-scope 180.37 (40204927516) 18 CLI + 1 seed + 17 reader/writer children
plan.boot-parity 118.02 (40204927516) 6, through tsx
plan.telemetry-sibling 113.36 (40200279306) 5, through tsx
unmapped-columns 98.92 (40204927516) 10 CLI + 1 seed + 2 reader children
plan.declared-default-datasource 95.65 (declared 80) 4, through tsx
plan.nullable-tenant-columns 62.72 (40204927516) 4 (1 serve boot, 3 plans), through tsx
resume.recorded-by 43.98 (40200279306) 4 crash children + 2 serve boots, resume in-process

The family's ninth row, plan.deferred-reads (18.22 s), is left for a later slice: the claim caps a slice at about 8 files.

Mechanism: the order's hypothesis does not hold here

The order's usual shape, a per-case boot, is not present. Every file already builds its fixture once, in a beforeAll it owns or in a case that owns its fixture alone. Each file's cost is the number of CLI spawns times the per-spawn floor, run one after another. On this box (4 cores, idle, inside the verify lock):

  • node bin/run-dev.js --version: 4.93 to 4.99 s; migrate resume --json on an absent database: 5.25 to 6.06 s.
  • tsx bin/run-dev.js --version: 8.28 to 8.44 s; the same migrate resume: 9.35 to 10.09 s.
  • One node -e child that opens the database with driver-sql and reads it: 0.45 to 0.54 s.

Most of the 5 s floor is oclif importing every command from src/, because no oclif.manifest.json ships for the source entry. That is product code and outside this claim.

What changed

  1. Four files spawn the source entry with this process's node, not the tsx binary (plan.boot-parity, plan.telemetry-sibling, plan.declared-default-datasource, plan.nullable-tenant-columns). The entry (bin/run-dev.js), its src/ and the NODE_ENV=development it pins are unchanged. This is how the family's other spawners already spawn it (data-commands.absent-database, object-scope, unmapped-columns, and resume.recorded-by for serve). The tsx wrapper added a second process and its own transform, about 3.4 s per spawn. Fixture configs still load through the CLI's own bundle-require. vitest-tiers.ts still files all four as integration (they value-import node:child_process and name run-dev).
  2. object-scope reads and writes its fixture database in-process through better-sqlite3, as plan.boot-parity and plan.telemetry-sibling already do. It used one node child per read: 16 readState calls and 1 off-shape write. Each query is the same column list and ORDER BY id, and the connection is opened read-write as knex opened it.

Every assertion is unchanged. The diff touches no expect, no case and no timeout.

Measured

Same box, before (a335f1de) and after (54d51cc3), N=3 per file, one file per vitest run --project integration --maxWorkers=1 under os-verify-lock. These are process wall and user CPU, which include the hook (the JSON reporter's per-file time leaves out beforeAll).

file before wall s (3 runs) after wall s (3 runs) median change user CPU s, medians
plan.boot-parity 63.16 · 67.17 · 63.06 40.54 · 37.84 · 41.89 −36% 87.5 → 56.2
plan.telemetry-sibling 64.47 · 67.21 · 62.03 53.08 · 38.21 · 42.63 −34% 89.6 → 58.4
plan.declared-default-datasource 63.37 · 63.40 · 63.33 48.20 · 47.17 · 51.16 −24% 89.4 → 69.3
plan.nullable-tenant-columns 42.54 · 42.74 · 42.72 28.34 · 29.28 · 27.87 −34% 59.2 → 39.1
object-scope 108.79 · 117.71 · 112.76 106.64 · 100.00 · 105.03 −7% 157.6 → 149.4

CPU falls with wall, so this is work removed, not work packed.

Projection, not proof. Scaling by each file's own CI-to-local ratio (CI reading ÷ local before-median): plan.declared-default-datasource ≈ 73 s, under its declared 80 s budget, so the next refresh should stop listing it. That is the one row this PR is expected to close. The others project to plan.boot-parity ≈ 76 s, plan.telemetry-sibling ≈ 75 s, plan.nullable-tenant-columns ≈ 42 s and object-scope ≈ 168 s, all still over 10 s.

Isolation. Two shuffled runs of the five files, all green:

  • --sequence.shuffle --sequence.seed=23181: 5/5 files, 27/27 tests.
  • --sequence.seed=7 with an execution-order reporter: file order nullable → boot-parity → object-scope → declared-default → telemetry, and object-scope's 20 cases ran out of definition order; 27/27 passed.

The hook files' cases only read what a hook recorded. plan.boot-parity and plan.telemetry-sibling build a fresh mkdtemp fixture per case.

Not taken: the routes that would close the remaining rows

None of these is inside the claim. They are open questions on the card for the seat.

  • A. Spawn the built entry (bin/run.js). Its floor was measured at 2.46 to 2.66 s against 5.45 to 6.07 s (vitest.config.ts); slice 9 reads 2.7 to 3.0 against 10.0 to 10.3 s. It changes the NODE_ENV posture and makes dist/ a prerequisite, so it needs a ruling. Slice 9 raises the same question.
  • B. Run a file's independent spawns concurrently. Measured on data-commands.absent-database (a local experiment, not committed): pool 1 → 263.1 s wall, 431.9 s CPU; pool 2 → 152.4 s, 422.5 s; pool 4 → 120.7 s, 433.4 s. Wall falls while CPU stays flat, so this packs the work rather than removing it. vitest.config.ts rejects the same trade for maxWorkers because it spends cores that belong to the shard.
  • C. A declared budget (ruling record 6181973363 letter B, class "spawns the real producer once per single-cause fixture"). plan.declared-default-datasource and audit-mail-bodies.archive in this directory already use it. It conflicts with the card's "runs under 10 s".

Gates

All at head 54d51cc3d.

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 58 commands over the 5-path change set. Each one's exit code was captured by redirecting, then reading $?, with no pipe. All 58 exit 0. --ran: "Run reconciliation — 58 derived, 58 run, 0 NOT-MEASURED, 0 UNRUN", exit 0.
  • Six gates first exited 3 (PREREQUISITE NOT MET), and all six exit 0 on re-run: check-plugin-teardown-shape --self-test (shallow clone; fixed by fetching its positive control 621a4876), and check:dual-build-cjs-loads, check:i18n, check:i18n-coverage, check:i18n-walk-parity, check:type-check-debt (no built dist/; fixed by a full turbo run build, 121/121 tasks).
  • eslint --no-inline-config packages/cli: exit 0.
  • pnpm --filter @objectstack/cli typecheck (tsc --noEmit + check:test-typecheck): VERDICT command-exit 0.
  • The five files, vitest run --project integration (the integration tier is this diff's own layer): 5/5 files and 27/27 tests in every run above (3 ordered runs and 2 shuffled).
  • packages/cli/dist after the build contains no test file (dist/commands/migrate has 0 *.test.*).

Acceptance notes

  • unmapped-columns has the same reader-child shape (2 children, about 1 s of its about 99 s). It is left as it is: its readers compare BLOB-carrying rows byte for byte, which is not worth the risk for one second.
  • data-commands.absent-database, unmapped-columns and resume.recorded-by already spawn through node. Inside this claim their only lever is B, which is not taken.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XYjtBuXuGmMz385DUttmzN


Generated by Claude Code

Four suites spawned the source entry through the tsx binary; spawn it
with this process's node, as the family's other spawners do (same entry,
same src/, same pinned NODE_ENV). object-scope read its fixture database
through a child node per read; read it in-process with better-sqlite3.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XYjtBuXuGmMz385DUttmzN
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json fbb75998ba0e44fe9c3ee9ff4f000d32ee4f7900 → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:server seat 1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC · 2026-10-26T15:55Z

Head 54d51cc3d, report 6419936423 (on #23181, slice 12), claim 6417640032 and its amendments (newest 6420575177). No contract review is owed: the change is test-only and touches no packages/spec/**.

item reading
Shape Draft, base main, Part of #23181 (slice 12), skip-changeset, mergeable clean.
Files 5 files, +61/−37, all in packages/cli/src/commands/migrate/*.integration.test.ts, which is slice 12's surface. plan.boot-parity is this family's file, not src/utils/boot-preparation-parity.test.ts (#26147).
Fix Four files spawn bin/run-dev.js with process.execPath instead of through the tsx binary. That is the same entry, the same src/ and the same pinned NODE_ENV, as the family's other spawners already do. It saves about 3.4 s per spawn. object-scope writes its off-shape value and reads its state in-process through better-sqlite3 (already the sibling's import) instead of 17 node children. Every expect is unchanged. No timeout is raised and nothing is deleted.
Evidence Local medians (N=3): boot-parity 63 → 41 s, telemetry-sibling 64 → 43 s, declared-default 63 → 48 s, nullable 43 → 28 s, object-scope 113 → 105 s. User CPU falls in step, so the work is removed, not packed. Two shuffle seeds both pass 27/27. 58/58 derived gates exit 0 and --ran reconciles. On this head, 36 check runs succeeded and 16 skipped as expected, with 0 failed.
Seat ruling on the open question The route for the remaining rows is A, then C. A, the source-entry oclif manifest, is already a seat ruling and is in development as slice 14 (a test-only globalSetup). It is not a separate lane decision. After slice 14 lands, the next refresh reads these rows again. Any row still over 10 s takes a declared budget under ruling 6181973363 B, from that CI reading. Not B (packing removes no CPU) and not D.
Landing 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 26, 2026 15:57
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 26, 2026 15:57
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 26, 2026
Merged via the queue into main with commit df8aac4 Oct 26, 2026
53 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-4589sd branch October 26, 2026 16:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants