Skip to content

test(parity): bidirectional snapshot ratchet for the gap suite (#797)#6755

Open
TheHypnoo wants to merge 4 commits into
mainfrom
feat/gap-snapshot-ratchet
Open

test(parity): bidirectional snapshot ratchet for the gap suite (#797)#6755
TheHypnoo wants to merge 4 commits into
mainfrom
feat/gap-snapshot-ratchet

Conversation

@TheHypnoo

@TheHypnoo TheHypnoo commented Jul 22, 2026

Copy link
Copy Markdown
Member

Second of the base-hygiene changes drawn from oxc's workspace (after #6754). Adopts their conformance ratchet: a generated snapshot, committed, diffed by CI in both directions.

The problem

run_gap_tests.sh gated with comm -23 <failures> known_failures.json — "fail on any failure not already triaged". That is one-directional. A listed test that starts passing produces no signal at all, so its entry survives the bug it was tracking, and the file decays into orphans nobody can revalidate. That decay is exactly what #797 asks someone to clean up by hand.

The change

scripts/gap_snapshot.py generates and checks test-parity/gap_snapshot.json, listing every gap test that is not passing. A test absent from the file is expected to pass. CI fails on any divergence in either direction — a new failure, a listed test that now passes, or a status change. Accept the current state with:

UPDATE_SNAPSHOT=1 ./scripts/run_gap_tests.sh

and commit the diff. Because an improvement is a red build until it is recorded, an orphaned entry becomes impossible rather than something to audit periodically.

Design points worth reviewing:

  • Only non-passing tests are stored. "Fails and is not listed" already means regression, so listing the ~370 passing tests would quadruple the file without adding a single detection. The snapshot stays small enough to read in a PR diff.
  • node_fail / skipped are recorded explicitly instead of being dropped from the denominator. A test the oracle stopped covering becomes a visible diff — that hole is how a Node 22 pin hid 14 tests for months (runtime: DisposableStack / Symbol.dispose surface incomplete (.disposed returns undefined) #6364).
  • Statuses only, never program output. The harness truncation marker embeds an exact line count (TRUNCATED … (total: N)) that is not stable across runs for a flapping test (CI: stop truncating gap-suite output, add per-test summary #796).
  • Shard-safe. Check and update act only on tests present in the report, so a --shard N/8 run never judges or rewrites its siblings' entries. An entry is pruned only when its test file is gone from disk, which is decidable from any shard.
  • Triage metadata carries across a status change (parity_fail -> crash is the same open bug); a fixed test drops out of the file entirely. New failures land as category: untriaged with a null issue — that placeholder is the gate asking for triage.

Also adds test-parity/README.md: the category reference that known_failures.json's schema has always pointed at and which never existed (a #797 finding).

Scope

known_failures.json still gates the tag-gated parity job (the full test-files/*.ts suite); its scope is now documented in-file. Migrating that job needs a full-suite baseline from a tag run and is a follow-up.

Bootstrap — why this starts red

The snapshot is evidence about a specific platform and oracle, so it cannot honestly be generated on a laptop: CI runs Ubuntu with Node pinned by .node-version (26.5.0), and several failure classes are Linux-only or oracle-version-dependent. Generating it on macOS/Node 24 would commit wrong statuses.

So the first conformance-smoke run here is expected to fail, listing the real failures as regressions against an absent snapshot. Its 8 shards still upload their reports (if: always()), and the snapshot is seeded by folding those 8 reports in — the merge-only update semantics exist precisely so 8 partial reports compose into one file. Once seeded and committed, the run goes green and this leaves draft.

Validation

gap_snapshot.py --self-test covers both diff directions, the sharded-update case, triage carry-over, and pruning; it runs in the lint job so the logic gating conformance-smoke is checked on the cheap job rather than only 8 shards deep. Additionally exercised end-to-end against synthetic reports: seed → green, improvement → red, regression → red, status change → red, and a one-test sharded update leaving a sibling entry intact.

Per contributor convention, no version bump — maintainer bumps at merge time.


Update: snapshot seeded — and it immediately found a hole

Seeded from this PR's first (intentionally red) run: 401 tests, 384 passing, 17 not. All 8 shards now check green against it.

The 8 real failures were already in known_failures.json, and their triage is carried over rather than discarded. The other 9 were invisible: node_fail tests that no skip-list entry covered, because the old gate dropped node_fail from the denominator. They break down as:

Tests Why node exits non-zero Verdict
6 npm-import (exponential-backoff, cron, dayjs, moment, rate-limiter-flexible, slugify) Import cannot resolve without node_modules Same class as test_ramda_sum; needs a CI fixture (#1634)
test_gap_4510_enum_forward_ref, test_gap_derived_param_props ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX — enums and parameter properties cannot run in strip-only mode These have never verified the feature they were written for. Needs an expected-output fixture
test_gap_prop_plan_cache_invalidation TypeError: Cannot assign to read only property 'q' (ESM strict mode) Needs triage

The two ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX cases are the sharpest argument for the ratchet: both were written to catch real Perry bugs (#4510 enum forward-references, #1758/#321 derived parameter properties), and both have been silently no-ops against the oracle ever since. The harness already supports expected-output fixtures (test-parity/expected/<name>.txt), so the fix is mechanical — worth its own issue rather than bundling here.

Summary by CodeRabbit

  • CI Improvements

    • Added snapshot-based validation for gap-suite test results.
    • CI now detects both newly introduced failures and tests that unexpectedly begin passing.
    • Added self-validation for snapshot behavior.
  • Documentation

    • Documented gap-test gating, snapshot updates, status tracking, and regeneration procedures.
    • Added a generated snapshot recording expected non-passing test results and triage details.

…napshot ratchet

run_gap_tests.sh gated with `comm -23 <failures> known_failures.json` — fail on
any failure not already triaged. That only catches regressions: a listed test
that starts PASSING produces no signal, so its entry outlives the bug it tracked
and the file decays into orphans nobody can revalidate (#797).

- New scripts/gap_snapshot.py generates and checks test-parity/gap_snapshot.json,
  a committed list of every non-passing gap test. Checked BOTH ways — a new
  failure is red, and so is a listed test that now passes, which makes an
  orphaned entry impossible instead of something to audit by hand. Accept the
  current state with UPDATE_SNAPSHOT=1 ./scripts/run_gap_tests.sh.
- Only non-passing tests are stored: "fails and is not listed" already means
  regression, so listing the ~370 passing tests would quadruple the file
  without adding a detection.
- node_fail/skipped are recorded explicitly rather than dropped from the
  denominator, so an oracle-coverage change is a visible diff, not a silent
  hole (a Node 22 pin hid 14 tests that way, #6364).
- Statuses only, never program output: the harness truncation marker embeds an
  exact line count that is not stable across runs (#796).
- Shard-safe: check and update act only on tests present in the report, so a
  --shard N/8 run never judges or rewrites its siblings' entries. Entries are
  pruned only when the test file is gone, decidable from any shard.
- Triage metadata carries across a status change (parity_fail -> crash is the
  same open bug); a fixed test drops out of the file entirely.
- Add test-parity/README.md — the category reference known_failures.json's
  schema has always pointed at but which never existed (#797 finding).
- known_failures.json keeps gating the tag-gated full parity job; its scope is
  now documented in-file. Migrating that job is a follow-up.
- The checker's self-test runs in the lint job, so the logic that decides
  whether conformance-smoke goes red is verified on the cheap job.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The gap test gate now compares reports against a committed snapshot, detects regressions and improvements, supports explicit snapshot updates, and validates its comparison logic in CI. Documentation distinguishes the snapshot gate from the legacy parity allowlist.

Changes

Gap snapshot ratchet

Layer / File(s) Summary
Snapshot comparison and merge tool
scripts/gap_snapshot.py
Loads reports and snapshots, detects status divergences, merges non-passing results, preserves triage metadata, prunes removed tests, and provides self-test and CLI modes.
Gap test gate integration
scripts/run_gap_tests.sh, .github/workflows/test.yml
Runs snapshot checks or updates, forwards shard arguments, retains crash reporting, and executes the snapshot self-test in lint CI.
Committed snapshot and gate documentation
test-parity/gap_snapshot.json, test-parity/README.md, changelog.d/6755-gap-snapshot-ratchet.md, test-parity/known_failures.json
Adds the generated gap snapshot, documents both conformance gates, records the ratchet change, and updates legacy allowlist metadata and reasons.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Developer
  participant RunGapTests
  participant ParityRunner
  participant GapSnapshot
  Developer->>RunGapTests: invoke gap suite with optional shard arguments
  RunGapTests->>ParityRunner: run filtered gap tests
  ParityRunner-->>RunGapTests: produce test report
  RunGapTests->>GapSnapshot: check report against committed snapshot
  GapSnapshot-->>RunGapTests: return divergence status
  Developer->>RunGapTests: set UPDATE_SNAPSHOT=1 when accepting changes
  RunGapTests->>GapSnapshot: update committed snapshot
Loading

Suggested reviewers: proggeramlug

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: a bidirectional snapshot ratchet for the gap suite.
Description check ✅ Passed The description covers the summary, rationale, changes, validation, and related issue, though it doesn't follow the template headings exactly.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/gap-snapshot-ratchet

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
.github/workflows/test.yml

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

test-parity/gap_snapshot.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

test-parity/known_failures.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'


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.

Folded from the 8 shard reports of this PR's first conformance-smoke run —
Ubuntu, Node 26.5.0 per .node-version. Generating it on a macOS/Node 24
laptop would have committed statuses that are wrong for the gate's platform
and oracle, so the run had to happen first (that is why the first run was red).

Seeding it immediately surfaced 9 node_fail tests that NO existing skip-list
entry covered, because the old gate dropped node_fail from the denominator
entirely — the #6364 hole, still open:

- 6 npm-import tests (exponential-backoff, cron, dayjs, moment,
  rate-limiter-flexible, slugify): node cannot resolve the import without
  node_modules, so the oracle never runs. Same class as test_ramda_sum;
  tracked to #1634 pending a CI-side fixture.
- test_gap_4510_enum_forward_ref and test_gap_derived_param_props: node
  refuses the file outright with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX — enums
  and parameter properties cannot run in strip-only mode. These tests have
  therefore never verified the very features they were written for. The fix
  is an expected-output fixture, not an edit to the test.
- test_gap_prop_plan_cache_invalidation: node throws TypeError "Cannot assign
  to read only property 'q'" under ESM strict mode; needs triage.

Triage for the 8 failures already in known_failures.json is carried over
rather than discarded, so no provenance is lost. Every entry has a category
and a reason; none are left untriaged.
@TheHypnoo
TheHypnoo marked this pull request as ready for review July 22, 2026 14:36

@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

🤖 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 `@scripts/run_gap_tests.sh`:
- Around line 46-52: Update the run_parity_tests.sh invocation in
run_gap_tests.sh to remove any existing latest.json before execution, capture
its exit status, and only continue for the documented aggregate-parity failure;
propagate every other non-zero status so setup or runtime failures cannot use
stale or partial results.
🪄 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: e8c4ebd5-068e-4ff9-9acb-64366b999301

📥 Commits

Reviewing files that changed from the base of the PR and between f5ab0c0 and 8d24b93.

📒 Files selected for processing (7)
  • .github/workflows/test.yml
  • changelog.d/6755-gap-snapshot-ratchet.md
  • scripts/gap_snapshot.py
  • scripts/run_gap_tests.sh
  • test-parity/README.md
  • test-parity/gap_snapshot.json
  • test-parity/known_failures.json

Comment thread scripts/run_gap_tests.sh
Comment on lines +46 to 52
# run_parity_tests.sh exits 1 when AGGREGATE parity < 80%. We gate on the
# snapshot diff instead (below), so don't let its aggregate exit abort us.
set +e
# Forward extra args (notably --shard N/M) so CI can fan the gap suite out
# across parallel runners; with no args this is the full serial gap suite.
./run_parity_tests.sh --filter test_gap_ "$@"
set -e

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Preserve unexpected parity-runner failures.

The runner exit status is discarded entirely. A setup/runtime failure can therefore proceed against a stale or partial latest.json, and UPDATE_SNAPSHOT=1 can commit that state. Capture the status, remove any prior report before execution, and only tolerate the documented aggregate-parity exit; fail for every other non-zero status.

As per coding guidelines, “When evaluating test or benchmark results, verify the harness exit code directly and do not use binaries from another worktree or stale build artifacts as bisect endpoints or performance baselines.”

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 47-47: set +e (or set +o errexit) disables the shell's errexit option, so the script keeps running after a command fails. This masks failures of security-critical operations (downloads, signature/checksum verification, permission changes, cleanup of secrets), letting the script proceed with a bad or insecure state. Leave errexit enabled (set -e / set -euo pipefail), or handle failures explicitly with if/|| and an explicit exit instead of globally turning off failure detection.
Context: set +e
Note: [CWE-754] Improper Check for Unusual or Exceptional Conditions.

(set-plus-e-error-masking-bash)

🤖 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 `@scripts/run_gap_tests.sh` around lines 46 - 52, Update the
run_parity_tests.sh invocation in run_gap_tests.sh to remove any existing
latest.json before execution, capture its exit status, and only continue for the
documented aggregate-parity failure; propagate every other non-zero status so
setup or runtime failures cannot use stale or partial results.

Source: Coding guidelines

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