Skip to content

cross-runtime: run node:test files under bun test, matching the deno treatment - #820

Merged
colinhacks merged 9 commits into
mainfrom
bun-test-accommodation
Aug 31, 2026
Merged

cross-runtime: run node:test files under bun test, matching the deno treatment#820
colinhacks merged 9 commits into
mainfrom
bun-test-accommodation

Conversation

@colinhacks

@colinhacks colinhacks commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

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.

…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.
Copilot AI lite review requested due to automatic review settings August 31, 2026 00:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vercel

vercel Bot commented Aug 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nub Ready Ready Preview Aug 31, 2026 3:25am

Request Review

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 test routing for node:test filesbuildPlainCommand's bun branch now returns bun test --timeout=<ms> <absolute path> when usesNodeTest(source) holds and a four-condition regex heuristic does not classify the file as a node:test run() driver.
  • BUN_TEST_DRAIN_EVENT_LOOP=1 — added to env for every bun invocation, not only the bun test branch.
  • results.json regenerated for bun only — verified independently: 161 flips to pass, 20 to fail, zero changes for node / nub / deno / node25, file set identical at 5,664.
  • Published figures moved — README tables regenerated (readme-table.mjs --check exits 0), homepage COMPAT and the blog sentence go 68.1% → 70.7%, both reconciling with scores.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:419 sets BUN_TEST_DRAIN_EVENT_LOOP=1 on the plain bun <file> path as well as the bun test path. 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.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread tests/cross-runtime/README.md Outdated
Comment thread tests/cross-runtime/run.mjs Outdated
…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.
@colinhacks

Copy link
Copy Markdown
Contributor Author

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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 the bun test path (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 reads 1 bun, matching meta.retried.bun.flippedToPass.
  • Figures regeneratedreadme-table.mjs --check exits 0; homepage COMPAT and the blog sentence move 70.7 → 70.8, reconciling with scores.denoExclusions.bun (3,573 / 5,046 = 70.81%).
  • 164 files flipped to pass and 20 flipped to fail — verified exact against main. No node / nub / deno / node25 record 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)

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread tests/cross-runtime/README.md Outdated
…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.
@colinhacks

Copy link
Copy Markdown
Contributor Author

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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.json re-measured on the v26.7.0 treemeta is back to corpusNodeVersion 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_VERSION appends -nightly when src/node_version.h carries NODE_VERSION_IS_RELEASE 0.
  • --merge refuses a corpus mismatch (run.mjs:693-705) — throws when prior.meta.corpusCommit differs from the corpus checkout's HEAD.
  • Figures moved 70.8 → 70.1 — README tables regenerated, homepage COMPAT and 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 61a3c870 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:45 documents --merge and 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.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread tests/cross-runtime/README.md Outdated
…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.
@colinhacks

Copy link
Copy Markdown
Contributor Author

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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 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:26 back to 4 bun — matches meta.retried.bun.flippedToPass in the regenerated results.json, which the re-measurement moved from 1 to 4. The other four counts in the sentence (node 1, nub 1, deno 2, node25 0) still match meta exactly.
  • wiki/research/node-test-suite-leverage.md:161 refreshed — all nine per-runtime figures now agree with the committed results.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 test switch 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 414 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.

Pullfrog  | Fix it ➔View workflow run | Using 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).
@colinhacks

Copy link
Copy Markdown
Contributor Author

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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 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--check now regex-extracts the homepage COMPAT rates and pass counts plus the blog's three percentages, compares them against scores.denoExclusions, and folds a mismatch into the existing drifted → exit 1 path.
  • README.md:120 rewritten — the hand-copy paragraph now states both site surfaces are checked, replacing the admission that they "drift silently until someone copies them across".
  • results.json untouchedgit diff --stat 50bdd370..10d6f70f is README.md (1 line) and readme-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 file process.exit(2), and .githooks/pre-push:245 treats any non-1 exit as infrastructure and warns past it. So relocating page.tsx silently disables the gate, while merely renaming a COMPAT entry correctly blocks (exit 1). These two files are tracked in this repo, so their absence is drift rather than an environment condition like a missing node.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using 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 = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread tests/cross-runtime/README.md Outdated
[`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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.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.
@colinhacks

Copy link
Copy Markdown
Contributor Author

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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 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 surfacescheckHandCopies() grew a by(lens) helper so a surface can assert against denoExclusions, bunUniverse or fullCorpus, and gained the wiki research doc's nine lens figures plus README.md:26's five retry-flip counts (meta.retried.<rt>.flippedToPass).
  • The pre-push trigger widened (.githooks/pre-push:230) — the pathspec now includes site/src/app/(home)/page.tsx, site/content/blog/introducing-nub.mdx and wiki/research/node-test-suite-leverage.md, so a push touching only a site or wiki surface runs --check.
  • README.md:120 restated — names all four gated surfaces and the widened trigger.
  • results.json and run.mjs untouchedgit diff --stat 10d6f70f..8954631 is the hook (4), README.md (2) and readme-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-245 prints tests/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 --write fixes only the two marker-delimited tables — a hand-copy mismatch survives it and reports README 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.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread tests/cross-runtime/readme-table.mjs
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).
@colinhacks

Copy link
Copy Markdown
Contributor Author

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).

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 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 takes major.minor from version(rt), and four value:-style wants compare it against the hand-written 'Deno 2.9' / 'Bun 1.4' on the homepage and the for Deno 2.9 / for Bun 1.4 in the blog sentence.
  • A value branch in the want loopif (w.value !== undefined) compares m[1] and continues before the rate/count logic, so a version want reuses the existing not-found and bad → exit 1 plumbing.
  • results.json and every other file untouchedgit diff --stat 8954631f..c95ec26b is readme-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.92.8 and the blog's for Bun 1.41.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.41.3 and blog for Deno 2.92.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.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread tests/cross-runtime/readme-table.mjs
…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).
@colinhacks

Copy link
Copy Markdown
Contributor Author

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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 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 labelscorpusMM takes major.minor from meta.corpusNodeVersion (26.7.026.7), and two wants compare it against page.tsx's name: '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 of tests: '5,046 / 5,046' against denoExclusions.nodePass, with rate: 100 pinned literally in the pattern so a moved rate fails as pattern-not-found rather than passing.
  • Gated the miss countMost of Nub&rsquo;s (\d+) misses against nodePass − nub.pass (5,046 − 4,968 = 78).
  • Generalized the value branch to valuesw.value !== undefined || w.values folds the multi-capture case into the existing not-found and bad → exit 1 plumbing.
  • Nothing else movedgit diff --stat c95ec26b..07daa4ed is readme-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,0465,040 / 5,046, the miss count 7879, and the blog's On Node 26.7's25.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 12bun 99, :93's 48 of the 5,664999 of the 1,111 and :53's 5,0789,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.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using 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" },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

page.tsx carries two corpus labels and this want reaches only one. Line 1007's prose reads Node &#38;rsquo;s own test suite — literally Node 26.7&#38;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&rsquo;s own test suite under Deno&rsquo;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.]+)&rsquo;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`.

Comment thread tests/cross-runtime/README.md Outdated
[`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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread tests/cross-runtime/readme-table.mjs Outdated
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];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.
@colinhacks

Copy link
Copy Markdown
Contributor Author

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.

@colinhacks
colinhacks merged commit a896fd1 into main Aug 31, 2026
45 checks passed
@colinhacks
colinhacks deleted the bun-test-accommodation branch August 31, 2026 22:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants