cross-runtime: run node:test files under bun test, matching the deno treatment - #820
Conversation
…test treatment bun's node:test registers tests only inside its runner, so a node:test file run as a plain script errors where node runs the same file standalone; deno already got the equivalent switch to deno test. Also set BUN_TEST_DRAIN_EVENT_LOOP=1 so bun drains the event loop before exit the way node does. Remeasured bun 1.4.0 over the corpus: 161 verdicts flip to pass, 20 flip to fail (those had exited 0 before their 'exit'-handler assertions ran). Regenerates the README tables and updates the homepage and blog compat figures from the merged results.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Important
The re-measurement itself checks out exactly — 161 bun verdicts to pass, 20 to fail, no non-bun verdict touched — but two derived claims are now false: a retry-flip figure in the README the regeneration overwrote, and a tracked wiki doc still publishing bun's pre-PR percentages.
Reviewed changes — the bun test runner switch in the cross-runtime harness and every figure downstream of it.
bun testrouting fornode:testfiles —buildPlainCommand's bun branch now returnsbun test --timeout=<ms> <absolute path>whenusesNodeTest(source)holds and a four-condition regex heuristic does not classify the file as anode:testrun()driver.BUN_TEST_DRAIN_EVENT_LOOP=1— added toenvfor every bun invocation, not only thebun testbranch.results.jsonregenerated for bun only — verified independently: 161 flips to pass, 20 to fail, zero changes fornode/nub/deno/node25, file set identical at 5,664.- Published figures moved — README tables regenerated (
readme-table.mjs --checkexits 0), homepageCOMPATand the blog sentence go 68.1% → 70.7%, both reconciling withscores.denoExclusions.bun(3,570 / 5,046 = 70.749%).
I also chased the obvious hazard — that usesNodeTest is a bare substring check, so a file merely mentioning node:test gets routed to bun test and could exit 0 without registering anything — and it does not hold up. bun test fails only on zero test files, not zero tests (oven-sh/bun#23424, shipped in 1.3.1), and the committed record shows four bun failures with Ran 0 tests tails, so a zero-registration file still fails when its body throws. Same verdict it would have earned as a plain script. Not a finding.
⚠️ A third hand-copied site still publishes bun's pre-PR figures
wiki/research/node-test-suite-leverage.md:161 cites bun 68.1%, bun 69.8% and bun 63.8% — exactly the denoExclusions / bunUniverse / fullCorpus values this PR supersedes with 70.75 / 72.68 / 66.33. wiki/ is tracked and world-readable, so the repo now publishes two different bun compatibility numbers, and lat check cannot see the discrepancy because nothing about it is a link.
Technical details
# Stale bun compat figures in the wiki research doc
## Affected sites
- `wiki/research/node-test-suite-leverage.md:161` — publishes `bun 68.1%` / `69.8%` / `63.8%`, the pre-PR `denoExclusions` / `bunUniverse` / `fullCorpus` values. Current `results.json` supports 70.75 / 72.68 / 66.33.
- `tests/cross-runtime/README.md:120` — enumerates the hand-copy sites as the homepage `COMPAT` array and the blog sentence only. This wiki doc is a third one, which is why it drifted.
## Required outcome
- The wiki line agrees with the committed `results.json`, or stops quoting per-runtime percentages and points at the README table instead.
- The README's hand-copy enumeration names every site that carries a copied figure, so the next re-measure has a complete checklist.
## Open questions for the human
- The `nub` and `deno` figures on that same line (`98.1` / `97.9` / `97.1`, `74.2` / `71.8` / `68.1`) do not match the current table either and appear to predate this PR. Worth refreshing the whole sentence in one pass rather than only bun's three numbers.ℹ️ Nitpicks
run.mjs:419setsBUN_TEST_DRAIN_EVENT_LOOP=1on the plainbun <file>path as well as thebun testpath. If it is a no-op outside the test runner, scoping it to the branch that needs it would make the diff say so; if it is not a no-op, the plain path took a second undocumented behavior change in the same pass.- The README attributes the 20 regressions to files "exiting 0 before their
'exit'-handler assertions ran", but the runner switch and the drain variable landed in one measurement, so that split is an inference rather than something the run separates.
Claude Opus | 𝕏
…t path Review findings: the bun test invocation dropped nodeFlagArgs, unlike the plain path and deno's test path, and the README retry sentence contradicted the committed meta.retried. bun test skips unrecognized long flags (probed with a passing control plus a bogus flag), so the tokens ride along safely. Re-ran the 66 flagged node:test files: 3 verdicts flip to pass, none flip to fail. Tables and site figures regenerated from the merged results.
|
Both fixed in 61a3c87: retry sentence now matches meta.retried; bun test path carries the same Flags tokens as the plain path. Re-ran the 66 flagged node:test files: 3 flips to pass, 0 to fail. |
There was a problem hiding this comment.
Important
Both prior findings are fixed and the 164/20 flip claim is exact — but the results.json this commit ships was measured against a different corpus checkout than the one the other four runtimes' verdicts came from, so the published 70.8% is not an apples-to-apples number.
Reviewed changes — the delta since 4358a7a, plus every figure downstream of it.
// Flags:tokens now ride thebun testpath (run.mjs:432) —...nodeFlagArgs(tokens)is spread before the absolute path argument. Effect measured independently: 3 files flipped to pass, 0 to fail (test-repl-envvars.js,test-runner-mock-timers.js,test-trace-events-get-category-enabled-buffer.js). Gains only.- Retry-flip figure corrected (
README.md:26) — now reads1 bun, matchingmeta.retried.bun.flippedToPass. - Figures regenerated —
readme-table.mjs --checkexits 0; homepageCOMPATand the blog sentence move 70.7 → 70.8, reconciling withscores.denoExclusions.bun(3,573 / 5,046 = 70.81%). 164 files flipped to pass and 20 flipped to fail— verified exact againstmain. Nonode/nub/deno/node25record changed, across all 5,664 files.
I also chased the hazard that a // Flags: token could act as a bun test positional filter and silently narrow the run — it does not hold up. Positionals are file-path substring filters and the explicit absolute target is always included, so a stray token widens rather than narrows; a zero-match filter has exited non-zero since 1.2.8 (oven-sh/bun#20814); and the commit's own toFail=0 confirms it empirically. Not a finding.
⚠️ results.json mixes two corpus revisions, so bun's number is not comparable to the others
The committed meta changed corpus underneath the measurement:
main |
this PR | |
|---|---|---|
corpusNodeVersion |
26.7.0 |
27.0.0 |
corpusCommit |
b4f23d3619… |
cebe4248af… |
eligibleFiles |
5664 | 5490 |
denominator / merged keys |
5664 | 5664 |
cebe4248aff54f6420fd24a8bb7884e5a452e955 is not a v27.0.0 release — it is an ordinary test_runner commit on Node's main carrying NODE_VERSION_IS_RELEASE=0, 573 commits past v26.7.0. CORPUS_NODE_VERSION reads only MAJOR/MINOR/PATCH out of src/node_version.h and never checks the release flag, which is why a nightly stamps as a clean 27.0.0. b4f23d36 on main is the genuine v26.7.0 tag.
eligible.length is a raw directory census taken before any --only / --dirs / --files filtering, so 5490 ≠ 5664 can only mean a different tree was walked — the CLI-filter explanation is ruled out. Since the merge keeps 5664 keys, at least 174 bun verdicts could not have been re-measured and are pre-accommodation carry-overs.
That is directly observable in the committed file: 10 records still carry the bun tail Cannot use describe/test outside of the test runner. Run "bun test" to run tests. — the exact error the accommodation exists to eliminate — and all 10 are byte-identical to main's record. I ruled out the benign explanation (that they are legitimately isRunDriver-classified and still take the plain path) for parallel/test-zlib-reject-garbage-after-end.js by reading it at v26.7.0: it registers with test(...) at column zero and never calls run(), so registersAtColumnZero is true, isRunDriver is false, and it would have taken the bun test path had it been re-run.
Technical details
# `results.json` merges nightly-corpus bun verdicts into a v26.7.0-corpus results file
## Affected sites
- `tests/cross-runtime/results.json` — `meta.corpusCommit cebe4248af…` / `corpusNodeVersion 27.0.0`
/ `eligibleFiles 5490`, merged over 5,664 keys whose non-bun verdicts were measured on
`b4f23d3619…` (v26.7.0).
- `tests/cross-runtime/run.mjs` — the merge branch overwrites `corpusNodeVersion`, `corpusCommit`
and `eligibleFiles` unconditionally while inheriting `parallelism` / `retryFailures`, so the
header describes the last run and the verdicts describe several.
- `tests/cross-runtime/run.mjs` — the file's own stated invariant is "ONE fixed test list across
all four runtimes".
- `tests/cross-runtime/README.md:3` — "every runtime runs the same files, with the same flags".
- `tests/cross-runtime/README.md:7` — pins the corpus to "a full checkout of Node v26.7.0 (tag
commit `b4f23d3619c98bed09af93a21192f6080197a8c6`)", which the committed `meta` now contradicts.
- `site/src/app/(home)/page.tsx` and `site/content/blog/introducing-nub.mdx` — publish the resulting
70.8% under the claim "On Node 26.7's own test suite".
## Required outcome
- bun is re-measured against the same `tests/node-suite` checkout the other runtimes were measured
on (v26.7.0 / `b4f23d3619…`), so `meta.eligibleFiles` returns to 5664 and no verdict in the file
predates the accommodation.
- The 10 records still carrying `Cannot use describe/test outside of the test runner` are gone, or
each is shown to be a genuine `isRunDriver` classification.
## Suggested approach
- `CORPUS_NODE_VERSION` could additionally read `NODE_VERSION_IS_RELEASE` and stamp something like
`27.0.0-nightly` when it is 0, so a nightly checkout is visible in the committed header instead of
reading as a shipped release.
- A merge run could refuse (or loudly warn) when the incoming `corpusCommit` differs from the prior
file's, since that is the condition under which a composite results file stops being one
measurement.
## Open questions for the human
- Was the submodule intentionally moved to Node `main`? If so the whole matrix wants re-running on
it, not just bun — and README:7, the homepage and the blog all need the new corpus version.Mode IncrementalReview · Files reviewed 5 · Commits reviewed 2 (61a3c87a, 4358a7ad) · Base main · Head bun-test-accommodation (61a3c87a) · Prior review: #820 (review)
Claude Opus | 𝕏
…inst corpus drift The earlier re-measures ran against a drifted tests/node-suite checkout (a Node nightly), so 174 names missing from that tree kept pre-accommodation verdicts and the re-run's sources did not match the other runtimes'. The submodule is restored to the recorded v26.7.0 commit and bun re-measured in one full pass over it: 99 verdicts flip to pass, none to fail, and no other runtime's record changes. run.mjs now stamps a -nightly suffix when NODE_VERSION_IS_RELEASE is 0 and refuses a --merge whose corpus commit differs from the prior file's. Tables and site figures regenerated.
|
Confirmed: the submodule drift was unintentional (left over from earlier corpus work). Fixed in 1b5017b — corpus restored to v26.7.0 b4f23d3619, bun re-measured in one full pass over it: eligibleFiles back to 5664, 0 stale describe-tails, 99 flips to pass, 0 to fail, no other runtime's record changed. run.mjs now stamps -nightly on a non-release checkout and refuses a --merge across corpus commits. |
There was a problem hiding this comment.
Important
The corpus-drift finding is fully fixed and the re-measurement is clean — but the regeneration moved meta.retried.bun back to 4, so the README sentence the last commit corrected to 1 bun is wrong again.
Reviewed changes — the delta since 61a3c87, which re-measures bun on the pinned corpus and adds two drift guards.
results.jsonre-measured on the v26.7.0 tree —metais back tocorpusNodeVersion 26.7.0/corpusCommit b4f23d3619…/eligibleFiles 5664, matching README:7's pin.- Nightly corpora are now visible in the header (
run.mjs:75-79) —CORPUS_NODE_VERSIONappends-nightlywhensrc/node_version.hcarriesNODE_VERSION_IS_RELEASE 0. --mergerefuses a corpus mismatch (run.mjs:693-705) — throws whenprior.meta.corpusCommitdiffers from the corpus checkout's HEAD.- Figures moved 70.8 → 70.1 — README tables regenerated, homepage
COMPATand the blog sentence updated; a dated caveat was added to README:126's wording-vs-behavior paragraph.
I verified the fix independently rather than taking the commit message for it. The bun delta against main is 99 to pass, 0 to fail, with zero changes to node / nub / deno / node25 across all 5,664 files — so README:26's headline is exact, and the earlier 164/20 was indeed inflated by drift. The decisive marker from the last review is gone: records still carrying the bun tail Cannot use describe/test outside of the test runner went 247 on main → 10 at 61a3c87 → 0 here. readme-table.mjs --check exits 0, and scores.denoExclusions.bun (3,535 / 5,046 = 70.06%) reconciles with both published sites.
I also checked the two new guards rather than assuming they work. The nightly regex is not inert: v26.7.0's header reads #define NODE_VERSION_IS_RELEASE 1 and both main and the contaminating cebe4248 read ... 0, single ASCII space in each, and the macro's only other occurrences are #if / #else / #endif lines that cannot false-match. capture() is a hoisted declaration, so the guard's call site is fine, and it mirrors the existing corpusCommit idiom exactly. Not findings.
One thing worth confirming, since it is invisible from the diff: README:91's hand-written per-directory figures still hold. All 99 flips land in es-module (6), parallel (91) and sequential (2), so pseudo-tty/ 12, wpt/ 6 and ffi/ 13 are untouched.
⚠️ A third hand-copied site still publishes bun's pre-PR figures
Carried over from the first review and still unfixed — the target numbers have moved again, so it is worth restating them. wiki/research/node-test-suite-leverage.md:161 cites bun 68.1%, 69.8% and 63.8% for the denoExclusions / bunUniverse / fullCorpus lenses; this PR makes those 70.1 / 71.8 / 65.6. wiki/ is tracked and world-readable, so on merge the repo publishes two different bun compatibility numbers, and lat check cannot see it because nothing about it is a link.
Technical details
# Stale bun compat figures in the wiki research doc
## Affected sites
- `wiki/research/node-test-suite-leverage.md:161` — publishes `bun 68.1%` / `69.8%` / `63.8%`.
Current `results.json` supports 70.06 / 71.79 / 65.58.
- `tests/cross-runtime/README.md:120` — enumerates the hand-copy sites as the homepage `COMPAT`
array and the blog sentence only. This wiki doc is a third one, which is why it drifted twice.
## Required outcome
- The wiki line agrees with the committed `results.json`, or stops quoting per-runtime percentages
and points at the README table instead.
- The README's hand-copy enumeration names every site carrying a copied figure, so the next
re-measure has a complete checklist.
## Open questions for the human
- The `nub` and `deno` figures on that same line (`98.1` / `97.9` / `97.1`, `74.2` / `71.8` /
`68.1`) do not match the current table either and predate this PR — current values are
98.45 / 98.16 / 97.40 and 74.16 / 71.75 / 68.07. Worth refreshing the whole sentence in one pass.ℹ️ The PR description still carries the pre-re-measurement numbers
The body says "161 verdicts flip to pass, 20 flip to fail (previously exited 0 before 'exit'-handler assertions ran)". After 1b5017b the clean figures are 99 to pass and none to fail, so the parenthetical describes regressions that no longer exist. This matters beyond cosmetics: the PR body becomes the squash-merge commit message, and this repo requires commit messages to be factual and neutral.
ℹ️ Nitpicks
tests/cross-runtime/README.md:45documents--mergeand parenthesises one refusal condition ("the merge refuses a runtime that lacks a verdict for some file"). The new corpus-mismatch refusal belongs in the same parenthetical, so someone hitting the throw can find it documented.
Claude Opus | 𝕏
…esh wiki lens figures The clean bun re-measure's own retry pass flipped 4 verdicts, so the README sentence reads 4 bun again, matching meta.retried. The research doc's headline lens figures move to the regenerated results (bun 70.1 on Deno's lens), with a changelog entry.
|
Both fixed in 50bdd37: retry sentence back to 4 bun, matching the regenerated meta; wiki/research/node-test-suite-leverage.md lens figures refreshed to the new results with a changelog entry. |
There was a problem hiding this comment.
ℹ️ Both outstanding findings are fixed and I verified every figure against the committed data. The only things left are outside the code: the PR description still carries the pre-re-measurement numbers, and the hand-copy surfaces that caused both drifts are still ungated.
Reviewed changes — the delta since 1b5017b, which is two prose lines and a changelog entry.
README.md:26back to4 bun— matchesmeta.retried.bun.flippedToPassin the regeneratedresults.json, which the re-measurement moved from1to4. The other four counts in the sentence (node 1, nub 1, deno 2, node25 0) still matchmetaexactly.wiki/research/node-test-suite-leverage.md:161refreshed — all nine per-runtime figures now agree with the committedresults.json, including the two that were stale before this PR touched them.- A changelog entry dated 2026-08-30 added to the doc's revision list, naming the
bun testswitch and the 68.1 → 70.1 move on Deno's lens.
I checked the numbers rather than trusting the regeneration. Every wiki figure rounds correctly from results.json: denoExclusions nub 98.4542 → 98.5, deno 74.1577 → 74.2, bun 70.0555 → 70.1; bunUniverse 98.1630 → 98.2, 71.7483 → 71.7, 71.7905 → 71.8; fullCorpus 97.4003 → 97.4, 68.0734 → 68.1, 65.5805 → 65.6. Deno's bunUniverse figure moving 71.8 → 71.7 is a rounding correction, not a measurement change — 71.7483 was previously rounded the wrong way. readme-table.mjs --check exits 0, npm run lat:check passes, and the homepage 70.1 / 3,535 / 5,046 plus the blog's 70.1% still reconcile with scores.denoExclusions.bun. results.json is byte-identical to 1b5017b, so the 99-to-pass / 0-to-fail delta I verified last run still stands.
ℹ️ The PR description still carries the pre-re-measurement numbers
The body says "161 verdicts flip to pass, 20 flip to fail (previously exited 0 before 'exit'-handler assertions ran)". The committed measurement is 99 to pass and none to fail, so the parenthetical describes regressions that do not exist. Raised on 1b5017b and still open; it matters because the body becomes the squash-merge commit message, which this repo requires to be factual — and unlike the README, that is not editable after merge.
Technical details
# PR body contradicts the committed measurement
## Affected sites
- PR #820 description — "161 verdicts flip to pass, 20 flip to fail (previously exited 0 before
'exit'-handler assertions ran)". `results.json` at `50bdd37` supports 99 to pass, 0 to fail.
- `tests/cross-runtime/README.md:26` — the in-repo sentence already states the correct 99 / 0.
## Required outcome
- The description's flip counts match `README.md:26`, and the regression parenthetical is dropped
since `toFail` is 0.ℹ️ Four hand-copied figure surfaces, one of them enumerated
README.md:120 still names only the homepage COMPAT array and the blog sentence as the places figures are hand-copied. This PR demonstrates two more: wiki/research/node-test-suite-leverage.md:161 drifted and was fixed twice across review rounds, and README.md:26's own retry-flip sentence went 4 → 1 → 4 across three commits because each regeneration moves meta.retried and nothing checks the prose. readme-table.mjs --check and the pre-push hook validate only the two marker-delimited tables, so every one of these fails silently.
Technical details
# The hand-copy checklist is incomplete and none of it is gated
## Affected sites
- `tests/cross-runtime/README.md:120` — enumerates two hand-copy sites; there are at least four
(homepage `COMPAT`, the blog sentence, `wiki/research/node-test-suite-leverage.md:161`, and
`README.md:26`'s retry-flip counts).
- `tests/cross-runtime/readme-table.mjs` — `--check` compares only the two marker-delimited tables.
## Required outcome
- The enumeration names every surface carrying a copied figure, so the next re-measure has a
complete checklist.
## Suggested approach (optional)
- The retry-flip sentence is the cheapest one to gate rather than document: the five counts are
`meta.retried.<rt>.flippedToPass`, so `--check` could assert them the way it asserts the tables,
and the flip-flop class ends there.Claude Opus | 𝕏
…--check The homepage COMPAT array and the blog's compatibility sentence are retyped from results.json and nothing rechecked them, so both drifted when a re-measurement moved a score. --check now compares each surface's rates and counts against scores.denoExclusions and fails on a mismatch (verified red against a deliberately broken rate).
|
PR body updated to the clean measurement. 10d6f70 gates the hand-copied surfaces: readme-table.mjs --check (run by the pre-push hook) now compares the homepage COMPAT rates and the blog sentence against scores.denoExclusions and fails on a mismatch. |
There was a problem hiding this comment.
ℹ️ No critical issues — the new gate works, and I confirmed it fails for the right reason rather than trusting its exit code. What's left is which surfaces it watches.
Reviewed changes — the delta since 50bdd37: one README sentence and a 47-line addition to readme-table.mjs.
checkHandCopies()gates the two site surfaces —--checknow regex-extracts the homepageCOMPATrates and pass counts plus the blog's three percentages, compares them againstscores.denoExclusions, and folds a mismatch into the existingdrifted→ exit 1 path.README.md:120rewritten — the hand-copy paragraph now states both site surfaces are checked, replacing the admission that they "drift silently until someone copies them across".results.jsonuntouched —git diff --stat 50bdd370..10d6f70fisREADME.md(1 line) andreadme-table.mjs(+47/−1), so every figure verified in the earlier rounds still stands.
I ran the gate rather than reading it. --check exits 0 with README tables and site figures match results.json, and a negative control confirms it is not inert: perturbing the homepage's Bun entry to rate: 68.1, tests: '3,438 / 5,046' and the blog's Deno figure to 74.9% drives it to exit 1 with three exact diagnostics naming each file, runtime, found value and expected; restoring returns it to 0. Two things I checked that are not findings — rate() recomputes pass / nodePass * 100 rather than reading pct, and the two agree (pct is that same quotient at 2dp: bun 70.06 / 70.0555, nub 98.45 / 98.4542), so the check and the README table share one definition; and each validated pattern occurs exactly once in its file, so re.exec's first-match-only behavior is masking no second copy today. A reformat that breaks a pattern also fails safe — pattern not found sets bad and exits 1 rather than passing quietly.
Separately, the PR description now carries the correct 99-to-pass / none-to-fail figures, which closes the last round's outstanding note, and meta.retried still matches README.md:26's five counts (node 1, nub 1, bun 4, deno 2, node25 0).
ℹ️ Nitpicks
readme-table.mjs:118— an unreadable surface fileprocess.exit(2), and.githooks/pre-push:245treats any non-1 exit as infrastructure and warns past it. So relocatingpage.tsxsilently disables the gate, while merely renaming aCOMPATentry correctly blocks (exit 1). These two files are tracked in this repo, so their absence is drift rather than an environment condition like a missingnode.
Claude Opus | 𝕏
| const s = results.scores.denoExclusions; | ||
| const by = Object.fromEntries(s.runtimes.map((r) => [r.runtime, r])); | ||
| const rate = (rt) => (by[rt].pass / s.nodePass * 100).toFixed(1); | ||
| const surfaces = [ |
There was a problem hiding this comment.
The two surfaces listed here are the two that are currently correct. The two that actually drifted during this PR aren't covered: wiki/research/node-test-suite-leverage.md:161 carries the same three denoExclusions rates (plus six more from bunUniverse/fullCorpus) and went stale across two review rounds before 50bdd37 fixed it, and README.md:26's retry-flip counts went 4 → 1 → 4 across three commits. Neither is reachable by the BLOCKS loop, which only covers the marker-delimited tables.
Technical details
# The hand-copy gate covers 2 of 4 surfaces
## Affected sites
- `tests/cross-runtime/readme-table.mjs:97` — `surfaces` lists the homepage `COMPAT` array and the blog sentence only.
- `wiki/research/node-test-suite-leverage.md:161` — publishes `nub 98.5%, deno 74.2%, bun 70.1%` (`denoExclusions`), `98.2 / 71.7 / 71.8` (`bunUniverse`) and `97.4 / 68.1 / 65.6` (`fullCorpus`). Tracked and world-readable. Currently correct, ungated, and this PR had to hand-edit it after the re-measurement.
- `tests/cross-runtime/README.md:26` — `1 node, 1 nub, 4 bun, 2 deno and 0 node25` are `meta.retried.<rt>.flippedToPass`, hand-written prose. The same line's `99 files flipped to pass and none flipped to fail` is likewise hand-derived.
- `tests/cross-runtime/readme-table.mjs:88-92` — the explanatory comment states the same two-surface enumeration, so the code and the prose agree with each other but not with the repo.
## Required outcome
- Every surface carrying a figure derived from `results.json` is either asserted by `--check` or explicitly named as out of scope, so the next re-measurement has a complete checklist.
## Suggested approach (optional)
- `README.md:26`'s five counts are the cheapest to add: they are direct `meta.retried.<rt>.flippedToPass` reads, so one more `surfaces` entry pointing at `README` with a `/flipped (\d+) node, (\d+) nub/`-style pattern ends that flip-flop class.
- The wiki line spans three lenses rather than one, so it may be better served by dropping the per-runtime percentages and pointing at the README table — which is already generated — than by adding nine more assertions.
## Open questions for the human
- Is the wiki doc meant to carry standing figures at all, or is a pointer to the generated table the better long-term shape given it has now drifted twice?| [`results-prior-versions.json`](./results-prior-versions.json) is a historical run on the Node **25.8.1** corpus (bun 1.3.14, deno 2.8.1, 2026-08-20), kept for the before/after comparison recorded in git history; it is not comparable with the table above. | ||
|
|
||
| A regenerated `results.json` is only half the update. The published figures are hand-copied into the `COMPAT` array in `site/src/app/(home)/page.tsx` and into the compatibility sentence in `site/content/blog/introducing-nub.mdx`; nothing reads this file at build time, so both drift silently until someone copies them across. | ||
| A regenerated `results.json` is only half the update. The published figures are hand-copied into the `COMPAT` array in `site/src/app/(home)/page.tsx` and into the compatibility sentence in `site/content/blog/introducing-nub.mdx`; nothing reads this file at build time. `readme-table.mjs --check` (run by the pre-push hook) now compares both surfaces' rates against `scores.denoExclusions` and fails on a mismatch, so the copy step can no longer be skipped silently. |
There was a problem hiding this comment.
.githooks/pre-push:228 decides whether to run --check with git diff --name-only "$base" "$local_sha" -- tests/cross-runtime/, but the new assertions read files under site/ — which that trigger never examines. A push editing only page.tsx or introducing-nub.mdx therefore never runs them. The claim holds for the regenerate-results-first direction that drifted twice; it doesn't hold for a site-only edit.
Technical details
# The new site assertions never run for site-only pushes
## Affected sites
- `tests/cross-runtime/README.md:120` — "`readme-table.mjs --check` (run by the pre-push hook) now compares both surfaces' rates against `scores.denoExclusions` and fails on a mismatch, so the copy step can no longer be skipped silently."
- `.githooks/pre-push:219-232` — `table_touched` is set only when the pushed range touches `tests/cross-runtime/`; the path filter does not include `site/`.
- `tests/cross-runtime/readme-table.mjs:99,107` — the checked files are `site/src/app/(home)/page.tsx` and `site/content/blog/introducing-nub.mdx`.
## Required outcome
- Either the hook also arms the check when the pushed range touches the two site files, or the README sentence states the direction it actually covers (a regenerated `results.json` can no longer ship without the copy) rather than implying the surfaces are watched from both sides.
## Notes
- The separate site gate further down `.githooks/pre-push` runs `next build`, which does not read `results.json`, so it does not close this gap.
- Nothing in CI or `package.json` runs `readme-table.mjs --check` either — the pre-push hook is the only caller, so this is the whole enforcement surface.…retry sentence The two surfaces that actually drifted during review were not the two the gate covered: the wiki research doc's nine lens figures and this README's retry-flip counts. --check now validates both (each verified red against a deliberate mismatch), and the pre-push trigger also fires for pushes that touch only the checked site/wiki files.
|
8954631 extends the gate to the two surfaces that actually drifted: the wiki doc's nine lens figures and the README retry sentence (both red-tested), and the pre-push trigger now fires for pushes touching only the checked site/wiki files. |
There was a problem hiding this comment.
ℹ️ Both outstanding findings are fixed, and I confirmed the two newly-gated surfaces fail for the right reasons rather than trusting the exit code. What's left is one dimension the gate still wildcards, and where the gate runs.
Reviewed changes — the delta since 10d6f70f: the hook trigger, one README sentence, and a rework of checkHandCopies().
- The gate now covers all four hand-copy surfaces —
checkHandCopies()grew aby(lens)helper so a surface can assert againstdenoExclusions,bunUniverseorfullCorpus, and gained the wiki research doc's nine lens figures plusREADME.md:26's five retry-flip counts (meta.retried.<rt>.flippedToPass). - The pre-push trigger widened (
.githooks/pre-push:230) — the pathspec now includessite/src/app/(home)/page.tsx,site/content/blog/introducing-nub.mdxandwiki/research/node-test-suite-leverage.md, so a push touching only a site or wiki surface runs--check. README.md:120restated — names all four gated surfaces and the widened trigger.results.jsonandrun.mjsuntouched —git diff --stat 10d6f70f..8954631is the hook (4),README.md(2) andreadme-table.mjs(+56/−22), so every measurement figure verified in earlier rounds still stands.
I ran the two new assertions rather than reading them. Perturbing wiki:161's bun 70.1 → 68.1 (denoExclusions), deno 71.7 → 71.8 (bunUniverse) and bun 65.6 → 63.8 (fullCorpus), plus README.md:26's 4 bun → 1 bun, drives --check to exit 1 with four exact diagnostics — each naming the right runtime and capture position, including the one that is a rounding correction rather than a measurement change. Restoring returns it to 0. I also checked the hook's new pathspec against real git rather than assuming: git diff --name-only 26d92e3 8954631 -- <the four paths> returns all seven changed files, and the unquoted parentheses in (home) are accepted literally.
One thing worth confirming since it is invisible from the diff: the four-surface enumeration is complete. Grepping every tracked file outside tests/cross-runtime/ for the nine percentages and the 5,046 / 4,968 / 3,535 / 3,742 counts matches only wiki/research/node-test-suite-leverage.md:161, site/content/blog/introducing-nub.mdx:36 and page.tsx:955-958. The wiki changelog's 68.1 → 70.1 at line 178 is dated history, not a drift surface, and no checkHandCopies pattern can match it.
ℹ️ The gate exists only in an opt-in local hook, so "can no longer be skipped silently" still overstates it
Nothing in CI, Makefile, package.json or scripts/ runs readme-table.mjs --check — .githooks/pre-push is the only caller. package.json has no prepare or husky step, so core.hooksPath .githooks is set per clone by hand (AGENTS.md says a fresh clone must do it), and --no-verify or NUB_SKIP_README_TABLE_CHECK=1 bypasses it. A contributor who never ran that config can move any of the four surfaces with the gate never firing. lat-check.yml is the in-repo precedent for the missing job — it gates the same wiki file this PR just added to the trigger.
Technical details
# The hand-copy gate has no CI backstop
## Affected sites
- `tests/cross-runtime/README.md:120` — claims the copy step "can no longer be skipped silently";
true only for a clone that has opted into `core.hooksPath .githooks` and did not pass
`--no-verify`.
- `.githooks/pre-push:219-251` — the sole caller of `readme-table.mjs --check`.
- `.github/workflows/` — no job invokes it; `grep -rn cross-runtime .github/` returns nothing.
## Required outcome
- Either a CI job runs `readme-table.mjs --check` on pull requests that touch the four gated
surfaces, or README:120's claim is scoped to "the pre-push hook, when installed".
## Suggested approach (optional)
- `.github/workflows/lat-check.yml` already gates `wiki/research/node-test-suite-leverage.md` on
PRs to `main` and is the closest existing shape; the check is ~50ms and needs only `node` plus a
checkout, with no submodule (`tests/node-suite` stays `update = none`).
## Open questions for the human
- Is a local-only gate the intended bar here? Every other figure-drift round on this PR was caught
in review rather than by a hook, which is the argument for the CI job — but if the hand-copy
surfaces are considered maintainer-only, softening the sentence is the cheaper answer.ℹ️ Nitpicks
.githooks/pre-push:244-245printstests/cross-runtime/README.md's results table does not match results.json./Regenerate it: node tests/cross-runtime/readme-table.mjs --write. Exit 1 now has four possible causes and--writefixes only the two marker-delimited tables — a hand-copy mismatch survives it and reportsREADME tables rewritten, so the advice sends the developer in a circle. The precise per-surface diagnostics do print immediately above, so this is recoverable; wording it as "regenerate the tables, or copy the figures across" would match what the check now covers.
Claude Opus | 𝕏
The gated lines' 'Bun 1.4' / 'Deno 2.9' labels were wildcarded, so a re-measure on a newer runtime would leave them stale with the gate green. They now compare against major.minor of the measured binary in meta.binaries (red-tested against a perturbed label).
|
c95ec26 asserts the version labels as well: the homepage and blog 'Bun 1.4' / 'Deno 2.9' labels now compare against major.minor of the measured binary in meta.binaries (red-tested). |
There was a problem hiding this comment.
ℹ️ No critical issues — the version-label gate works and I confirmed all four new assertions fail for the right reason. Two of the four sites the prior thread named are still unaddressed, and I found one more derived figure on an already-gated file.
Reviewed changes — the delta since 8954631f, which is an 11-line addition to readme-table.mjs and nothing else.
- Version labels are now asserted, not wildcarded — a new
mm(rt)helper takesmajor.minorfromversion(rt), and fourvalue:-stylewantscompare it against the hand-written'Deno 2.9'/'Bun 1.4'on the homepage and thefor Deno 2.9/for Bun 1.4in the blog sentence. - A
valuebranch in the want loop —if (w.value !== undefined)comparesm[1]andcontinues before the rate/count logic, so a version want reuses the existing not-found andbad→ exit 1 plumbing. results.jsonand every other file untouched —git diff --stat 8954631f..c95ec26bisreadme-table.mjs(+11) alone, so every measurement figure verified in earlier rounds still stands.
I ran the new assertions rather than reading them. Perturbing the homepage's Deno 2.9 → 2.8 and the blog's for Bun 1.4 → 1.5 drives --check to exit 1 with both exact diagnostics (page.tsx: Deno version label says 2.8, measured binary is 2.9); a second control on the other two labels — homepage Bun 1.4 → 1.3 and blog for Deno 2.9 → 2.10 — also exits 1 with both. Restoring returns it to 0. So all four are live, not just the two that mirror an existing pattern. version() falls back to "?" when meta.binaries[rt] is absent, so mm fails safe rather than vacuously passing.
The thread on this change stays open because it named four sites and this commit fixes two. page.tsx:955's name: 'Node 26.7' row and the blog's "On Node 26.7's own test suite" are still unasserted against meta.corpusNodeVersion (26.7.0) — the same mm-style compare would cover both, so whether to gate them or accept the corpus label as stable is the one call left for you.
Claude Opus | 𝕏
…he miss-count prose The homepage's 'Node 26.7' label, its 5,046/5,046 row, the blog's corpus sentence and the homepage's 'Most of Nub's 78 misses' derive from results.json and were unasserted. All four now compare against meta.corpusNodeVersion and scores.denoExclusions (red-tested).
|
07daa4e gates the rest: the Node corpus labels on both surfaces (vs meta.corpusNodeVersion), the homepage's Node row counts, and the 'Most of Nub's 78 misses' prose (vs nodePass − nub.pass). All red-tested. Every figure and label on the four surfaces now traces to results.json. |
There was a problem hiding this comment.
ℹ️ No critical issues — all four new assertions are live and I confirmed each fails for the right reason. What's left is one corpus label the new want doesn't reach, and a scope question about the README's own prose.
Reviewed changes — the delta since c95ec26b, which is an 11-line addition to readme-table.mjs and nothing else.
- Gated the Node corpus labels —
corpusMMtakesmajor.minorfrommeta.corpusNodeVersion(26.7.0→26.7), and two wants compare it against page.tsx'sname: 'Node 26.7'row and the blog's "On Node 26.7's own test suite". - Gated the Node row's counts — a
values:pair asserts both captures oftests: '5,046 / 5,046'againstdenoExclusions.nodePass, withrate: 100pinned literally in the pattern so a moved rate fails as pattern-not-found rather than passing. - Gated the miss count —
Most of Nub’s (\d+) missesagainstnodePass − nub.pass(5,046 − 4,968 = 78). - Generalized the
valuebranch tovalues—w.value !== undefined || w.valuesfolds the multi-capture case into the existing not-found andbad→ exit 1 plumbing. - Nothing else moved —
git diff --stat c95ec26b..07daa4edisreadme-table.mjs(+11/−2) alone, so every measurement figure verified in earlier rounds still stands.
I ran the new assertions rather than reading them. Perturbing page.tsx's name: 'Node 26.7' → 'Node 25.9' and its 5,046 / 5,046 → 5,040 / 5,046, the miss count 78 → 79, and the blog's On Node 26.7's → 25.9's drives --check to exit 1 with four exact diagnostics naming each surface, label and expected value; restoring returns it to 0. So all four are live, including the values: pair that exercises the new branch. corpusMM falls back to "?" when meta.corpusNodeVersion is absent, so it fails safe rather than vacuously passing.
Both threads from the prior rounds are now addressed, and I've replied and resolved them — this commit closed the last two of the four sites the version-label thread named.
ℹ️ The README's own prose carries four more derived figures, none of them gated
The gate covers the three published surfaces thoroughly now, but tests/cross-runtime/README.md also hand-writes numbers derived from results.json outside its two marker-delimited blocks, and --check reads that file only for the retry-flip sentence. I perturbed :91's bun 12 → bun 99, :93's 48 of the 5,664 → 999 of the 1,111 and :53's 5,078 → 9,078 together; --check still exits 0.
:91 is the one that matters. Its eight per-directory tallies come from scores.perDirectory and move on every runtime re-measure, so they were genuinely exposed by this PR's bun pass — they happened not to move only because all 99 flips landed in es-module/, parallel/ and sequential/. The other three are corpus-derived and stay put while the corpus is pinned.
Whether these belong in the gate is a scope call, not a bug: they are internal benchmark documentation rather than published marketing copy, so "deliberately out of scope" is a defensible answer. Right now the omission reads as accidental rather than chosen.
Technical details
# Derived figures in README prose sit outside the gate
## Affected sites
- `tests/cross-runtime/README.md:91` — per-directory tallies `pseudo-tty/` nub 28 / 31, deno 15,
bun 12; `wpt/` nub 24 / 25, bun 6, deno 0; `ffi/` nub 11 / 13, bun 13, deno 13. All eight match
`scores.perDirectory` exactly (verified). These move on any runtime re-measure.
- `tests/cross-runtime/README.md:93` — `fails 48 of the 5,664 files` = `fullCorpus.files -
fullCorpus.<node>.pass` (5,664 − 5,616) and `meta.eligibleFiles`.
- `tests/cross-runtime/README.md:53-58` — the hand-typed lens table's `Files` column
(5,078 / 4,760 / 5,664 / 4,946 / 4,111 / 718) duplicates `scores.<lens>.files`, which the
generated `<!-- results-table -->` block also carries and which IS checked.
- `tests/cross-runtime/README.md:9` — `718 corpus paths` = `scores.engineSpecificOnly.files`.
- `tests/cross-runtime/readme-table.mjs:140-148` — the README surface's `wants` array, which
asserts the retry-flip sentence only.
## Required outcome
- Either these figures fail `--check` when they disagree with `results.json`, or the README says
in one clause that its prose figures are not gated, so the next re-measure knows to eyeball them.
## Suggested approach (optional)
- `:91` is the only one with real drift exposure and `by(lens)` does not model `perDirectory`, so
it needs a small helper rather than one more `wants` line. The three corpus-derived figures are
cheap `value:` wants if you want them.
## Open questions for the human
- Is the gate scoped to *published* surfaces (site + wiki + the headline sentence), with the
benchmark's internal README exempt? If so this is a one-line doc clarification, not code.Claude Opus | 𝕏
| { re: /name: 'Bun [\d.]+', rate: ([\d.]+), tests: '([\d,]+) \/ ([\d,]+)'/, lens: deno, rt: "bun" }, | ||
| { re: /name: 'Deno ([\d.]+)'/, value: mm("deno"), label: "Deno version label" }, | ||
| { re: /name: 'Bun ([\d.]+)'/, value: mm("bun"), label: "Bun version label" }, | ||
| { re: /name: 'Node ([\d.]+)'/, value: corpusMM, label: "Node corpus label" }, |
There was a problem hiding this comment.
page.tsx carries two corpus labels and this want reaches only one. Line 1007's prose reads Node &rsquo;s own test suite — literally Node 26.7&rsquo;s own test suite, the same corpusMM value the blog's equivalent sentence is now checked against — but the name: ' prefix exists only at line 955. Verified: perturbing line 1007 alone to Node 22.1 leaves --check at exit 0.
Technical details
# The page.tsx prose corpus label is still ungated
## Affected sites
- `site/src/app/(home)/page.tsx:1007` — `Node 26.7’s own test suite under Deno’s
compatibility lens`. Equals `meta.corpusNodeVersion` major.minor, the same quantity `corpusMM`
already computes.
- `tests/cross-runtime/readme-table.mjs:115` — `/name: 'Node ([\d.]+)'/` anchors on the COMPAT
array at page.tsx:955, so it cannot reach line 1007.
- `site/content/blog/introducing-nub.mdx:36` — the analogous prose sentence IS gated by the new
`/On Node (\d+\.\d+)'s own test suite/` want, which is what makes the page.tsx omission stand out.
## Required outcome
- A corpus label on page.tsx that disagrees with `meta.corpusNodeVersion` fails `--check`, wherever
on the page it appears.
## Suggested approach (optional)
- One more want on the surface the gate already reads:
`{ re: /Node ([\d.]+)’s own test suite/, value: corpusMM, label: "Node corpus label (prose)" }`.
Note the label then appears twice in the file, so if these two ever need to differ the patterns
must stay distinct — today both should read `26.7`.| [`results-prior-versions.json`](./results-prior-versions.json) is a historical run on the Node **25.8.1** corpus (bun 1.3.14, deno 2.8.1, 2026-08-20), kept for the before/after comparison recorded in git history; it is not comparable with the table above. | ||
|
|
||
| A regenerated `results.json` is only half the update. The published figures are hand-copied into the `COMPAT` array in `site/src/app/(home)/page.tsx` and into the compatibility sentence in `site/content/blog/introducing-nub.mdx`; nothing reads this file at build time, so both drift silently until someone copies them across. | ||
| A regenerated `results.json` is only half the update. The published figures are hand-copied into the `COMPAT` array in `site/src/app/(home)/page.tsx` and into the compatibility sentence in `site/content/blog/introducing-nub.mdx`; nothing reads this file at build time. `readme-table.mjs --check` (run by the pre-push hook, including for pushes that touch only those files) now compares both surfaces' rates against `scores.denoExclusions` — plus the wiki research doc's lens figures and this README's own retry-flip sentence — and fails on a mismatch, so the copy step can no longer be skipped silently. |
There was a problem hiding this comment.
nit: this sentence predates both label rounds. It describes --check as comparing the two site surfaces' rates plus the wiki lens figures and the retry sentence, but the check now also asserts the Bun/Deno version labels (c95ec26b) and, in this commit, the Node corpus labels, the Node row counts and the miss count. Worth restating so the paragraph stays the complete checklist it is meant to be.
| const name = w.label || w.rt || (w.rts || []).join("/"); | ||
| if (!m) { console.error(`${path.basename(file)}: pattern for ${name} not found (${w.re})`); bad = true; continue; } | ||
| if (w.value !== undefined || w.values) { | ||
| const wants = w.values || [w.value]; |
There was a problem hiding this comment.
nit: wants here shadows the array the enclosing for (const w of wants) is iterating. It is correct — for...of evaluates its iterable in the outer scope before the body block exists — but a reader has to prove that to themselves before moving on.
| const wants = w.values || [w.value]; | |
| const list = w.values || [w.value]; | |
| list.forEach((want, i) => { |
…klist page.tsx carries two corpus labels and only the COMPAT row's was asserted; the prose one is now checked too (red-tested). The README's gate paragraph restated to name everything checked, and the values loop unshadowed.
|
07daa4e's gap closed in the follow-up commit: the homepage prose corpus label is asserted (red-tested), the README gate paragraph names everything checked, and the values loop is unshadowed per the suggestion. |

bun's node:test registers tests only inside its runner, so a node:test file run as a plain script errors where node runs the same file standalone. deno already gets the equivalent switch to deno test, so the harness now grants bun the same accommodation, plus BUN_TEST_DRAIN_EVENT_LOOP=1 for node-parity exit semantics. Re-measured bun 1.4.0 in one full pass over the v26.7.0 corpus: 99 verdicts flip to pass, none to fail; no other runtime's record changes. Adds two drift guards (a -nightly corpus stamp, and --merge refuses across corpus commits), regenerates the README tables, and updates the homepage/blog/wiki compat figures.