test(scripts): read spawned children's verdicts as numbers, not prose - #8107
Conversation
objectui#7897. A pin test that spawns a child and regexes the child's human-readable output fails in two silent ways: SGR sequences land inside the matched text under GitHub Actions (green locally, red only in CI), and a count spelled `\d+` is satisfied by `0` (green everywhere, forever, including for the outcome it exists to refuse). The census over `scripts/__tests__/` is in the PR body. This converts the subset it found unsafe or non-discriminating: - `helpers/child-verdict.ts` — one reader: `stripAnsi`, `verdictCount`, `selfTestCases`. Pinned by `child-verdict.test.ts`, including the non-equivalence of the old and new spellings. - eight self-test pins — `\d+ cases pass` also asserted as a count > 0. - `upstream-port-parity-wiring` — `ported file(s) match` reconciled against the pin shipped in the same commit, so an empty pin can no longer read as parity. - `check-control-bytes` — GNU grep 3.11 writes `binary file matches` on STDERR and exits 0 with an empty stdout, so the old negative assertion read a stream that message never reaches and could not fail; both streams are read now, and a NUL-bearing fixture is the control that the negative half can go red. - `shadcn-sync-fetch-cache` — the child colours unconditionally; the list is reconciled against the manifest the printer reads, after ANSI stripping. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
|
Standing down on
Every other check on Generated by Claude Code |
|
Armed — squash auto-merge enabled and the PR is in the merge queue,
On MERGED: content probe on re-fetched Generated by Claude Code |
|
LANDED — merged at 2026-09-06T16:57:56Z as Content probe on re-fetched
#7897 closed by the Generated by Claude Code |
Fixes #7897
Base
01c27c431. Head at the time every reading below was taken:db58b216b.The card asked for a sweep in four steps: census, convert the unsafe subset to a machine-readable verdict, drop
GITHUB_ACTIONSfrom children that are expected to fail, and — the step triage called the core of the card — check that each assertion actually discriminates the outcomes it names. All four are below, including one falsified premise and one step whose target set turned out to be empty.STEP 1 — the census
Population:
scripts/__tests__/*.test.tson01c27c431— 112 test files, 29 of which spawn a child (execFileSync/spawnSync/execSync/spawn). Triage's upper bound was 27 of 110; the dispatching seat read 29. 29 is confirmed.vitest" is falsified, and the way it is false matters: all 29 files mentionvitest, because every test file in this repo importsdescribe/it/expectfrom it. Exactly one file spawns the vitest binary —check-vi-mock-inherit.test.ts, the worked instance PR #7889 already converted, and one of the four names excluded here. Measured withgrep -rn "node_modules/.bin" scripts/__tests__/.Split criterion, from the card: asserting on a repo gate's own verdict line is safe because gates do not colour; asserting on a third-party tool's presentation is unsafe. ⭐ That premise was measured rather than assumed —
grep -rlE 'chalk|picocolors|kleur|ansi-colors|\\u001b\[|\\x1b\[|\\033\['overscripts/returns three files, and one of them,scripts/shadcn-sync.js, is spawned by a test in this census. So "repo script ⇒ safe" is not true in this tree; "repo gate ⇒ safe" is (nocheck-*.mjscolours).GITHUB_ACTIONSbash32-floor-wiringnode check-bash32-floor.mjs --self-test;git ls-files\d+ cases passaccepts0catalog-index-regenerable-4633python3 scripts/regenerate-catalog-index.py(repo generator)check-changeset-overwritenodegate;git(fixture setup)check-changeset-presencenodegate;git(fixture setup)check-control-bytesgit(setup); GNU grepcheck-doc-component-typesnodegate;git ls-filescheck-doc-expression-carriagenodegate (+--self-test)check-doc-fence-languagesnodegate--self-test\d+accepts0check-doc-linksgit ls-filescheck-doc-snippet-typesbashon a workflowrun:step, executables shimmedGITHUB_OUTPUTfilecheck-governed-queue-guardnodegate--self-test\d+accepts0check-half-statesnode scripts/pm/check-half-states.mjs --self-test\d+accepts0check-i18n-en-driftnodegate;git(setup)check-pre-install-import-graphnodegate--self-testtoContain('self-test:')+/^✓/check-shell-escape-residuenodegatecheck-skills-pathsnodegate: OK+ exit 0, floors pinned in-processcheck-vi-mock-inheritnodegate,git grepstripAnsicheck-vi-mock-specifiersnodegate;git initensure-chromium-readybash scripts/ensure-chromium-ready.sh(repo script, no colour)entry-guard-wiringnodegate--self-test\d+accepts0js-comment-mask-jsx-6891node scripts/js-comment-mask.mjs --self-test\d+accepts0one-authority-per-exported-name-6273git ls-files -zpackage-files-existgit ls-files -z,git check-ignoreshadcn-sync-fetch-cachenode scripts/shadcn-sync.js --list— colours unconditionallyside-effects-declaration-consistencygit ls-files -zsite-next-agent-files-4160git check-ignore,git ls-files --error-unmatchturbo-build-outputs-cover-published-entriesgit ls-filesunconsumed-widget-option-claim-6186git ls-files -zupstream-port-parity-wiringnodegate--self-testand plain\d+ cases pass, andported file(s) matchwhich0 ported file(s) matchsatisfiesTotals: 29 spawning files · 3 assert on a third-party tool's presentation (rows 5, 17, 24) · 26 assert on a repo gate's verdict or on machine-readable git data · 7 carry no presentation assertion at all (rows 9, 22, 23, 25, 26, 27, 28) · 10 files converted, carrying 13 assertion sites · 4 excluded by name.
check-i18n-call-site-keys.test.ts(PR #8101), is not in this census at all: it spawns no child process. Nothing was owed on it and nothing is left behind there.The two defects the census found, in full
check-control-bytes— a negative assertion that could not fail. The pair was:Measured on GNU grep 3.11 — the build this container carries and the one the file's own header already cites — a declined binary file produces:
execFileSyncreturns stdout only. The message the negative assertion hunts is written on stderr, so that assertion was matching a stream it can never appear on: green for every file on earth, including the one it exists to refuse. This is precisely the card's near-miss shape — an assertion silently weaker than it reads, which no CI run catches, because it passes. The whole pin was carried by the positive half beside it (a declined file yields empty stdout, sotoMatch(needle)did fail). Both halves are real now:spawnSync, both streams read, both of grep's historical spellings of the refusal recognised, and a new case hands grep a NUL-bearing fixture as the control that the negative half can go red.shadcn-sync-fetch-cache— a colouring child, survived by luck.scripts/shadcn-sync.jswrites SGR unconditionally: no tty check, noNO_COLOR.Component ListandCustom ObjectUI Components:happened to be wrapped whole, so a substring match on raw stdout still hit — but every per-component line is• ${name.padEnd(20)} ESC[2m${description}ESC[0m, an escape sitting between the two things a reader would want to assert across. That is the same byte layout that broke PR #7889 in CI. The two old substring checks also passed on a run that printed the headers and zero components, which is the silent no-op the case says it exists to refuse. Now: ANSI stripped, then every entry ofpackages/components/shadcn-components.jsonreconciled against the printer's own format. ⛔ No machine-readable channel was preferable here —--listhas no JSON mode and this card's surface is test files only, so no gate script was touched; stripping is the belt and the manifest is the machine-readable half that was available.STEP 2 — the conversions
scripts/__tests__/helpers/child-verdict.ts, one shared reader (earned: 9 call sites, well past the three-conversion bar), with its own unit case inscripts/__tests__/child-verdict.test.ts:stripAnsi— SGR built from the escape's code point, never a raw control byte.verdictCount(output, pattern, what)— strips, then captures one number; throws naming the whole output when the pattern misses, so an absent verdict can never be read back as a real zero.selfTestCases(output, gate)— the<gate> self-test: N cases passline every gate here ends with, prefix-agnostic (✓,OK, or none).Every converted call site keeps the original prose match (now applied to the stripped output — the second belt) and adds the count assertion, so no assertion is weaker than it was.
upstream-port-parity-wiringgoes further: theported file(s) matchcount is reconciled againstscripts/upstream-port-pin.jsonas shipped in the same commit, so an emptied pin can no longer read as a tree at parity.⭐ The helper's unit case pins the non-equivalence of the old and new spellings in both directions —
0 cases passsatisfies/\d+ cases pass/and is refused by the count; coloured bytes fail the raw regex and are read correctly after stripping. Without that, the next reader is free to conclude the two spellings are interchangeable, and they are not: that is the whole content of the module.STEP 3 —
GITHUB_ACTIONSin child envs: measured, and the target set is emptyThe leg is real. Reproduced independently of any repo file, on a throwaway fixture with one deliberately failing test and vitest resolved by symlink:
Same 1-vs-0 the card measured on PR #7889. But there is nothing left to change. Only vitest children emit those annotations, and the census finds exactly one test that spawns vitest —
check-vi-mock-inherit.test.ts, which already deletesGITHUB_ACTIONSfrom the child env (PR #7889) and is excluded here. The three repo scripts that print::errorthemselves (check-merge-queue-head.mjs,dependabot-merge-gate.mjs,shadcn-check-report.mjs) are imported by four tests in this census but spawned by none. So step 3 is reported as measured-and-satisfied rather than applied.STEP 4 — discrimination
Ten of the thirteen converted assertion sites were non-discriminating; they are listed per-row in the census table and detailed above. The pattern in eight of them is one shape: a count written
\d+, which accepts0— a self-test whose case table went empty prints0 cases passand exits 0, and the pin reads green. Every one of them now asserts the count, not the shape.Ablation — one converted file, both spellings on the same run
On
shadcn-sync-fetch-cache.test.ts, with the conversion committed first, the pre-conversion spelling (the printer-format match against the child's raw stdout) was re-introduced on disk beside the converted one and both were run together withFORCE_COLOR=1in the parent env. Mutation proved on disk by blob hash and marker count before restoring under atrapwith absolute paths:FORCE_COLOR=1is not what makes the old spelling fail — it fails on every machine. That makes the ablation deterministic rather than environment-dependent, and it also means this particular file was one manifest description away from failing locally, not only in CI.Gates — all at
db58b216b, exit codes captured by redirect-then-capturepnpm exec vitest run scripts/__tests__/Test Files 113 passed (113)·Tests 3356 passed (3356)· 214.00s. No flake, so no second run was owed.Test Files 11 passed (11)·Tests 219 passed (219)pnpm type-check:scriptstsc -p tsconfig.scripts.json, no diagnosticspnpm lint:root✖ 32 problems (0 errors, 32 warnings)— none of the 32 is in a changed file (grepped by name)pnpm check:doc-example-readersOK 80 documented symbol(s), 3947 call site(s), 11 slot(s)…pnpm check:control-bytes✅ check-control-bytes: OK (scanned 6487 tracked text file(s); skipped 85 binary).grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'— emptynode scripts/check-changeset-presence.mjs12 file(s) changed, 0 of them published source…·✅ no changeset is owednode scripts/check-governed-queue-guard.mjs --test <12 paths>✅ NOT GOVERNED — 12 path(s) checked against 5 governed surface(s); none matched.Re-derived beyond the dispatched list, because the diff adds a
.tsfile underscripts/and a new test file:check:unreferenced-sources0,check:entry-guard0 (70 scripts/ file(s)),check:vi-mock-specifiers0,check:vi-mock-inherit0,check-lint-coverage.mjs0 (46/46 packages linted). All green.Live E2E (informational)is red on every branch today for an upstream reason (#7990, objectstack#16186) — not this branch's.Scope
Test files and one test-only helper. ⛔ No gate script under
scripts/*.mjs, ⛔ nopackages/**source, ⛔ novitest.config.*, ⛔ no timeout raised anywhere. Changeset: none owed, per the presence script's own verdict above. Related and named without closing anything: PR #7889 (the worked instance this copies), #8062 and #8074 (the TIMEOUT class, a different class, left alone here).🤖 Generated with Claude Code
https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
Generated by Claude Code