Commit ce52eb7

Eric Bower  ·  2026-07-17 15:15:12 -0400 EDT
parent fbf900c
fix: flaky bats tests
5 files changed,  +65, -19
+22, -2
 1@@ -2565,6 +2565,12 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
 2     var vt_stream = term.vtStream();
 3     defer vt_stream.deinit();
 4 
 5+    // Carries the tail of the previous PTY read so the task-exit marker
 6+    // search below can see across a read() boundary. Sized to comfortably
 7+    // hold "ZMX_TASK_COMPLETED:" (19 bytes) plus a u8 exit code and CRLF.
 8+    var marker_carry: [32]u8 = undefined;
 9+    var marker_carry_len: usize = 0;
10+
11     daemon_loop: while (daemon.running) {
12         poll_fds.clearRetainingCapacity();
13 
14@@ -2675,9 +2681,17 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
15                         util.respondToDeviceAttributes(daemon.alloc, &daemon.pty_write_buf, buf[0..n]);
16                     }
17 
18-                    // In run mode, scan output for exit code marker
19+                    // In run mode, scan output for exit code marker. The marker
20+                    // can straddle two PTY reads (more likely under a throttled
21+                    // scheduler, e.g. containers), so prepend the tail carried
22+                    // over from the previous read before searching.
23                     if (daemon.is_task_mode and daemon.task_exit_code == null) {
24-                        if (util.findTaskExitMarker(buf[0..n])) |exit_code| {
25+                        var scan_buf: [marker_carry.len + buf.len]u8 = undefined;
26+                        @memcpy(scan_buf[0..marker_carry_len], marker_carry[0..marker_carry_len]);
27+                        @memcpy(scan_buf[marker_carry_len..][0..n], buf[0..n]);
28+                        const scan_len = marker_carry_len + n;
29+
30+                        if (util.findTaskExitMarker(scan_buf[0..scan_len])) |exit_code| {
31                             daemon.task_exit_code = exit_code;
32                             daemon.task_ended_at = @intCast(std.time.timestamp());
33 
34@@ -2689,6 +2703,12 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
35                                 c.has_pending_output = true;
36                             }
37                         }
38+
39+                        marker_carry_len = @min(marker_carry.len, scan_len);
40+                        @memcpy(
41+                            marker_carry[0..marker_carry_len],
42+                            scan_buf[scan_len - marker_carry_len .. scan_len],
43+                        );
44                     }
45 
46                     // Broadcast data to all clients.
+9, -4
 1@@ -328,8 +328,14 @@ test "rewritePromptRedraw: embedded in larger output" {
 2 pub fn findTaskExitMarker(output: []const u8) ?u8 {
 3     const marker = "ZMX_TASK_COMPLETED:";
 4 
 5-    // Search for marker in output
 6-    if (std.mem.indexOf(u8, output, marker)) |idx| {
 7+    // The command line is echoed back by the PTY (canonical mode) before the
 8+    // shell evaluates it, so the *first* occurrence of the marker in the
 9+    // output is often the literal, unexpanded "ZMX_TASK_COMPLETED:$?" from
10+    // the echo, not the real "ZMX_TASK_COMPLETED:<code>" written once the
11+    // shell actually runs it. Keep scanning past unparseable occurrences
12+    // instead of giving up on the first one.
13+    var search_start: usize = 0;
14+    while (std.mem.indexOfPos(u8, output, search_start, marker)) |idx| {
15         const after_marker = output[idx + marker.len ..];
16 
17         // Find the exit code number and newline
18@@ -344,8 +350,7 @@ pub fn findTaskExitMarker(output: []const u8) ?u8 {
19         if (std.fmt.parseInt(u8, exit_code_str, 10)) |exit_code| {
20             return exit_code;
21         } else |_| {
22-            std.log.warn("failed to parse task exit code from: {s}", .{exit_code_str});
23-            return null;
24+            search_start = idx + marker.len;
25         }
26     }
27 
+13, -8
 1@@ -84,7 +84,7 @@ load test_helper
 2 @test "send: does not append CR by default" {
 3   "$ZMX" run test-send-raw -d echo ready
 4   wait_for_session test-send-raw
 5-  sleep 0.5
 6+  wait_for_output test-send-raw ready
 7 
 8   # Send text without \r — it should NOT execute as a command
 9   run "$ZMX" send test-send-raw "partial-text"
10@@ -107,12 +107,12 @@ load test_helper
11 @test "send: accepts piped stdin" {
12   "$ZMX" run test-send-pipe -d echo ready
13   wait_for_session test-send-pipe
14-  sleep 0.5
15+  wait_for_output test-send-pipe ready
16 
17   run bash -c 'printf "echo piped-marker-xyz789\r" | "$0" send test-send-pipe' "$ZMX"
18   [ "$status" -eq 0 ]
19 
20-  sleep 0.5
21+  wait_for_output test-send-pipe piped-marker-xyz789
22   run "$ZMX" history test-send-pipe
23   [[ "$output" == *"piped-marker-xyz789"* ]]
24 }
25@@ -198,7 +198,12 @@ load test_helper
26   pid=$("$ZMX" list 2>/dev/null | grep test-force | sed 's/.*pid=\([0-9]*\).*/\1/')
27   if [[ -n "$pid" ]]; then
28     kill -9 "$pid" 2>/dev/null || true
29-    sleep 0.5
30+    # Wait for the OS to actually reap the process before relying on
31+    # --force to see it as dead.
32+    for _ in $(seq 1 50); do
33+      kill -0 "$pid" 2>/dev/null || break
34+      sleep 0.1
35+    done
36   fi
37 
38   # Regular kill may fail on the dead session; --force cleans up
39@@ -229,7 +234,7 @@ load test_helper
40 @test "history: captures session output" {
41   "$ZMX" run test-hist -d echo "bats-marker-xyzzy"
42   wait_for_session test-hist
43-  sleep 0.5  # give the command time to produce output
44+  wait_for_output test-hist bats-marker-xyzzy
45 
46   run "$ZMX" history test-hist
47   [ "$status" -eq 0 ]
48@@ -243,7 +248,7 @@ load test_helper
49 @test "wait: returns after session command completes" {
50   "$ZMX" run test-wait -d echo done
51   wait_for_session test-wait
52-  sleep 1  # give the command time to finish
53+  wait_for_output test-wait done
54 
55   # `wait` should return once the command finishes
56   run timeout 10 "$ZMX" wait test-wait
57@@ -274,12 +279,12 @@ load test_helper
58 @test "print: text appears in history" {
59   "$ZMX" run test-print-hist -d echo ready
60   wait_for_session test-print-hist
61-  sleep 0.3
62+  wait_for_output test-print-hist ready
63 
64   # Caller is responsible for newlines; trailing \r\n ensures the text
65   # lands on its own line before SIGWINCH triggers a prompt redraw.
66   printf "\r\nbats-print-marker-abc123\r\n" | "$ZMX" print test-print-hist
67-  sleep 0.3
68+  wait_for_output test-print-hist bats-print-marker-abc123
69 
70   run "$ZMX" history test-print-hist
71   [ "$status" -eq 0 ]
+5, -5
 1@@ -15,7 +15,7 @@ load test_helper
 2   run bash -c 'printf "echo stdin-marker-abc123\n" | timeout 10 "$0" run test-stdin-basic' "$ZMX"
 3   [ "$status" -eq 0 ]
 4 
 5-  sleep 0.3
 6+  wait_for_output test-stdin-basic stdin-marker-abc123
 7   run "$ZMX" history test-stdin-basic
 8   [[ "$output" == *"stdin-marker-abc123"* ]]
 9 }
10@@ -24,7 +24,7 @@ load test_helper
11   run bash -c 'printf "echo '\''hello \$USER \$(whoami) \\\"double\\\" ; # comment'\''\n" | timeout 10 "$0" run test-stdin-special' "$ZMX"
12   [ "$status" -eq 0 ]
13 
14-  sleep 0.3
15+  wait_for_output test-stdin-special '$USER'
16   run "$ZMX" history test-stdin-special
17   [[ "$output" == *'$USER'* ]]
18   [[ "$output" == *'$(whoami)'* ]]
19@@ -36,7 +36,7 @@ load test_helper
20   run bash -c 'printf "%s" "$1" | timeout 10 "$0" run test-stdin-multi' "$ZMX" "$script"
21   [ "$status" -eq 0 ]
22 
23-  sleep 0.5
24+  wait_for_output test-stdin-multi line-three-ccc
25   run "$ZMX" history test-stdin-multi
26   [[ "$output" == *"line-one-aaa"* ]]
27   [[ "$output" == *"line-two-bbb"* ]]
28@@ -51,7 +51,7 @@ load test_helper
29   run bash -c 'printf "%s" "$1" | timeout 10 "$0" run test-stdin-heredoc' "$ZMX" "$script"
30   [ "$status" -eq 0 ]
31 
32-  sleep 0.5
33+  wait_for_output test-stdin-heredoc '$variables that should not expand'
34   run "$ZMX" history test-stdin-heredoc
35   [[ "$output" == *'$variables that should not expand'* ]]
36 }
37@@ -60,7 +60,7 @@ load test_helper
38   run timeout 10 env SHELL=/bin/bash "$ZMX" run test-args-only echo args-only-marker-999
39   [ "$status" -eq 0 ]
40 
41-  sleep 0.3
42+  wait_for_output test-args-only args-only-marker-999
43   run "$ZMX" history test-args-only
44   [[ "$output" == *"args-only-marker-999"* ]]
45 }
+16, -0
 1@@ -38,3 +38,19 @@ wait_for_session() {
 2   echo "Timed out waiting for session '$name'" >&2
 3   return 1
 4 }
 5+
 6+# Helper: wait for a marker to appear in a session's history (up to N seconds).
 7+# Use this instead of a fixed sleep whenever a test needs the daemon to have
 8+# processed and buffered PTY output before it checks/sends more.
 9+wait_for_output() {
10+  local name="$1" marker="$2" timeout="${3:-5}" i=0
11+  while (( i < timeout * 10 )); do
12+    if "$ZMX" history "$name" 2>/dev/null | grep -qF "$marker"; then
13+      return 0
14+    fi
15+    sleep 0.1
16+    (( i++ )) || true
17+  done
18+  echo "Timed out waiting for output '$marker' in session '$name'" >&2
19+  return 1
20+}