diff --git a/src/drivers.rs b/src/drivers.rs index 1c8d067..fdd4d3a 100644 --- a/src/drivers.rs +++ b/src/drivers.rs @@ -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::() + .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 } diff --git a/src/runner_evidence.rs b/src/runner_evidence.rs index 9eeab4c..d77961f 100644 --- a/src/runner_evidence.rs +++ b/src/runner_evidence.rs @@ -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);