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