test(sigint): share the conformance vectors and assert the exact branch

Four acceptance gaps, all upheld.

1. Neither suite distinguished a validated refusal from a boundary
   error. Both exit 2 (and both produce Err in Rust), so comparing exit
   codes or is_ok() let a validator that accepts EVERY status-2 pair
   pass the whole matrix --- the precise defect A6c exists to catch.
   Every stub now emits a sentinel on stderr, and an Outcome enum
   (Safe / ValidatedIgnored / ValidatedError / Boundary) is asserted
   branch-exact: a validated verdict must surface the sentinel, a
   boundary failure must withhold it. Verified: mutating the gate to
   accept any status 2 now fails the MATRIX, where before it only
   failed a dedicated row. Each helper arm's exact stdout token is
   asserted as well.

2. The 45-case set was duplicated in both suites and could drift while
   both still reported length 45. It now lives in
   tests/common/sigint_conformance.rs and both validators consume the
   same vectors.

3. A8 was incomplete --- nothing forced capture-directory creation to
   fail. A bounded row points TMPDIR at a missing directory so
   `mktemp -d` fails, asserting boundary error 2, no stage execution and
   no residue; mutating the failure branch to fall through makes it
   fail. Temporary directories are RAII throughout, replacing the
   keep()-plus-manual-cleanup shape.

4. The R-d comment still claimed a shared helper means the consumers
   "can never disagree" and described status-only behaviour. Both were
   withdrawn by revision 13; the comment now points at the shared matrix
   as what actually keeps them in step.

36 gate rows, 16 GPU rows, clippy clean, full gate green.

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 09:15:47 +02:00
parent bc7d776569
commit 8802d6a1a2
No known key found for this signature in database
5 changed files with 334 additions and 275 deletions

View File

@ -288,11 +288,20 @@ from #171 and #215.
`status=`/`token=` on every refusing branch. R-d validates the same
pair from `Command::output()` bytes — no capture files, since only
the shell needs them.
- **Conformance: 45 shared cases, generated as a cross-product, run by
BOTH validators** (`gate_validates_the_whole_shared_conformance_set`,
`rd_precondition_validates_the_whole_conformance_set`), plus Rust's
X2 no-status spawn error = 46. **34 gate rows, 16 GPU rows, full gate
green.**
- **Conformance vectors live in `tests/common/sigint_conformance.rs`**
and are consumed by BOTH validators, so the two copies cannot drift
while each still reports "45 cases". Every stub emits a **sentinel on
stderr**, which is what separates `ValidatedError` from `Boundary` —
they share exit 2, so comparing codes alone let a validator that
accepted every status 2 pass the whole matrix. An `Outcome` enum
(Safe / ValidatedIgnored / ValidatedError / Boundary) is asserted
branch-exact in both suites, and each helper arm's exact stdout token
is asserted too.
- **A8 is complete**: a bounded row points `TMPDIR` at a missing
directory so `mktemp -d` fails, and asserts boundary error 2, no
stage, and no residue. Temporary directories are RAII throughout;
the `keep()`-plus-manual-cleanup shape is gone.
- **36 gate rows, 16 GPU rows, full gate green.**
- **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

View File

@ -25,3 +25,4 @@
pub mod daemon;
pub mod iso;
pub mod pty;
pub mod sigint_conformance;

View File

@ -0,0 +1,161 @@
//! Shared SIGINT-guard conformance vectors
//! (`docs/gpu-probe-sigint-framing.md` §7c).
//!
//! # Why these live here rather than in each suite
//!
//! The contract is that the **shell** consumer (`scripts/gate`) and the
//! **Rust** consumer (R-d's `sigint_diagnosis`) agree on every case.
//! Two independently written copies of the list can drift while both
//! still report "45 cases" — the same-length-different-content
//! divergence this matrix exists to rule out. One generator, two
//! consumers.
/// What a consumer must do with a given `(status, stdout)` pair.
///
/// `ValidatedError` and `Boundary` **both exit 2**, so a test comparing
/// only exit codes cannot separate them — and a validator that accepted
/// *every* status 2 would pass. They are told apart by whether the
/// child's stderr is surfaced: a validated verdict speaks with the
/// helper's voice; a boundary failure must not.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Outcome {
Safe,
ValidatedIgnored,
ValidatedError,
Boundary,
}
pub const TOKEN_SAFE: &[u8] = b"pmacs-sigint-v1:safe";
pub const TOKEN_IGNORED: &[u8] = b"pmacs-sigint-v1:ignored";
pub const TOKEN_ERROR: &[u8] = b"pmacs-sigint-v1:error";
/// Emitted on stderr by **every** stub, so a consumer's output can be
/// searched for it: present ⇒ the child's stderr was surfaced.
pub const SENTINEL: &str = "PMACS-CONFORMANCE-SENTINEL";
pub struct Case {
pub name: String,
pub status: i32,
pub stdout: Vec<u8>,
pub expect: Outcome,
}
/// A `/bin/sh` stub reproducing one case, sentinel included.
#[must_use]
pub fn stub_script(case: &Case) -> String {
let octal = case.stdout.iter().fold(String::new(), |mut acc, b| {
use std::fmt::Write as _;
let _ = write!(acc, "\\{b:03o}");
acc
});
format!(
"#!/bin/sh\nprintf '{octal}'\necho '{SENTINEL}' >&2\nexit {}\n",
case.status
)
}
/// The shared set: ten classes × encodings × three statuses, plus
/// X1/X3/X4. Only the diagonal validates.
///
/// X2 — a spawn error with no status — is deliberately absent: the
/// shell boundary cannot represent it, because an `exec` failure there
/// becomes a status. Rust exercises it separately.
#[must_use]
pub fn shared_cases() -> Vec<Case> {
let toks: [(&str, &[u8]); 3] = [
("safe", TOKEN_SAFE),
("ignored", TOKEN_IGNORED),
("error", TOKEN_ERROR),
];
let mut out = Vec::new();
for (idx, (name, correct)) in toks.iter().enumerate() {
let status = i32::try_from(idx).expect("0..=2");
let diagonal = match status {
0 => Outcome::Safe,
1 => Outcome::ValidatedIgnored,
_ => Outcome::ValidatedError,
};
let mut lf = correct.to_vec();
lf.push(b'\n');
out.push(Case {
name: format!("{status}/V/{name}/bare"),
status,
stdout: correct.to_vec(),
expect: diagonal,
});
out.push(Case {
name: format!("{status}/V/{name}/lf"),
status,
stdout: lf,
expect: diagonal,
});
for (other, bytes) in &toks {
if other == name {
continue;
}
let mut olf = bytes.to_vec();
olf.push(b'\n');
out.push(Case {
name: format!("{status}/M/{other}/bare"),
status,
stdout: bytes.to_vec(),
expect: Outcome::Boundary,
});
out.push(Case {
name: format!("{status}/M/{other}/lf"),
status,
stdout: olf,
expect: Outcome::Boundary,
});
}
let mut leading = vec![b'\n'];
leading.extend_from_slice(correct);
let mut extra = correct.to_vec();
extra.extend_from_slice(b"\n\n");
let mut spaces = b" ".to_vec();
spaces.extend_from_slice(correct);
spaces.push(b' ');
let mut crlf = correct.to_vec();
crlf.extend_from_slice(b"\r\n");
let mut doubled = correct.to_vec();
doubled.extend_from_slice(correct);
let mut nul = correct.to_vec();
nul.push(0);
for (cls, bytes) in [
("E/empty", Vec::new()),
("U/unknown", b"pmacs-sigint-v2:safe".to_vec()),
("L/leading-lf", leading),
("X/extra-lf", extra),
("S/spaces", spaces),
("C/crlf", crlf),
("D/doubled", doubled),
("N/nul", nul),
] {
out.push(Case {
name: format!("{status}/{cls}"),
status,
stdout: bytes,
expect: Outcome::Boundary,
});
}
}
out.push(Case {
name: "X1/status-126".to_owned(),
status: 126,
stdout: TOKEN_SAFE.to_vec(),
expect: Outcome::Boundary,
});
out.push(Case {
name: "X3/ignored-text-no-token".to_owned(),
status: 1,
stdout: Vec::new(),
expect: Outcome::Boundary,
});
out.push(Case {
name: "X4/stderr-noise".to_owned(),
status: 0,
stdout: TOKEN_SAFE.to_vec(),
expect: Outcome::Safe,
});
out
}

View File

@ -28,6 +28,8 @@
//! `~/build/pmacs-gate-targets`, which matters most for the prune
//! tests — a prune bug is unrecoverable.
mod common;
use std::path::{Path, PathBuf};
use std::process::Command;
@ -243,24 +245,23 @@ fn gate_maps_an_unexecutable_helper_to_error_not_ignored() {
);
}
/// One stub-worktree gate run against a controlled `(status, stdout)`
/// pair, returning the gate's own exit code and stderr.
/// A6/A6b/A6c, shell consumer: the shared vectors, asserting the exact
/// branch rather than only the exit code.
///
/// The gate is the **shell consumer** of the pair ABI. Driving it
/// through a stub worktree exercises its real code path — including
/// the `set -eu` handling and the capture directory — against inputs
/// no real helper would produce.
fn gate_sees_pair(status: i32, stdout: &[u8], stderr_line: &str) -> (Option<i32>, String, PathBuf) {
/// `ValidatedError` and `Boundary` both exit 2, so comparing codes
/// alone would let a validator that accepts every status 2 pass. The
/// stubs emit a sentinel on stderr; a validated verdict surfaces it, a
/// boundary failure must withhold it.
#[test]
fn gate_validates_the_whole_shared_conformance_set() {
use common::sigint_conformance::{Outcome, SENTINEL, shared_cases, stub_script};
let cases = shared_cases();
assert_eq!(cases.len(), 45, "the shared set is 45 cases");
for case in cases {
let root = tempfile::tempdir().expect("tempdir");
let octal = stdout.iter().fold(String::new(), |mut acc, b| {
use std::fmt::Write as _;
let _ = write!(acc, "\\{b:03o}");
acc
});
let repo = gate_with_stub_helper(
&format!("#!/bin/sh\nprintf '{octal}'\n{stderr_line}exit {status}\n"),
true,
);
let repo = gate_with_stub_helper(&stub_script(&case), true);
let out = Command::new(repo.path().join("scripts/gate"))
.arg("--self-test")
.current_dir(repo.path())
@ -268,151 +269,108 @@ fn gate_sees_pair(status: i32, stdout: &[u8], stderr_line: &str) -> (Option<i32>
.env("TMPDIR", root.path())
.output()
.expect("run the stub-worktree gate");
let code = out.status.code();
let err = String::from_utf8_lossy(&out.stderr).into_owned();
// Keep `root` alive for the residue check by returning its path
// after leaking the handle: the caller inspects it, then it is
// dropped with the TempDir at end of test.
let path = root.keep();
(code, err, path)
}
let err = String::from_utf8_lossy(&out.stderr);
let name = &case.name;
/// A6/A6b/A6c, shell consumer: the same 45 shared conformance cases the
/// Rust validator runs, so the two cannot diverge.
///
/// X2 is absent by construction — a shell `exec` failure becomes a
/// shell status, so the shell boundary cannot present a status-less
/// spawn error.
#[test]
#[allow(
clippy::too_many_lines,
reason = "the bulk is the generated case list, which is data; splitting \
it would put the cases and the expectations they encode in \
different places"
)]
fn gate_validates_the_whole_shared_conformance_set() {
const SAFE: &[u8] = b"pmacs-sigint-v1:safe";
const IGNORED: &[u8] = b"pmacs-sigint-v1:ignored";
const ERROR: &[u8] = b"pmacs-sigint-v1:error";
let toks: [(&str, &[u8]); 3] = [("safe", SAFE), ("ignored", IGNORED), ("error", ERROR)];
let mut cases: Vec<(String, i32, Vec<u8>, &str, bool)> = Vec::new();
for (idx, (name, correct)) in toks.iter().enumerate() {
let status = i32::try_from(idx).expect("0..=2");
let mut lf = correct.to_vec();
lf.push(b'\n');
// Only status 0's correct token is `safe`; the correct token at
// 1 and 2 is a VALIDATED verdict, which refuses with its own
// status rather than continuing.
let safe_here = status == 0;
cases.push((
format!("{status}/V/{name}/bare"),
status,
correct.to_vec(),
"",
safe_here,
));
cases.push((format!("{status}/V/{name}/lf"), status, lf, "", safe_here));
for (other, bytes) in &toks {
if other == name {
continue;
}
let mut olf = bytes.to_vec();
olf.push(b'\n');
cases.push((
format!("{status}/M/{other}/bare"),
status,
bytes.to_vec(),
"",
false,
));
cases.push((format!("{status}/M/{other}/lf"), status, olf, "", false));
}
let mut leading = vec![b'\n'];
leading.extend_from_slice(correct);
let mut extra = correct.to_vec();
extra.extend_from_slice(b"\n\n");
let mut spaces = b" ".to_vec();
spaces.extend_from_slice(correct);
spaces.push(b' ');
let mut crlf = correct.to_vec();
crlf.extend_from_slice(b"\r\n");
let mut doubled = correct.to_vec();
doubled.extend_from_slice(correct);
let mut nul = correct.to_vec();
nul.push(0);
for (cls, bytes) in [
("E/empty", Vec::new()),
("U/unknown", b"pmacs-sigint-v2:safe".to_vec()),
("L/leading-lf", leading),
("X/extra-lf", extra),
("S/spaces", spaces),
("C/crlf", crlf),
("D/doubled", doubled),
("N/nul", nul),
] {
cases.push((format!("{status}/{cls}"), status, bytes, "", false));
}
}
cases.push(("X1/status-126".to_owned(), 126, SAFE.to_vec(), "", false));
cases.push((
"X3/ignored-text-no-token".to_owned(),
1,
Vec::new(),
"echo 'pmacs: SIGINT is ignored; run this command with SIGINT deliverable' >&2\n",
false,
));
cases.push((
"X4/stderr-noise".to_owned(),
0,
SAFE.to_vec(),
"echo 'chatter on stderr' >&2\n",
true,
));
assert_eq!(cases.len(), 45, "the shared set is 45 cases");
for (name, status, stdout, stderr_line, expect_pass) in cases {
let (code, err, dir) = gate_sees_pair(status, &stdout, stderr_line);
if expect_pass {
// `safe` continues into the self-test plan, which exits
// non-zero ON PURPOSE — what matters is that the guard did
// not refuse.
assert!(
match case.expect {
Outcome::Safe => assert!(
!err.contains("REFUSING TO RUN"),
"case {name}: the guard must not refuse a validated safe pair: {err}"
);
} else {
"case {name}: a validated safe pair must continue: {err}"
),
Outcome::ValidatedIgnored => {
assert_eq!(out.status.code(), Some(1), "case {name}: {err}");
assert!(
err.contains("REFUSING TO RUN"),
"case {name}: the guard must refuse: {err}"
err.contains(SENTINEL),
"case {name}: a validated verdict surfaces the helper's \
stderr: {err}"
);
let validated = name.contains("/V/") && (status == 1 || status == 2);
if validated {
assert_eq!(
code,
Some(status),
"case {name}: validated verdicts pass their status through"
);
} else {
assert_eq!(code, Some(2), "case {name}: boundary errors map to 2");
assert!(err.contains("token=valid"), "case {name}: {err}");
}
Outcome::ValidatedError => {
assert_eq!(out.status.code(), Some(2), "case {name}: {err}");
assert!(
!err.contains("SIGINT is ignored"),
"case {name}: a boundary failure must not speak with the \
err.contains(SENTINEL),
"case {name}: a validated error also speaks with the \
helper's voice: {err}"
);
assert!(err.contains("token=valid"), "case {name}: {err}");
}
Outcome::Boundary => {
assert_eq!(out.status.code(), Some(2), "case {name}: {err}");
assert!(
err.contains("SIGINT guard boundary error"),
"case {name}: {err}"
);
assert!(
!err.contains(SENTINEL),
"case {name}: a boundary failure must NOT surface the \
child's stderr --- this is what separates it from a \
validated error, which shares its exit code: {err}"
);
}
}
// A8: no capture directory survives, on any path.
let residue: Vec<_> = std::fs::read_dir(&dir)
// A8: no capture directory survives, on any path. `root` is RAII
// --- it is dropped at the end of this iteration.
let residue: Vec<_> = std::fs::read_dir(root.path())
.expect("read tmpdir")
.filter_map(Result::ok)
.filter(|e| e.file_name().to_string_lossy().starts_with("pmacs-sigint."))
.collect();
assert!(residue.is_empty(), "case {name}: capture residue survived");
std::fs::remove_dir_all(&dir).ok();
}
}
/// A8: the guard cannot create its capture directory.
///
/// Bounded --- it never reaches a stage. `TMPDIR` points at a path that
/// does not exist, so `mktemp -d` fails and the guard must refuse
/// before running the helper at all.
#[test]
fn gate_refuses_when_the_capture_directory_cannot_be_created() {
let root = tempfile::tempdir().expect("tempdir");
let repo = gate_with_stub_helper("#!/bin/sh\nprintf 'pmacs-sigint-v1:safe'\nexit 0\n", true);
let out = Command::new(repo.path().join("scripts/gate"))
.arg("--self-test")
.current_dir(repo.path())
.env("PMACS_GATE_TARGET_ROOT", root.path())
.env("TMPDIR", root.path().join("absent-directory"))
.output()
.expect("run the stub-worktree gate");
let err = String::from_utf8_lossy(&out.stderr);
assert_eq!(out.status.code(), Some(2), "boundary error: {err}");
assert!(
err.contains("capture directory"),
"the failure names what could not be created: {err}"
);
assert!(err.contains("no stage has run"), "and no stage ran: {err}");
assert!(
!String::from_utf8_lossy(&out.stdout).contains("[01]"),
"no stage may run"
);
}
/// 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};
let safe = Command::new(sigint_helper()).output().expect("run helper");
assert_eq!(safe.status.code(), Some(0));
assert_eq!(
safe.stdout,
[TOKEN_SAFE, b"\n"].concat(),
"the safe arm emits exactly its token plus one LF"
);
let erroring = Command::new(sigint_helper())
.env("PATH", "")
.output()
.expect("run helper");
assert_eq!(erroring.status.code(), Some(2));
assert_eq!(erroring.stdout, [TOKEN_ERROR, b"\n"].concat());
}
/// §7c: the helper answers `safe` when `SIGINT` is deliverable.
#[test]
fn sigint_helper_reports_safe_when_the_signal_is_deliverable() {

View File

@ -2,6 +2,8 @@
#![cfg(unix)]
mod common;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
@ -183,13 +185,17 @@ mod crdt {
/// teardown defect that is not there; that misreading cost nine
/// framing revisions (§4c).
///
/// The **same checked-in helper the gate uses** owns the
/// classification and the wording, so the two can never disagree
/// about what "ignored" means. This consumer does not re-derive
/// either: it proceeds only on exit 0 and otherwise panics with the
/// helper's own stderr. A helper that cannot be executed is an
/// `error` at this boundary, never evidence that `SIGINT` is
/// ignored.
/// Both consumers use the **same checked-in helper**, but that alone
/// no longer makes them agree: each validates the
/// `(status, token)` pair independently, in a different language.
/// Revision 12's "they can never disagree" is withdrawn, and the
/// shared matrix in `tests/common/sigint_conformance.rs` replaces
/// it — both validators run the same vectors.
///
/// This consumer proceeds only on a validated `(0, safe)` pair. On a
/// **boundary** failure the helper's stderr is untrusted and is
/// withheld; only a validated verdict speaks with the helper's
/// voice.
const SIGINT_TOKEN_SAFE: &[u8] = b"pmacs-sigint-v1:safe";
const SIGINT_TOKEN_IGNORED: &[u8] = b"pmacs-sigint-v1:ignored";
const SIGINT_TOKEN_ERROR: &[u8] = b"pmacs-sigint-v1:error";
@ -274,131 +280,55 @@ mod crdt {
}
}
/// The shared conformance set, generated rather than listed
/// (§7c): ten classes, two encodings where applicable, three
/// statuses = 42, plus X1/X3/X4. Only the diagonal validates.
fn sigint_conformance_cases() -> Vec<(String, i32, Vec<u8>, bool)> {
let toks: [(&str, &[u8]); 3] = [
("safe", SIGINT_TOKEN_SAFE),
("ignored", SIGINT_TOKEN_IGNORED),
("error", SIGINT_TOKEN_ERROR),
];
let mut out = Vec::new();
for (idx, (name, correct)) in toks.iter().enumerate() {
let status = i32::try_from(idx).expect("0..=2");
let ok = status == 0;
let mut lf = correct.to_vec();
lf.push(b'\n');
out.push((
format!("{status}/V/{name}/bare"),
status,
correct.to_vec(),
ok,
));
out.push((format!("{status}/V/{name}/lf"), status, lf, ok));
// Every OTHER valid token, both encodings: enumerated, not
// sampled, since sampling one leaves half untested.
for (other, bytes) in &toks {
if other == name {
continue;
}
let mut olf = bytes.to_vec();
olf.push(b'\n');
out.push((
format!("{status}/M/{other}/bare"),
status,
bytes.to_vec(),
false,
));
out.push((format!("{status}/M/{other}/lf"), status, olf, false));
}
let mut leading = vec![b'\n'];
leading.extend_from_slice(correct);
let mut extra = correct.to_vec();
extra.extend_from_slice(b"\n\n");
let mut spaces = b" ".to_vec();
spaces.extend_from_slice(correct);
spaces.push(b' ');
let mut crlf = correct.to_vec();
crlf.extend_from_slice(b"\r\n");
let mut doubled = correct.to_vec();
doubled.extend_from_slice(correct);
let mut nul = correct.to_vec();
nul.push(0);
for (cls, bytes) in [
("E/empty", Vec::new()),
("U/unknown", b"pmacs-sigint-v2:safe".to_vec()),
("L/leading-lf", leading),
("X/extra-lf", extra),
("S/spaces", spaces),
("C/crlf", crlf),
("D/doubled", doubled),
("N/nul", nul),
] {
out.push((format!("{status}/{cls}"), status, bytes, false));
}
}
out.push((
"X1/status-126".to_owned(),
126,
SIGINT_TOKEN_SAFE.to_vec(),
false,
));
out.push(("X3/ignored-text-no-token".to_owned(), 1, Vec::new(), false));
out.push((
"X4/stderr-noise".to_owned(),
0,
SIGINT_TOKEN_SAFE.to_vec(),
true,
));
out
}
/// A6/A6b/A6c, R-d consumer: the whole shared set, plus Rust's X2.
/// A6/A6b/A6c, R-d consumer: the shared vectors, asserting the exact
/// branch.
///
/// `ValidatedError` and `Boundary` both produce `Err`, so comparing
/// `is_ok()` alone would let a validator that accepts every status 2
/// pass. The stubs emit a sentinel on stderr; a validated verdict
/// 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};
let dir = tempfile::tempdir().expect("tempdir");
let cases = sigint_conformance_cases();
let cases = shared_cases();
assert_eq!(cases.len(), 45, "the shared set is 45 cases");
for (name, status, stdout, expect_ok) in cases {
let path = dir.path().join(name.replace('/', "_"));
let extra = if name.starts_with("X3") {
"echo 'pmacs: SIGINT is ignored; run this command with SIGINT deliverable' >&2\n"
} else if name.starts_with("X4") {
"echo 'chatter on stderr' >&2\n"
} else {
""
};
let octal = stdout.iter().fold(String::new(), |mut acc, b| {
use std::fmt::Write as _;
let _ = write!(acc, "\\{b:03o}");
acc
});
fs::write(
&path,
format!("#!/bin/sh\nprintf '{octal}'\n{extra}exit {status}\n"),
)
.expect("write stub");
for case in cases {
let path = dir.path().join(case.name.replace('/', "_"));
fs::write(&path, stub_script(&case)).expect("write stub");
fs::set_permissions(&path, fs::Permissions::from_mode(0o755)).expect("chmod");
let got = sigint_diagnosis(&path);
assert_eq!(got.is_ok(), expect_ok, "case {name}: got {got:?}");
if let Err(message) = got {
let validated = name.contains("/V/") && (status == 1 || status == 2);
let name = &case.name;
match case.expect {
Outcome::Safe => assert!(got.is_ok(), "case {name}: {got:?}"),
Outcome::ValidatedIgnored | Outcome::ValidatedError => {
let message = got.expect_err("a validated refusal");
assert!(
validated || !message.contains("SIGINT is ignored"),
"case {name}: a boundary failure must not speak with the \
helper's voice: {message}"
message.contains(SENTINEL),
"case {name}: a validated verdict surfaces the helper's stderr: {message}"
);
assert!(message.contains("token=valid"), "case {name}: {message}");
}
Outcome::Boundary => {
let message = got.expect_err("a boundary refusal");
assert!(message.contains("boundary error"), "case {name}: {message}");
assert!(
!message.contains(SENTINEL),
"case {name}: a boundary failure must NOT surface the child's \
stderr --- that is what separates it from a validated error, \
which shares its outcome type: {message}"
);
}
}
}
// X2 — Rust only: a shell exec failure becomes a status, so the
// shell consumer cannot present this input at all.
// X2 --- Rust only: a shell exec failure becomes a status, so the
// shell consumer cannot present a status-less spawn error.
let message = sigint_diagnosis(&dir.path().join("absent")).expect_err("must not validate");
assert!(
message.contains("status=unavailable") && !message.contains("SIGINT is ignored"),
message.contains("status=unavailable") && !message.contains(SENTINEL),
"X2: {message}"
);
}