Skip to content

ci: standardize the gap-suite Node oracle on 26.5.0 (single .node-version pin)#6367

Merged
proggeramlug merged 3 commits into
mainfrom
ci/node-oracle-upgrade
Jul 13, 2026
Merged

ci: standardize the gap-suite Node oracle on 26.5.0 (single .node-version pin)#6367
proggeramlug merged 3 commits into
mainfrom
ci/node-oracle-upgrade

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

conformance-smoke (a required check) compiles every test-files/test_gap_*.ts and byte-diffs it against node --experimental-strip-types. Node is the oracle, so its version is a correctness input — but CI pinned Node 22 while the suite grew Node 24/26 features, and the pins were incoherent across the repo (20, 22, 24 and 26 all in use).

The failure mode is silent, not red. When node can't run a test, node exits non-zero, run_parity_tests.sh classifies it node_fail, and the test is dropped from the gate entirely. So anything Perry gets wrong about a Node 24/26 feature is invisible by construction.

This standardizes the oracle on Node 26.5.0, behind a single .node-version pin.

Measured delta (this is the point — the numbers came first)

All 299 gap tests, run under node 22 / 24 / 26 with the harness's exact invocation, then the same Perry build diffed against each oracle.

Tests the oracle hides (node_fail ⇒ excluded from the gate):

oracle node_fail tests actually gated
Node 22 (today) 18 281
Node 24 12 287
Node 26 (this PR) 4 295

Node 26 restores 14 tests to the gate. Node 24 would restore only 6.

The 14 Node 22 was hiding — and what they do under the new oracle:

test verdict on Node 26
test_gap_temporal_now, _duration, _options, _plaindate_plaintime, _plaindatetime, _instant_zoned, _construct_overflow ✅ pass (7)
test_gap_float16array_2902 ✅ pass
test_gap_u8_base64_hex_2901 ✅ pass
test_gap_2899_2779_2777_static_helpers ✅ pass
test_gap_async_advanced ✅ pass
test_gap_constants_tail_3683plus ✅ pass
test_gap_error_2836_2838_2904_2882 ✅ pass
test_gap_disposablestack_2875 fails → triaged, #6364

13 of 14 pass. Perry already implements Temporal, Float16Array, Uint8Array base64/hex and DisposableStack — the required gate had simply never checked a single one of them. That is the blind spot this closes.

Tests whose expected output changes between 22 and 26 (both oracles exit 0, but node's output differs): 8. Six are already triaged in known_failures.json (gate-safe under either oracle); the remaining two — test_gap_console_methods (timer ms) and test_gap_url_batch2_3057_3060_3062 (DEP0169 warning) — are invariant after the harness's own normalize_output, verified by extracting and running that function. So they carry no risk.

Net: the only newly-untriaged failure the bump exposes is disposablestack_2875, and it is triaged here. The gate lands green.

Sanity check: my Node-26 measurement independently reproduces the known local baseline — 4 node_fail ⇒ 295 runnable, matching the 277/295 + 14 triaged + 2 untriaged figure.

Why Node 26, not 24

  • Coverage. 24 leaves 8 of the 14 still hidden — all 7 Temporal tests plus Uint8Array base64/hex. Standardizing on 24 would leave Perry's entire Temporal implementation permanently unverified by the required gate.
  • No regression in visibility. There is no test that runs on 22 but node_fails on 26. Node 26 strictly dominates.
  • It fixes the local-vs-CI mismatch, which is the second half of the problem: devs run 26, and feature-matrix / node-suite-guard were already on 26.
  • LTS is the wrong criterion here. Perry compiles to native binaries; users never run Node. The oracle's job is to be the most complete reference implementation of the spec, not to mirror a production deployment.

Why not a 22+26 matrix

Rejected on evidence, not cost. Eight tests produce different output on 22 vs 26 (BigInt RangeError text, Intl locale error text, v8 heap node_fields, diagnostics_channel store semantics, SQLite ExperimentalWarning). Perry emits one byte string, so it cannot byte-match both oracles simultaneously — a matrix would be permanently red by construction, not merely expensive.

For the record, the cost would also have been real: the gap suite is 8 shards × 19–37 min, so a second oracle roughly doubles the repo's most expensive required gate, at roughly +150–300 runner-minutes per PR.

Coherence: one pin, three documented exemptions

.node-version (26.5.0) is now the single source of truth; every test workflow reads it via setup-node's node-version-file. The pin is exact on purpose — node patch releases change observable output (we measured error-message text changing), so a byte-diff oracle must not drift silently under us.

Standardized: test.yml (×6, incl. conformance-smoke + parity), npm-package-sweep, feature-matrix, node-suite-guard, benchmark.

Exempt, and each now says so inline so the next person doesn't "fix" it into breakage:

  • node-core-subset.yml — runs Node's own test/parallel corpus, sparse-checked-out at the ref in test-compat/node-core/pinned-version.txt (v22.x). Those tests are written against that Node line's internals, so the runtime must match the corpus, not the oracle. It now derives its version from that same file, so the coupling is explicit and can't break — previously a bare inline "22" looked exactly like drift.
  • release-hono-server.yml (24) and release-packages.yml (20) — npm publishing toolchains (OIDC Trusted Publishing needs npm ≥ 11.5.1); they never run a parity diff. Checked, not assumed.

Out of scope

test_gap_handle_band_object_ops (#6363) is an untriaged failure on main, but it is oracle-invariant — node exits 0 and emits byte-identical output on 22, 24 and 26 — so this change cannot affect its verdict either way. Left alone deliberately.

Validation

  • python3 -c "import yaml,...; yaml.safe_load(...)" over all 14 workflows — valid.
  • Delta measured on one Perry build across all three oracles (299 tests × 3).
  • The one exposed failure is triaged with issue + date + category per the _schema contract.

Summary by CodeRabbit

  • Chores

    • Updated CI and performance workflows to use the repository’s pinned Node.js version.
    • Set the pinned Node.js version to 26.5.0.
    • Preserved documented exceptions for publishing and Node core test workflows.
  • Documentation

    • Clarified Node.js compatibility, gap-test behavior, and workflow versioning guidance.
  • Tests

    • Added a newly identified parity failure to the known-failures list for tracking.

Ralph Küpper added 3 commits July 13, 2026 12:24
The gap suite (conformance-smoke, a required check) byte-diffs every
test-files/test_gap_*.ts against `node --experimental-strip-types`, so Node is
the oracle and its version is a correctness input. CI pinned Node 22 while the
suite grew Node 24/26 features. When node itself cannot run a test it exits
non-zero, run_parity_tests.sh classifies it `node_fail`, and the test is
dropped from the gate — so a feature Perry gets wrong is invisible by
construction rather than red.

Measured over all 299 gap tests, same Perry build, node 22 vs 24 vs 26:
  node 22 -> 18 node_fail (18 tests excluded from the gate)
  node 24 -> 12 node_fail
  node 26 ->  4 node_fail
Node 26 restores 14 tests to the gate; Node 24 only 6. The 8 that only 26
un-hides are Temporal (7) plus Uint8Array base64/hex — whole feature families
that Node 24 cannot see at all. No test regresses (there is no test that runs
on 22 but node_fails on 26), so 26 strictly dominates.

A 22+26 matrix was rejected on evidence, not cost: 8 tests produce *different*
output on 22 vs 26 (BigInt RangeError text, Intl locale error text, v8 heap
node_fields, diagnostics_channel store semantics, SQLite ExperimentalWarning).
Perry emits one byte string, so it cannot byte-match both oracles at once — a
matrix would be permanently red by construction.

Pins now come from .node-version, so the oracle cannot drift silently and local
runs are comparable to CI. Exempt, each documented inline:
  - node-core-subset.yml runs Node's OWN test corpus and must match the ref in
    test-compat/node-core/pinned-version.txt; now derived from that file so the
    coupling is explicit instead of looking like drift.
  - release-hono-server.yml (24) and release-packages.yml (20) are npm
    publishing toolchains, not oracles.
…aring

The gap suite is a byte-diff against node, so the oracle version decides which
tests are even *visible* to the required gate — a test whose feature the pinned
node lacks is dropped as node_fail, not reported red. Say so where contributors
already read about the suite, and point at .node-version as the single pin.
…y the Node 26 oracle

Raising the gap-suite oracle from Node 22 to 26 restores 14 tests that node 22
could not run (and that the harness therefore dropped as `node_fail` rather
than reporting). Measured against the new oracle, 13 of the 14 PASS — the whole
Temporal family (7), Float16Array, Uint8Array base64/hex, static helpers,
async_advanced, the constants tail, and the error batch. Perry already
implements all of these; the required gate had simply never checked them.

The 14th, test_gap_disposablestack_2875, fails: `.disposed` returns undefined
where node returns false/true. That is a pre-existing gap in Perry's
DisposableStack surface (crates/perry-runtime/src/disposable.rs), already
tracked as #6364 — it is newly *visible*, not newly broken. Triaged here so the
oracle bump lands green instead of blocking every PR in the repo.
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Node version alignment

Layer / File(s) Summary
Node version contract and parity guidance
.node-version, CLAUDE.md, test-parity/known_failures.json
Node 26.5.0 is pinned, gap-suite oracle guidance is updated, and a newly observable DisposableStack parity failure is recorded.
Standard CI Node source
.github/workflows/benchmark.yml, .github/workflows/feature-matrix.yml, .github/workflows/node-suite-guard.yml, .github/workflows/npm-package-sweep.yml, .github/workflows/test.yml
Standard CI jobs now source Node from .node-version instead of hardcoded versions.
Special workflow Node sources and exceptions
.github/workflows/node-core-subset.yml, .github/workflows/release-hono-server.yml, .github/workflows/release-packages.yml
The Node core workflow derives its runtime from its pinned version file, while release workflows document their publishing-toolchain exemption.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: standardizing the gap-suite Node oracle on the repo's .node-version pin.
Description check ✅ Passed The description is detailed and covers the rationale, scope, and validation, though it doesn't mirror every template section.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/node-oracle-upgrade

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
.github/workflows/node-core-subset.yml (1)

78-101: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Use consistent file-reading approach for pinned-version.txt.

Line 93 reads the file with tr -d '[:space:]' (strips all whitespace), but line 106 reads the same file with $(cat ...) (strips only trailing newlines). If the file ever contains trailing spaces or carriage returns, the two reads would produce different values, potentially causing git clone --branch "$node_ref" to fail at line 107 while the version resolution at line 93 succeeds.

♻️ Suggested consistency fix for line 106
       - name: Sparse checkout pinned Node.js tests
         run: |
           set -euo pipefail
-          node_ref="$(cat test-compat/node-core/pinned-version.txt)"
+          node_ref="$(tr -d '[:space:]' < test-compat/node-core/pinned-version.txt)"
           git clone --no-checkout --depth 1 --branch "$node_ref" \
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/node-core-subset.yml around lines 78 - 101, Use the same
whitespace-normalizing read for pinned-version.txt in the later clone/setup flow
as the node_core_version resolution, replacing the inconsistent cat-based read
that assigns node_ref. Ensure both consumers derive the branch value from
identical trimmed content so git clone --branch receives the normalized pinned
ref.
CLAUDE.md (1)

16-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider trimming the historical narrative in line 18 for conciseness.

The paragraph contains essential operational guidance (oracle pin, node_fail drop behavior, exact-pin rationale) that belongs here, but also embeds historical CI details — specific test names, issue numbers, and the Node 22→24→26 baseline history — that read like changelog-adjacent content. Per the coding guidelines, CLAUDE.md should be kept concise and detailed per-version history should live in CHANGELOG.md. Consider moving the historical enumeration ("CI sat on Node 22…hid 14 tests — all of Temporal, plus DisposableStack, Float16Array, and Uint8Array base64/hex (#6364)") to CHANGELOG.md or the issue tracker, keeping only the actionable directive.

As per coding guidelines: "Keep CLAUDE.md concise; keep detailed per-version changelogs in CHANGELOG.md, not CLAUDE.md."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` around lines 16 - 20, Trim the pinned-Node guidance in CLAUDE.md
by removing the historical CI narrative, including the Node 22→24→26 history,
hidden-test count, feature list, and issue reference. Preserve the actionable
oracle requirements: use the exact .node-version, explain node_fail handling,
and retain the rationale for exact patch pinning; move historical details to
CHANGELOG.md or the issue tracker if they must be retained.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/test.yml:
- Around line 1422-1426: Update the comments accompanying node-version-file in
the affected package smoke jobs to retain the .node-version
single-source-of-truth note while replacing the gap/parity-oracle explanation
with the actual fixture-specific rationale for pinning Node. Apply the same
comment correction to all three corresponding job blocks, without changing the
Node configuration.

---

Nitpick comments:
In @.github/workflows/node-core-subset.yml:
- Around line 78-101: Use the same whitespace-normalizing read for
pinned-version.txt in the later clone/setup flow as the node_core_version
resolution, replacing the inconsistent cat-based read that assigns node_ref.
Ensure both consumers derive the branch value from identical trimmed content so
git clone --branch receives the normalized pinned ref.

In `@CLAUDE.md`:
- Around line 16-20: Trim the pinned-Node guidance in CLAUDE.md by removing the
historical CI narrative, including the Node 22→24→26 history, hidden-test count,
feature list, and issue reference. Preserve the actionable oracle requirements:
use the exact .node-version, explain node_fail handling, and retain the
rationale for exact patch pinning; move historical details to CHANGELOG.md or
the issue tracker if they must be retained.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b7126daa-743f-458d-aaca-61ef9c998a34

📥 Commits

Reviewing files that changed from the base of the PR and between dd49fc3 and 2f8e2ff.

📒 Files selected for processing (11)
  • .github/workflows/benchmark.yml
  • .github/workflows/feature-matrix.yml
  • .github/workflows/node-core-subset.yml
  • .github/workflows/node-suite-guard.yml
  • .github/workflows/npm-package-sweep.yml
  • .github/workflows/release-hono-server.yml
  • .github/workflows/release-packages.yml
  • .github/workflows/test.yml
  • .node-version
  • CLAUDE.md
  • test-parity/known_failures.json

Comment on lines +1422 to +1426
# Single source of truth: .node-version at the repo root. Node is the
# gap/parity oracle (we byte-diff against it), so the version is a
# correctness input, not an incidental toolchain detail — never pin it
# inline here. See CLAUDE.md ("TypeScript Parity Status").
node-version-file: .node-version

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use job-specific rationale for the Node pin.

These package smoke jobs do not run the gap/parity oracle steps in this workflow, but the comments describe Node as that oracle. Keep the single-source-of-truth note, and replace the copied oracle explanation with the fixture-specific reason for pinning Node.

Also applies to: 1488-1492, 1541-1545

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 1422 - 1426, Update the comments
accompanying node-version-file in the affected package smoke jobs to retain the
.node-version single-source-of-truth note while replacing the gap/parity-oracle
explanation with the actual fixture-specific rationale for pinning Node. Apply
the same comment correction to all three corresponding job blocks, without
changing the Node configuration.

@proggeramlug
proggeramlug merged commit db4b068 into main Jul 13, 2026
25 checks passed
@proggeramlug
proggeramlug deleted the ci/node-oracle-upgrade branch July 13, 2026 13:47
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.

1 participant