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
`/home/jeans/Repos/personal/pmacs-probe-sigint`. Recover with
`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
owns a guard-local capture dir, keeps `|| status=$?` under `set -eu`,
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
finished about 30 seconds before `bc7d776` was committed, so it
describes the implementation tree rather than a committed head.
- **A4 mutations, each biting:** accepting any status 2 regardless of
token → `gate_refuses_on_helper_error_…`; surfacing child stderr on a
boundary failure → the conformance row **and** that row; token to
stderr → `sigint_helper_reports_safe_…`. Noted for the record: the
first mutation is caught by the dedicated row rather than by the
conformance set, because the set's boundary cases mostly have empty
stderr and so cannot see which branch produced the exit 2.
- **A4 mutations, each biting the MATRIX now**, not a dedicated row:
accepting any status 2 regardless of token, and surfacing child
stderr on a boundary failure, both fail
`gate_validates_the_whole_shared_conformance_set`; token-to-stderr
fails `sigint_helper_reports_safe_…`. An earlier entry said the
status-2 mutation was caught only by the dedicated row — that was
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
is the **pre-declared A7 portability finding**, not an environment
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.
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 name: String,
pub status: i32,
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,
}
@ -49,8 +59,8 @@ pub fn stub_script(case: &Case) -> String {
acc
});
format!(
"#!/bin/sh\nprintf '{octal}'\necho '{SENTINEL}' >&2\nexit {}\n",
case.status
"#!/bin/sh\nprintf '{octal}'\necho '{}' >&2\nexit {}\n",
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
/// becomes a status. Rust exercises it separately.
#[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> {
let toks: [(&str, &[u8]); 3] = [
("safe", TOKEN_SAFE),
@ -81,12 +97,14 @@ pub fn shared_cases() -> Vec<Case> {
name: format!("{status}/V/{name}/bare"),
status,
stdout: correct.to_vec(),
stderr: SENTINEL.to_owned(),
expect: diagonal,
});
out.push(Case {
name: format!("{status}/V/{name}/lf"),
status,
stdout: lf,
stderr: SENTINEL.to_owned(),
expect: diagonal,
});
for (other, bytes) in &toks {
@ -99,12 +117,14 @@ pub fn shared_cases() -> Vec<Case> {
name: format!("{status}/M/{other}/bare"),
status,
stdout: bytes.to_vec(),
stderr: SENTINEL.to_owned(),
expect: Outcome::Boundary,
});
out.push(Case {
name: format!("{status}/M/{other}/lf"),
status,
stdout: olf,
stderr: SENTINEL.to_owned(),
expect: Outcome::Boundary,
});
}
@ -135,6 +155,7 @@ pub fn shared_cases() -> Vec<Case> {
name: format!("{status}/{cls}"),
status,
stdout: bytes,
stderr: SENTINEL.to_owned(),
expect: Outcome::Boundary,
});
}
@ -143,18 +164,25 @@ pub fn shared_cases() -> Vec<Case> {
name: "X1/status-126".to_owned(),
status: 126,
stdout: TOKEN_SAFE.to_vec(),
stderr: SENTINEL.to_owned(),
expect: Outcome::Boundary,
});
out.push(Case {
name: "X3/ignored-text-no-token".to_owned(),
status: 1,
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,
});
out.push(Case {
name: "X4/stderr-noise".to_owned(),
status: 0,
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,
});
out

View File

@ -254,7 +254,9 @@ fn gate_maps_an_unexecutable_helper_to_error_not_ignored() {
/// boundary failure must withhold it.
#[test]
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();
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 \
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]"),
"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.
#[test]
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");
assert_eq!(safe.status.code(), Some(0));
@ -369,6 +389,16 @@ fn sigint_helper_emits_the_exact_token_for_each_arm() {
.expect("run helper");
assert_eq!(erroring.status.code(), Some(2));
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.

View File

@ -254,7 +254,7 @@ mod crdt {
match (out.status.code(), token_ok) {
(Some(0), true) => Ok(()),
// 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. \
(status={status} token={token_state})\n{}",
String::from_utf8_lossy(&out.stderr).trim_end()
@ -289,7 +289,15 @@ mod crdt {
/// surfaces it, a boundary failure must not.
#[test]
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 cases = shared_cases();
@ -314,6 +322,11 @@ mod crdt {
Outcome::Boundary => {
let message = got.expect_err("a boundary refusal");
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!(
!message.contains(SENTINEL),
"case {name}: a boundary failure must NOT surface the child's \