SIGN IN SIGN UP

test(verify-features): re-run the RelayFlow proof corpus for regressions (#1646)

* test(verify-features): re-run the RelayFlow proof corpus for regressions

Every merged feature/bug-fix PR leaves a case under
tests/relayflows/cases/ that discriminates one bug: a base arm that
reproduces it and a head arm that does not. Each is a real end-to-end
reproduction someone already paid to build and get right.

They were also run exactly once each. The PR-proof dispatcher selects
only the single case a PR declares, and prepare.mjs hard-fails a PR that
touches any other case ("A PR must change exactly its one declared
RelayFlow case"), so a later PR cannot re-run an earlier proof even in
principle. Reintroduce a fixed bug and nothing notices.

This tier re-runs the corpus head-only on the current checkout and
requires each case to still report its head signature. A case reporting
its base signature again is a regression of that exact bug, named.

Head-only is deliberate: outside a PR there is no meaningful base SHA,
and asserting the fixed behaviour is the property we want to hold on
main. A case whose runner cannot execute is recorded as a skip with its
cause, never as a pass — an unrunnable proof must not read as a verified
one.

Runs between critical-paths and cleanup on the existing schedule, so it
costs no new workflow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014N3p9VEngj9kLDFFhrzHNd

Session-Id: 246fba6e-6436-46ae-bc50-6bb3cca0d95e

* style: auto-format with Prettier

* test(verify-features): harden the corpus tier and drop a stray symlink

Six review findings, all valid.

- `set -u` plus a bare $TMPDIR aborted the entire step before a single
  case recorded anything, and TMPDIR is routinely unset on Linux. Read
  through printenv instead. Verified by running the tier with TMPDIR
  unset: it now completes rather than dying at line 1.

- EXPECTED_TIERS stopped at critical-paths, so a corpus tier that
  crashed before its first record() reached verdict as silence rather
  than as "did not run". The step is failOnError:false, which made that
  reachable. Now listed.

- The runner was unbounded. A stall in npm ci, a build, or a spawned
  broker would have blocked cleanup, verdict and every alerting step
  from ever running. Now bounded by the manifest's own timeoutSeconds,
  with timeout(1)'s 124 reported distinctly: a timeout proves nothing
  either way.

- Only runner.command[1] was taken and `node` was forced, running a
  different command from the proof contract, which also accepts bash and
  extra arguments. The whole command is rebuilt and the executable is
  checked against the same node|bash allowlist contract.mjs enforces.

- Fixed artifact paths kept the previous run's result, so a runner that
  exited zero without writing would have been scored on a stale
  signature and recorded PASS. Cleared before each invocation.

- A machine-local node_modules symlink into a private temp path was
  committed. Removed. Root cause: .gitignore listed `node_modules/`,
  and the trailing slash matches only a directory, never a symlink;
  both forms are now listed.

Verified by running the extracted tier against a fixture case: passes on
a matching signature (via a bash runner with an extra argument, proving
the full command is used), fails with <no signature> when a stale result
is present and the runner writes nothing, fails distinctly on timeout,
and fails naming the regression when the case reports its base
signature.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014N3p9VEngj9kLDFFhrzHNd

Session-Id: 246fba6e-6436-46ae-bc50-6bb3cca0d95e

Session-Id: 246fba6e-6436-46ae-bc50-6bb3cca0d95e

* test(verify-features): drop the .gitignore change from this PR

The proof gate merged in #1645 correctly refused this PR: .gitignore is
not in the non-runtime allowlist, so it counts as runtime and a
non-functional declaration is not available.

.gitignore genuinely cannot change what an installed CLI or broker does,
so this is a real gap in the allowlist rather than a gate defect — but
widening the allowlist is a deliberate change to the gate and does not
belong smuggled into a workflow PR. Splitting it out; this PR is the
corpus tier only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014N3p9VEngj9kLDFFhrzHNd

Session-Id: 246fba6e-6436-46ae-bc50-6bb3cca0d95e

* test(verify-features): refuse a broker binary that predates the checkout

A leftover binary under target/ is worse than no binary: the broker
cases would run green against an OLD broker and report a head signature
that says nothing about this commit. The tier picked any executable it
found.

Provenance is now established by age. If any file under crates/, or
Cargo.toml or Cargo.lock, is newer than the candidate binary, that
binary predates the tree and is refused, and the cases that need it skip
naming the stale path. Deliberately conservative: on a fresh checkout
every file carries checkout time, so a restored target/ cache is
rejected rather than trusted.

Verified both arms against a fixture: a binary older than the sources
skips with "is older than crates/lib.rs, so it was not built from this
checkout"; a binary newer than every source runs and passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014N3p9VEngj9kLDFFhrzHNd

Session-Id: 246fba6e-6436-46ae-bc50-6bb3cca0d95e

* test(verify-features): validate cases through the proof contract itself

The tier read manifests by hand and checked only that the executable was
node or bash. It did not verify that the script stays under its own case
directory, nor the id, version, kind, requirements or timeout bounds, so
a copied or malformed case would have been scored by weaker rules than
the gate applies to the same file.

Rather than reimplement those checks in shell, where they would drift,
the tier now calls validateCaseManifest from scripts/pr-proof/contract.mjs
- the same validator the dispatcher uses - and derives the timeout,
expected head signature, broker requirement and full runner command from
its normalised output. That also removes the second CASE_ROOT literal.

A rejected manifest skips with the contract's own reasons rather than a
node stack trace, since the skip line is all a reader sees for a case
that never ran.

timeout now passes -k 10 so a runner that traps TERM is still killed,
and exit 137 is counted as a timeout alongside 124.

Verified against fixtures: a malformed manifest and one whose runner
script points outside its case directory both skip with the contract's
reason ("case runner script must stay under
tests/relayflows/cases/escaped-case/"), and a valid case still passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014N3p9VEngj9kLDFFhrzHNd

Session-Id: 246fba6e-6436-46ae-bc50-6bb3cca0d95e

Session-Id: 246fba6e-6436-46ae-bc50-6bb3cca0d95e

* test(verify-features): carry case metadata over a NUL-terminated transport

A newline is legal inside a runner argument, and the newline-separated
transport would have split one argument into two before the case ran -
executing a different command from the manifest while still scoring the
result.

Fields are now NUL terminated. Terminated rather than separated: 'read
-d' needs the final field terminated too, and joining with NUL alone
silently dropped the last command element, which my first attempt did.
The metadata goes through a file rather than a command substitution
because $() strips NUL bytes, which are the delimiter that makes this
safe.

Verified with a fixture whose manifest carries a newline inside an
argument and whose runner fails unless it receives exactly one argument
still containing that newline: it passes now, and fails when the
transport is reverted to newline separation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014N3p9VEngj9kLDFFhrzHNd

Session-Id: 246fba6e-6436-46ae-bc50-6bb3cca0d95e

---------

Co-authored-by: Proactive Runtime Bot <agent@agent-relay.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
K
Khaliq committed
7320d324736df5e21cfa54cbc0bc5e1cd3802f4c
Parent: 3e363af
Committed by GitHub <noreply@github.com> on 9/2/2026, 8:05:16 PM