Repository navigation
test(verify, rest, create-objectstack): test budget slice 5, the warm-up docblock says what it shares and two rows run under 10 s - #24249
Conversation
…wn boots The docblock said a warm-up "shares nothing a case asserts on". An ablation of the harness crypto provider falsified that: the process's provider is built on the process's first boot, so a warm-up builds it before a file's own posture exists. The paragraph now names what a warm-up leaves to the file's boots (the data key, NODE_ENV=development, the loaded modules' process-level state), why that is safe for a case that asserts on none of it, and which files therefore take no warm-up. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb
…ng collection Every first use in this file (the lazily imported exceljs, the SQL driver, the template writer in both locales, the workbook reader and writer, both import formats, the three export formats) ran inside whichever case reached it first. One throwaway engine now walks each path at module scope and is destroyed before the first case, so the clocked cases measure behaviour, never loading. No case, assertion, budget or timeout changes. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb
Each case's wall is its own block's waits in child processes (the workflow loop's sleep 2, the stub's boot delay, a backstop), and the cases share nothing: each has its own kernel-assigned port, harness directory and stub state. runBlock now awaits its bash child instead of blocking the worker in execFileSync, with the same answer (exit status, -1 on a signal, stdout on success, stdout then stderr on failure), and both suites run their cases concurrently, so the waits overlap instead of summing. Nothing waited on is shortened; no assertion or timeout changes. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb
…'close' Run concurrently, the harness read a child's death on 'exit' before it had read the child's stdout, so a refused bind was reported as "it exited before announcing a listener" with no output, in place of the EADDRINUSE it wrote. Node does not order 'exit' after the pipes are drained; 'close' does. The liveness flag the probe loop reads still comes from 'exit'. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb
…heir dropdowns exceljs's reader expands each range-keyed dataValidation into one entry per covered cell, and the template covers 50 000 rows per dropdown column: measured on the deal template, 100 000 entries and 39 ms a parse in full, 4 ms with the node skipped, every sheet's values identical. No case in this file asserts on a dropdown (import-template.test.ts pins them, read in full), so its reads skip the node through exceljs's own ignoreNodes option, which the test loader now hands through. The one case that writes a workbook back for the import door still reads it in full. No assertion, budget or timeout changes. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb
…thing a case must not change The safety paragraph claimed none of the shared state changes what a boot does; a module's memo of an environment read does, if a case changes that variable after the warm-up. The paragraph and the no-warm-up list now say so, and the NODE_ENV clause names the moment it holds for (before the kernel is built). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb
📓 Docs Drift Check1 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d550bfc7d6b879bd39222692423a8d57e5a55036 && git checkout d550bfc7d6b879bd39222692423a8d57e5a55036
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cbfa5e43354f430e35c4f9f24732e59848174889 257fe77d8bfdef9d1af2bb7984d4fd49edb3d36f && git checkout -B drift-repro cbfa5e43354f430e35c4f9f24732e59848174889 && git merge --no-ff 257fe77d8bfdef9d1af2bb7984d4fd49edb3d36f
node scripts/docs-audit/affected-docs.mjs --json cbfa5e43354f430e35c4f9f24732e59848174889 |
席位核对 — ACCEPT · PR #24249 at
|
Part of #23181
Clause-②: no
Slice 5 of the test-budget card (claim
6244488338): the warm-up docblock correction carried by the slice 4 release (6221486208), the audit of slice 3's six warmed files, and the card's@objectstack/restandcreate-objectstackrows, one file each. Test files and one test-layer helper only; nothing a package publishes moves, soskip-changesetrides this PR's create.Rows this PR closes
38897658479, CIcreate-objectstack·src/scaffold-e2e-boot-probe.test.ts38281387358)38858901527)@objectstack/rest·src/import-template-route.test.ts (local)38319885190)38877591164)The projection is the head median times the file's own CI ÷ isolated-base ratio. It is an estimate: the next landed refresh's list is the pin, and it decides both rows. For the
restrow the ratio model is conservative in one known direction (part of what the head removes is first-load cost, which a cold, contended CI shard pays at a higher multiple than this box), and it is not a measurement of that direction.How each reading was taken. Clocked = the vitest default-reporter file line (hooks plus tests, collection excluded), the number
report-test-timings.mjscaptures in CI;AI_AGENTandCLAUDECODEunset so vitest picks that reporter. Isolated = onevitest run --maxWorkers=2per file. Base = an untracked copy of the file beside it, blob-identical to the branch pointaf284b05e(a5a23b161980,6b3163931e5a), alternated with the head file round by round, every run throughos-verify-lockon a 4-vCPU box shared with other agents. All copies were moved out before every commit.rest, base / head per round: r3 (head first) 2574 / 1593 · r4 (base first) 2570 / 1585 · r5 (head first) 2570 / 1454 · r6 (base first) 2713 / 1504. r6 is at the final bytes of the file; r3 to r5 ran on bytes that differ from them in one comment only.create-objectstack, base / head per round: ab2 (head first) 15345 / 6357 · ab3 (base first) 17296 / 6284 · ab4 (head first) 15329 / 6311. Ten more head runs at451c13c48(the file's final bytes): 14/14 passed in each, 6296 to 6360 ms.create-objectstack: the cases' own waits, summedMeasured per case (base, isolated): every case's wall is the waits of its own child processes, not this worker's work. The two
os startblocks each cost one workflow-loopsleep 2(about 2.1 s) in the accept and fail-fast cases; the docker block's dying-container case 4.3 s (two loop sleeps on this box); the neighbour harness's "stayed alive" control its 2 s backstop; the other cases 3 to 230 ms. Run one after another, those waits summed to 15.3 s.The waits are the subject (the workflow's own
run:blocks, executed), so nothing waited on is shortened, stubbed or skipped. What changed is that they now overlap:runBlockawaits itsbash -echild (spawn, stdio ignore / pipe / pipe, the same 240 s timeout) instead of blocking the worker inexecFileSync. Its answer is the synchronous one's: the exit status (-1when a signal ended it, the timeout's included), stdout alone on success, stdout then stderr on failure.concurrent: true. The cases share nothing: each has its own kernel-assigned port (held, never probed), harness directory and stub state. The 6.3 s is consistent with vitest's defaultmaxConcurrency(5) starting the 4.3 s docker case after the first wave of about 2 s cases.One harness defect this exposed, fixed in the same file. At
758aa979e(concurrency, no other change) the run failed 2 of 14: the occupier control and the errno control read "it exited before announcing a listener … (nothing: the child wrote neither stdout nor stderr)" where the child had writtenEADDRINUSE. The neighbour harness diagnosed a death on the child'sexitevent, and Node does not orderexitafter this side has read the child's pipes, and with the cases running together the order flipped. The death is now diagnosed onclose(both pipes drained); the liveness flag the probe loop reads still comes fromexit. The same ordering is possible in the sequential file, only rarer.@objectstack/rest: first loads, and a reader materializing what no case readsTwo mechanisms, measured before either change:
exceljslazily inxlsx-module.ts, and the SQL driver, the template writer in both locales, the import door and the export writers each run cold once).warmUpFirstLoads()walks each path once at module scope on a throwaway engine and destroys it before the first case; every case still boots an engine of its own, and the warm-up shares no engine, row or spy with any case. Alone, it read 2090 and 2466 ms in 2 rounds whose base copies read 2837 and 2691 ms.TEMPLATE_VALIDATED_ROWS(50 000) rows per dropdown column. A probe on thedealtemplate (10 parses each, after warming): 100 000 entries, 39.4 ms a parse in full, 4.4 ms with the node skipped through exceljs's ownignoreNodesread option, every sheet's values identical; on a one-column template 22.6 ms against 3.6 ms. No case in this file asserts on a dropdown, andimport-template.test.tspins the dropdowns, read in full. So this file's reads go throughreadWorkbook(the node skipped); the test loaderxlsx-test-loader.tsgains an optionaloptionsargument handed straight toXlsx.load, and its other six callers are unchanged. The one case that writes a workbook back for the import door ("is not starred …") still reads it in full, so the file it imports carries its dropdowns.The parity case's budget (#21481,
PARITY_MS_PER_SHAPE) is untouched; its docblock gains one sentence that the parse no longer reads the dropdowns.The warm-up docblock (
packages/verify/src/boot-warm-up.testkit.ts)"⛔ It shares nothing a case asserts on" is replaced by what a warm-up DOES leave to the file's own boots, why that is safe for a case that asserts on none of it, and which files therefore take no warm-up. What it leaves, from source and from a probe (an untracked file snapshotting the process around one
warmUpBoot()):harnessCryptoProvider()inharness.tsbuilds the process's crypto provider on the first boot and keeps it (slice 4's ablation K is the measured consequence forharness.key-custody);NODE_ENV:bootStacksetsdevelopmentand never restores it; the probe's whole environment diff isNODE_ENV: test -> development;globalThisaddition,Symbol(better-auth:global)), and each module's memos and once-per-process notices (from source: an environment variable memoized on first read inmetadata-protocol, warn-once flags inruntimeandmetadata-protocol).LIVE_CONFIGURATIONS/LIVE_APP_PLUGINS, released bystop()) andbootStackOnce's map (a warm-up never calls it). Process listeners: unchanged.--maxWorkers=1, read two pids, and aglobalThissymbol one set was absent in the other.The six warmed files, read case by case for harness process state
action-flow-elevated-door.test.tsdefineStack/ObjectSchema.create, which read noNODE_ENV. No secret field, no provider, no notice is read.automation-trigger-elevated-door.test.tsdelete-behavior.test.tsderiveDeleteBehaviorCasescases run before the file's first boot, so underdevelopmentinstead oftestafter the warm-up; the derivation reads the spec'sFieldSchemaand the package's collection reader, neither of which readsNODE_ENV. The proof cases read their own stacks' reports.handle.test.tsbootStackrequests and restores per boot (the warm-up bootssingleand sets nothing). ThebootStackOnceidentity case keys onhandleFixtureStackandBOOT_OPTIONS; the warm-up boots its own configuration throughbootStack, so that map holds nothing of it.harness.one-composition.test.tsstop()(slice 4's ablation O failed the same two cases with and without the warm-up).harness.served-composition.test.tssys_emailrows of each case's own stack; the SMTP case deletes and restores its environment variable around its own boot.No edit to any of the six was needed.
Proof the changed files still fail when their subject breaks
Each mutation went through
scripts/ablation-replace.mjsin wrap mode (anchor hit x1 → x0, blob moved), inside a script with anEXIT INT TERMtrap; predictions were written before the runs; each restore was proven blob == HEAD andgit diff HEADempty. Nodist/sits between these mutations and the runs: both suites read the mutated file fromsrc.import-template.ts, the header loses its required*(header: required ? … : label,→header: label,). Predicted 5 red, the same five on head and base. Observed head 5 failed / 32 passed, base copy 5 failed / 32 passed, the same five cases ("keeps exactly the writable declared fields", "?fields= is honoured", "create on the object and no allowExport", "is not starred …", the parity case). The lean reader sees exactly what the full one sees.runBlockanswers status 0 for every block. Predicted 5 red. Observed 5 failed / 9 passed: both "refuses a neighbour", both "fails fast", and the docker "stops polling".Unchanged
it/describetitle line hashes the same ataf284b05eand the head (rest38 lines,1765d3f3f8608dd2;create-objectstack15 lines,cb0a2987aa6e987aonce the added options argument on the two suites is taken out), andexpect(counts are equal (104 = 104, 39 = 39).Tests and gates
dafeb41ad(this branch merged withorigin/maincbfa5e433, after which only257fe77d8, a comment inboot-warm-up.testkit.ts, landed):create-objectstack18 files / 278 tests passed andtsc --noEmitexit 0;@objectstack/rest(--project local) 294 files / 6030 passed, 326 skipped, andtypecheckexit 0 (check:test-typecheckOK, 0 errors);@objectstack/verify41 files / 362 tests passed andtypecheckexit 0. Each changed file is in its package's type-check program (--listFilesOnly: 1 hit each).257fe77d8:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 51 commands for the 4 paths (merge basecbfa5e433); all 51 exited 0 (check:dual-build-cjs-loadsafter a fullturbo run build, which it refuses to measure without);--ran: "51 derived, 51 run, 0 NOT-MEASURED, 0 UNRUN."pnpm exec eslint --no-inline-configover the 4 changed files: exit 0. Control-byte scan over them: no hit. The repo-widepnpm lint,test:repo(norestrepo-project test imports the loader) and every family CI runs are read from this PR's own check runs.Changeset
skip-changeset:@objectstack/rest,@objectstack/verifyandcreate-objectstackeach publishdist,README.mdandCHANGELOG.md. After building all three, the strings this change introduces (readWorkbook,warmUpFirstLoads,CASES_RUN_TOGETHER,ignoreNodes,loadXlsxWorkbook,xlsx-test-loader,warmUpBoot,boot-warm-up,What it shares) hit none of those paths (grep exit 1); the controlscreateRestApiPlugin,bootStackOnceanddetectPackageManagerhit each package'sdist.Acceptance notes
packages/rest/src/xlsx-test-loader.ts, the package's test-layer exceljs loader (not in any tsup entry), gains the optional read-options argument. Named for the seat to declare.parseXlsxToRows) pays the same dropdown expansion on every uploaded template, about 35 ms a parse on this box for thedealtemplate's two dropdown columns. A cost, not a defect class; no carrier.stop()oneTCPServerWrapresource remains (still one after a second warm-up, and nonet.Serveramong the active handles), consistent with the last boot's listener still closing rather than a per-boot leak. Not characterized further.Generated by Claude Code