test(sigint): distinct X3/X4 vectors, nesting-safe paths, and the gate I should have run

Five findings. The first was red CI that my local gate could not have
caught.

1. `crate::common` cannot resolve when gpu_invocation_acceptance.rs is
   compiled as a nested module of gpu_initial_target_acceptance.rs,
   where `crate::` is the outer test crate. Now `super::common`, which
   resolves in both modes --- verified by compiling each target
   explicitly. Clippy's `(Some(1 | 2), true)` folding applied too.

   The reason this shipped: plain `./scripts/gate` omits sweep-crdt,
   the only stage that compiles the nested target under crdt, while
   04-lib-crdt builds the lib alone. This lane gates with `--protocol`,
   and the ledger now says so.

2. X3 and X4 had stopped being the cases the framing specifies:
   stub_script() gave every case the same sentinel stderr, so X3 lacked
   the canonical ignored text and X4 was byte-identical to
   0/V/safe/bare --- 45 entries, 43 distinct inputs. Case now carries an
   explicit stderr payload; X3 emits the canonical wording with no
   token, and both consumers assert they never repeat it.

3. The capture-creation-failure row asserted exit, wording and stage
   output but not residue. It now inspects the temporary root before
   its RAII drop and requires it empty.

4. The exact-token test covered safe and error but not ignored, despite
   the ledger claiming all three. The ignored arm now asserts its exact
   stdout, driven through a SIGINT-ignoring shell.

5. The ledger's claim that the status-2 mutation is caught only by the
   dedicated row is superseded --- the sentinel matrix catches it --- and
   the self-referential "this commit" is replaced by bc7d776.

Also records two PRE-EXISTING crdt-only failures found while gating
properly (m4_24_bare_string_glob_stays_relative and
m4_24_d3_fallback_base_is_the_smallest_attachment_dir): they reproduce
in isolation and fail identically at 72da24a, so they are not this
lane's, and no cause is claimed for them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
Levi Neuwirth 2026-08-20 11:50:34 +02:00
parent 9321975197
commit fb8a904923
No known key found for this signature in database
4 changed files with 101 additions and 14 deletions

View File

@ -279,7 +279,7 @@ from #171 and #215.
**`72da24a`**, worktree **`72da24a`**, worktree
`/home/jeans/Repos/personal/pmacs-probe-sigint`. Recover with `/home/jeans/Repos/personal/pmacs-probe-sigint`. Recover with
`git fetch githubsucks && git checkout gpu-probe-sigint-teardown`. `git fetch githubsucks && git checkout gpu-probe-sigint-teardown`.
- **REVISION 13 IMPLEMENTED (this commit).** Helper emits the token on - **REVISION 13 IMPLEMENTED at `bc7d776`.** Helper emits the token on
stdout; both consumers validate the `(status, token)` pair. The gate stdout; both consumers validate the `(status, token)` pair. The gate
owns a guard-local capture dir, keeps `|| status=$?` under `set -eu`, owns a guard-local capture dir, keeps `|| status=$?` under `set -eu`,
selects `expected_token` before any `set -u`-sensitive use, compares selects `expected_token` before any `set -u`-sensitive use, compares
@ -313,13 +313,29 @@ from #171 and #215.
- **The earlier `…-2647615` run is NOT head-exact evidence**: it - **The earlier `…-2647615` run is NOT head-exact evidence**: it
finished about 30 seconds before `bc7d776` was committed, so it finished about 30 seconds before `bc7d776` was committed, so it
describes the implementation tree rather than a committed head. describes the implementation tree rather than a committed head.
- **A4 mutations, each biting:** accepting any status 2 regardless of - **A4 mutations, each biting the MATRIX now**, not a dedicated row:
token → `gate_refuses_on_helper_error_…`; surfacing child stderr on a accepting any status 2 regardless of token, and surfacing child
boundary failure → the conformance row **and** that row; token to stderr on a boundary failure, both fail
stderr → `sigint_helper_reports_safe_…`. Noted for the record: the `gate_validates_the_whole_shared_conformance_set`; token-to-stderr
first mutation is caught by the dedicated row rather than by the fails `sigint_helper_reports_safe_…`. An earlier entry said the
conformance set, because the set's boundary cases mostly have empty status-2 mutation was caught only by the dedicated row — that was
stderr and so cannot see which branch produced the exit 2. true of the pre-sentinel matrix and is **superseded**: every stub now
emits a sentinel on stderr, so the matrix can see which branch
produced the exit 2.
- **Two PRE-EXISTING crdt-only failures found while gating properly:**
`m4_24_bare_string_glob_stays_relative` and
`m4_24_d3_fallback_base_is_the_smallest_attachment_dir`. They
reproduce in isolation (so not load) and **fail identically at
`72da24a`**, so they are not this lane's. They are crdt-only — the
plain gate's `05-m4` stage runs without `crdt` and passes. Recorded,
not attributed; whether they are environment-specific is for CI to
say.
- **My local gate did NOT cover what CI covers, and CI caught it.**
Plain `./scripts/gate` omits `sweep-crdt`, which is the only stage
that compiles the nested `gpu_initial_target_acceptance` under
`crdt`; `04-lib-crdt` builds the lib alone. A `crate::common` path
that cannot resolve when nested, and a clippy lint, both shipped
green locally. **This lane gates with `--protocol`.**
- **CI ON `916007b`: 12 GREEN, 2 RED — both macOS `Test` jobs**, and it - **CI ON `916007b`: 12 GREEN, 2 RED — both macOS `Test` jobs**, and it
is the **pre-declared A7 portability finding**, not an environment is the **pre-declared A7 portability finding**, not an environment
excuse. Exactly one row: excuse. Exactly one row:

View File

@ -33,10 +33,20 @@ pub const TOKEN_ERROR: &[u8] = b"pmacs-sigint-v1:error";
/// searched for it: present ⇒ the child's stderr was surfaced. /// searched for it: present ⇒ the child's stderr was surfaced.
pub const SENTINEL: &str = "PMACS-CONFORMANCE-SENTINEL"; pub const SENTINEL: &str = "PMACS-CONFORMANCE-SENTINEL";
/// The canonical wording the helper uses for `ignored`. X3 emits it
/// **without** a valid token, so a consumer that surfaced untrusted
/// stderr would repeat it — the defect A6b forbids.
pub const CANONICAL_IGNORED: &str =
"pmacs: SIGINT is ignored; run this command with SIGINT deliverable";
pub struct Case { pub struct Case {
pub name: String, pub name: String,
pub status: i32, pub status: i32,
pub stdout: Vec<u8>, pub stdout: Vec<u8>,
/// Exact stderr this stub emits. Most cases use [`SENTINEL`]; X3
/// and X4 carry their own payloads, which is what makes them
/// distinct inputs rather than duplicates of other rows.
pub stderr: String,
pub expect: Outcome, pub expect: Outcome,
} }
@ -49,8 +59,8 @@ pub fn stub_script(case: &Case) -> String {
acc acc
}); });
format!( format!(
"#!/bin/sh\nprintf '{octal}'\necho '{SENTINEL}' >&2\nexit {}\n", "#!/bin/sh\nprintf '{octal}'\necho '{}' >&2\nexit {}\n",
case.status case.stderr, case.status
) )
} }
@ -61,6 +71,12 @@ pub fn stub_script(case: &Case) -> String {
/// shell boundary cannot represent it, because an `exec` failure there /// shell boundary cannot represent it, because an `exec` failure there
/// becomes a status. Rust exercises it separately. /// becomes a status. Rust exercises it separately.
#[must_use] #[must_use]
#[allow(
clippy::too_many_lines,
reason = "the bulk is the generated vector list; splitting it would \
separate a case from the outcome it encodes, which is the \
one thing this file exists to keep together"
)]
pub fn shared_cases() -> Vec<Case> { pub fn shared_cases() -> Vec<Case> {
let toks: [(&str, &[u8]); 3] = [ let toks: [(&str, &[u8]); 3] = [
("safe", TOKEN_SAFE), ("safe", TOKEN_SAFE),
@ -81,12 +97,14 @@ pub fn shared_cases() -> Vec<Case> {
name: format!("{status}/V/{name}/bare"), name: format!("{status}/V/{name}/bare"),
status, status,
stdout: correct.to_vec(), stdout: correct.to_vec(),
stderr: SENTINEL.to_owned(),
expect: diagonal, expect: diagonal,
}); });
out.push(Case { out.push(Case {
name: format!("{status}/V/{name}/lf"), name: format!("{status}/V/{name}/lf"),
status, status,
stdout: lf, stdout: lf,
stderr: SENTINEL.to_owned(),
expect: diagonal, expect: diagonal,
}); });
for (other, bytes) in &toks { for (other, bytes) in &toks {
@ -99,12 +117,14 @@ pub fn shared_cases() -> Vec<Case> {
name: format!("{status}/M/{other}/bare"), name: format!("{status}/M/{other}/bare"),
status, status,
stdout: bytes.to_vec(), stdout: bytes.to_vec(),
stderr: SENTINEL.to_owned(),
expect: Outcome::Boundary, expect: Outcome::Boundary,
}); });
out.push(Case { out.push(Case {
name: format!("{status}/M/{other}/lf"), name: format!("{status}/M/{other}/lf"),
status, status,
stdout: olf, stdout: olf,
stderr: SENTINEL.to_owned(),
expect: Outcome::Boundary, expect: Outcome::Boundary,
}); });
} }
@ -135,6 +155,7 @@ pub fn shared_cases() -> Vec<Case> {
name: format!("{status}/{cls}"), name: format!("{status}/{cls}"),
status, status,
stdout: bytes, stdout: bytes,
stderr: SENTINEL.to_owned(),
expect: Outcome::Boundary, expect: Outcome::Boundary,
}); });
} }
@ -143,18 +164,25 @@ pub fn shared_cases() -> Vec<Case> {
name: "X1/status-126".to_owned(), name: "X1/status-126".to_owned(),
status: 126, status: 126,
stdout: TOKEN_SAFE.to_vec(), stdout: TOKEN_SAFE.to_vec(),
stderr: SENTINEL.to_owned(),
expect: Outcome::Boundary, expect: Outcome::Boundary,
}); });
out.push(Case { out.push(Case {
name: "X3/ignored-text-no-token".to_owned(), name: "X3/ignored-text-no-token".to_owned(),
status: 1, status: 1,
stdout: Vec::new(), stdout: Vec::new(),
// The canonical ignored wording WITHOUT a token: a consumer
// that surfaced untrusted stderr would repeat it.
stderr: CANONICAL_IGNORED.to_owned(),
expect: Outcome::Boundary, expect: Outcome::Boundary,
}); });
out.push(Case { out.push(Case {
name: "X4/stderr-noise".to_owned(), name: "X4/stderr-noise".to_owned(),
status: 0, status: 0,
stdout: TOKEN_SAFE.to_vec(), stdout: TOKEN_SAFE.to_vec(),
// Noise on stderr must not affect classification --- and this
// payload is what distinguishes X4 from 0/V/safe/bare.
stderr: "unrelated chatter on stderr".to_owned(),
expect: Outcome::Safe, expect: Outcome::Safe,
}); });
out out

View File

@ -254,7 +254,9 @@ fn gate_maps_an_unexecutable_helper_to_error_not_ignored() {
/// boundary failure must withhold it. /// boundary failure must withhold it.
#[test] #[test]
fn gate_validates_the_whole_shared_conformance_set() { fn gate_validates_the_whole_shared_conformance_set() {
use common::sigint_conformance::{Outcome, SENTINEL, shared_cases, stub_script}; use common::sigint_conformance::{
CANONICAL_IGNORED, Outcome, SENTINEL, shared_cases, stub_script,
};
let cases = shared_cases(); let cases = shared_cases();
assert_eq!(cases.len(), 45, "the shared set is 45 cases"); assert_eq!(cases.len(), 45, "the shared set is 45 cases");
@ -307,6 +309,12 @@ fn gate_validates_the_whole_shared_conformance_set() {
child's stderr --- this is what separates it from a \ child's stderr --- this is what separates it from a \
validated error, which shares its exit code: {err}" validated error, which shares its exit code: {err}"
); );
assert!(
!err.contains(CANONICAL_IGNORED),
"case {name}: and it must never repeat the canonical \
ignored wording --- X3 emits exactly that on stderr \
with no token: {err}"
);
} }
} }
@ -348,12 +356,24 @@ fn gate_refuses_when_the_capture_directory_cannot_be_created() {
!String::from_utf8_lossy(&out.stdout).contains("[01]"), !String::from_utf8_lossy(&out.stdout).contains("[01]"),
"no stage may run" "no stage may run"
); );
// A8 on this path too: the temporary root is inspected BEFORE its
// RAII drop, and must contain nothing the guard left behind.
let residue: Vec<_> = std::fs::read_dir(root.path())
.expect("read tmpdir")
.filter_map(Result::ok)
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
assert!(
residue.is_empty(),
"a guard that could not create its capture directory must leave \
nothing behind: {residue:?}"
);
} }
/// Each helper arm emits its exact token on stdout. /// Each helper arm emits its exact token on stdout.
#[test] #[test]
fn sigint_helper_emits_the_exact_token_for_each_arm() { fn sigint_helper_emits_the_exact_token_for_each_arm() {
use common::sigint_conformance::{TOKEN_ERROR, TOKEN_SAFE}; use common::sigint_conformance::{TOKEN_ERROR, TOKEN_IGNORED, TOKEN_SAFE};
let safe = Command::new(sigint_helper()).output().expect("run helper"); let safe = Command::new(sigint_helper()).output().expect("run helper");
assert_eq!(safe.status.code(), Some(0)); assert_eq!(safe.status.code(), Some(0));
@ -369,6 +389,16 @@ fn sigint_helper_emits_the_exact_token_for_each_arm() {
.expect("run helper"); .expect("run helper");
assert_eq!(erroring.status.code(), Some(2)); assert_eq!(erroring.status.code(), Some(2));
assert_eq!(erroring.stdout, [TOKEN_ERROR, b"\n"].concat()); assert_eq!(erroring.stdout, [TOKEN_ERROR, b"\n"].concat());
// The ignored arm needs a shell that ignores SIGINT; assert its
// STDOUT, not merely its status and stderr.
let ignored = under_ignored_sigint(&sigint_helper(), &[], &repo_root(), &[]);
assert_eq!(ignored.status.code(), Some(1));
assert_eq!(
ignored.stdout,
[TOKEN_IGNORED, b"\n"].concat(),
"the ignored arm emits exactly its token plus one LF"
);
} }
/// §7c: the helper answers `safe` when `SIGINT` is deliverable. /// §7c: the helper answers `safe` when `SIGINT` is deliverable.

View File

@ -254,7 +254,7 @@ mod crdt {
match (out.status.code(), token_ok) { match (out.status.code(), token_ok) {
(Some(0), true) => Ok(()), (Some(0), true) => Ok(()),
// A VALIDATED verdict: the helper's stderr is the diagnosis. // A VALIDATED verdict: the helper's stderr is the diagnosis.
(Some(1) | Some(2), true) => Err(format!( (Some(1 | 2), true) => Err(format!(
"precondition failed --- this is NOT a teardown defect. \ "precondition failed --- this is NOT a teardown defect. \
(status={status} token={token_state})\n{}", (status={status} token={token_state})\n{}",
String::from_utf8_lossy(&out.stderr).trim_end() String::from_utf8_lossy(&out.stderr).trim_end()
@ -289,7 +289,15 @@ mod crdt {
/// surfaces it, a boundary failure must not. /// surfaces it, a boundary failure must not.
#[test] #[test]
fn rd_precondition_validates_the_whole_conformance_set() { fn rd_precondition_validates_the_whole_conformance_set() {
use crate::common::sigint_conformance::{Outcome, SENTINEL, shared_cases, stub_script}; // `self::` and NOT `crate::`: this file is ALSO compiled as a
// nested module of `gpu_initial_target_acceptance.rs`, where
// `crate::` is the outer test crate and has no `common`.
// `super::` and NOT `crate::`: this file is ALSO compiled as a
// nested module of `gpu_initial_target_acceptance.rs`, where
// `crate::` is the outer test crate and has no `common`.
use super::common::sigint_conformance::{
CANONICAL_IGNORED, Outcome, SENTINEL, shared_cases, stub_script,
};
let dir = tempfile::tempdir().expect("tempdir"); let dir = tempfile::tempdir().expect("tempdir");
let cases = shared_cases(); let cases = shared_cases();
@ -314,6 +322,11 @@ mod crdt {
Outcome::Boundary => { Outcome::Boundary => {
let message = got.expect_err("a boundary refusal"); let message = got.expect_err("a boundary refusal");
assert!(message.contains("boundary error"), "case {name}: {message}"); assert!(message.contains("boundary error"), "case {name}: {message}");
assert!(
!message.contains(CANONICAL_IGNORED),
"case {name}: never repeats the canonical ignored wording \
--- X3 emits exactly that with no token: {message}"
);
assert!( assert!(
!message.contains(SENTINEL), !message.contains(SENTINEL),
"case {name}: a boundary failure must NOT surface the child's \ "case {name}: a boundary failure must NOT surface the child's \