Skip to main content

max / makenotwork

pom: give the ssh test run a total timeout, not just ConnectTimeout TestsConfig.timeout_secs is documented as 'max seconds before killing the test command,' but was only wired to SSH ConnectTimeout — which bounds the handshake, so a connected-then-hung remote command ran forever (fuzz-2026-07-06). Wrap the run in tokio::time::timeout(timeout_secs) with kill_on_drop so the child is reaped on elapse, and surface a timed-out TestRun. Honors the field's contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-07 17:09 UTC
Commit: 1a0b49e5a0cd2cf248e80834c457529bb36b7f0d
Parent: 14b8ada
1 file changed, +33 insertions, -5 deletions
@@ -54,7 +54,13 @@ pub async fn run_tests(
54 54 cmd_str.push_str(f);
55 55 }
56 56
57 - let result = Command::new("ssh")
57 + // `timeout_secs` is documented as "max seconds before killing the test
58 + // command," but was only wired to SSH ConnectTimeout — which bounds the
59 + // handshake, not a connected-then-hung remote command (fuzz-2026-07-06). Wrap
60 + // the whole run in a total timeout and `kill_on_drop` so the ssh child is
61 + // reaped when it elapses, honoring the field's contract.
62 + let mut command = Command::new("ssh");
63 + command
58 64 .arg("-o")
59 65 .arg("BatchMode=yes")
60 66 .arg("-o")
@@ -62,14 +68,36 @@ pub async fn run_tests(
62 68 .arg(&config.ssh)
63 69 .arg("--")
64 70 .arg(&cmd_str)
65 - .output()
66 - .await;
71 + .kill_on_drop(true);
72 +
73 + let total_timeout = std::time::Duration::from_secs(config.timeout_secs.max(1));
74 + let result = tokio::time::timeout(total_timeout, command.output()).await;
67 75
68 76 let finished_at = chrono::Utc::now().to_rfc3339();
69 77 let duration_secs = start.elapsed().as_secs() as i64;
70 78
71 79 match result {
72 - Ok(output) => {
80 + Err(_elapsed) => TestRun {
81 + id: None,
82 + target: target_name.to_string(),
83 + started_at,
84 + finished_at: Some(finished_at),
85 + duration_secs: Some(duration_secs),
86 + exit_code: None,
87 + passed: false,
88 + summary: TestSummary {
89 + steps: vec![],
90 + total_passed: None,
91 + total_failed: None,
92 + details: vec![],
93 + },
94 + raw_output: format!(
95 + "SSH test command timed out after {}s (killed)",
96 + config.timeout_secs
97 + ),
98 + filter: filter.map(String::from),
99 + },
100 + Ok(Ok(output)) => {
73 101 let stdout = String::from_utf8_lossy(&output.stdout);
74 102 let stderr = String::from_utf8_lossy(&output.stderr);
75 103 let raw_output = format!("{stdout}{stderr}");
@@ -91,7 +119,7 @@ pub async fn run_tests(
91 119 filter: filter.map(String::from),
92 120 }
93 121 }
94 - Err(e) => TestRun {
122 + Ok(Err(e)) => TestRun {
95 123 id: None,
96 124 target: target_name.to_string(),
97 125 started_at,