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 bybc7d776. 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 at72da24a, 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:
parent
9321975197
commit
fb8a904923
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
Loading…
Reference in New Issue