Runner failure regression: a stdout-only report is kept with an empty stderr
The vili failure: node --test wrote only to stdout, stderr was empty, the step exited 1. Pin the cap at the literal 32 and the Debug impls that keep tails out of text. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7ef31ee699
commit
54a75ef9b0
2 changed files with 61 additions and 3 deletions
|
|
@ -16568,5 +16568,53 @@ processes 9
|
|||
assert!(format!("{error:#}").contains("starting Docker runner"), "{error:#}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The 2026-10-25 vili failure: `node --test` reported on stdout alone,
|
||||
/// stderr stayed empty (the runner's /tmp is a noexec tmpfs) and the step
|
||||
/// exited 1. The old capture dropped stdout and printed an empty message.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn a_step_that_reports_only_on_stdout_keeps_that_report_and_its_name() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let driver = fake_docker(
|
||||
temp.path(),
|
||||
&format!("echo 'not ok 1 - daemon {RUNNER_CANARY}'\necho '# fail 1'\nexit 1"),
|
||||
)?;
|
||||
let error = run_step(&driver, temp.path(), "test-daemon").expect_err("the step passed");
|
||||
// The error names the step and carries no output.
|
||||
let text = format!("{error:#}");
|
||||
assert!(text.contains("runner step test-daemon on runner rust exited with exit status 1"), "{text}");
|
||||
assert!(!text.contains(RUNNER_CANARY) && !text.contains("not ok"), "{text}");
|
||||
// The evidence keeps the stdout tail; the empty stderr is a recorded 0.
|
||||
let failure = error
|
||||
.downcast_ref::<RunnerStepFailure>()
|
||||
.context("the failure is not typed")?;
|
||||
let store = temp.path().join("runner-evidence.cc");
|
||||
let record = crate::runner_evidence::RunnerFailureRecord::from_failure(
|
||||
"tx-vili",
|
||||
"up-vili",
|
||||
"vili",
|
||||
failure,
|
||||
1,
|
||||
);
|
||||
crate::runner_evidence::record(&store, &record)?;
|
||||
let kept = crate::runner_evidence::for_transaction(&store, "tx-vili")?.context("no evidence kept")?;
|
||||
assert_eq!(kept.subject_id, "test-daemon");
|
||||
assert_eq!(kept.exit_code, Some(1));
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&kept.stdout_tail),
|
||||
format!("not ok 1 - daemon {RUNNER_CANARY}\n# fail 1\n")
|
||||
);
|
||||
assert_eq!(kept.stdout_total_bytes, kept.stdout_tail.len() as u64);
|
||||
assert!(kept.stderr_tail.is_empty());
|
||||
assert_eq!(kept.stderr_total_bytes, 0);
|
||||
let rendered = crate::runner_evidence::render(&kept).join("\n");
|
||||
assert!(rendered.contains("stderr (last 0 of 0 bytes)"), "{rendered}");
|
||||
assert!(rendered.contains(&format!("| not ok 1 - daemon {RUNNER_CANARY}")), "{rendered}");
|
||||
// Debug names the failure and prints no tail.
|
||||
let debug = format!("{failure:?}");
|
||||
assert!(debug.starts_with("RunnerStepFailure") && !debug.contains("not ok"), "{debug}");
|
||||
Ok(())
|
||||
}
|
||||
// END runner-failure-evidence tests
|
||||
}
|
||||
|
|
|
|||
|
|
@ -314,11 +314,12 @@ mod tests {
|
|||
fn the_evidence_store_keeps_the_newest_32() -> Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let path = runner_evidence_store_path(&temp.path().join("control.cc"));
|
||||
for index in 0..=RUNNER_EVIDENCE_RECORDS {
|
||||
// The literal, not the constant: the spec's bound is 32.
|
||||
for index in 0..=32 {
|
||||
record(&path, &record_at(&format!("tx-{index:02}"), 1000 + index as u64))?;
|
||||
}
|
||||
let kept: Vec<_> = snapshot(&path)?.1.into_iter().map(|stored| stored.transaction_id).collect();
|
||||
assert_eq!(kept.len(), RUNNER_EVIDENCE_RECORDS);
|
||||
assert_eq!(kept.len(), 32);
|
||||
assert!(!kept.contains(&"tx-00".to_owned()), "the oldest survived");
|
||||
assert!(kept.contains(&"tx-32".to_owned()), "the newest was evicted");
|
||||
assert!(kept.contains(&"tx-01".to_owned()), "the second oldest was evicted early");
|
||||
|
|
@ -328,7 +329,7 @@ mod tests {
|
|||
again.stdout_total_bytes = 10;
|
||||
record(&path, &again)?;
|
||||
let kept = snapshot(&path)?.1;
|
||||
assert_eq!(kept.len(), RUNNER_EVIDENCE_RECORDS);
|
||||
assert_eq!(kept.len(), 32);
|
||||
assert!(kept.iter().any(|stored| stored.transaction_id == "tx-01"));
|
||||
let stored = for_transaction(&path, "tx-05")?.context("rewritten record is gone")?;
|
||||
assert_eq!(stored.stdout_tail, b"rewritten\n");
|
||||
|
|
@ -407,6 +408,15 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_names_the_record_and_prints_no_tail() {
|
||||
let mut stored = record_at("tx-a", 1);
|
||||
stored.stdout_tail = b"SECRET-OUTPUT".to_vec();
|
||||
stored.stdout_total_bytes = 13;
|
||||
let debug = format!("{stored:?}");
|
||||
assert!(debug.contains("tx-a") && !debug.contains("SECRET"), "{debug}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_tail_renders_only_its_header() {
|
||||
let mut stored = record_at("tx-a", 1);
|
||||
|
|
|
|||
Loading…
Reference in a new issue