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:
Meta Vens 2026-10-25 02:36:34 +01:00
commit 54a75ef9b0
2 changed files with 61 additions and 3 deletions

View file

@ -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
}

View file

@ -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);