Jay Hemnani
·
2026-10-18
1#!/usr/bin/env bats
2# Session lifecycle tests for zmx.
3#
4# These tests create real zmx sessions — forking daemon processes, allocating
5# PTYs, running commands. Without the inherited-FD close fix, every test that
6# calls `zmx run` would hang indefinitely because bats waits for its internal
7# FDs (3+) to close, and the daemon inherits them.
8#
9# If this test suite completes at all, the FD fix is working.
10#
11# All `run` invocations use `-d` (detached) because `zmx run` blocks until
12# the command completes, and sessions outlive their initial command.
13# Note: `-d` must come after the session name (zmx run <name> -d <cmd>).
14
15load test_helper
16
17# ============================================================================
18# Session creation
19# ============================================================================
20
21@test "run: creates a session" {
22 run "$ZMX" run test-create -d echo hello
23 [ "$status" -eq 0 ]
24 [[ "$output" == *"session \"test-create\" created"* ]]
25
26 wait_for_session test-create
27 run "$ZMX" list --short
28 [[ "$output" == "test-create" ]]
29}
30
31@test "run: sends command to existing session" {
32 "$ZMX" run test-send -d echo first
33 wait_for_session test-send
34
35 run "$ZMX" run test-send -d echo second
36 [ "$status" -eq 0 ]
37 [[ "$output" == *"command sent"* ]]
38 # Should NOT say "created" — session already exists
39 [[ "$output" != *"created"* ]]
40}
41
42@test "run: blocking returns after command completes" {
43 run timeout 5 env SHELL=/bin/bash "$ZMX" run test-blocking echo hello
44 [ "$status" -eq 0 ]
45 [[ "$output" == *"session \"test-blocking\" created"* ]]
46}
47
48@test "run: -d after the command is passed to the command" {
49 run timeout 5 "$ZMX" run test-dash-d printf 'arg=%s\n' -d
50 [ "$status" -eq 0 ]
51 [[ "$output" != *"command sent"* ]]
52 wait_for_output test-dash-d "arg=-d"
53}
54
55@test "run: requires a command argument" {
56 run "$ZMX" run test-nocmd
57 [ "$status" -ne 0 ]
58}
59
60@test "run --help shows help without creating a session" {
61 run "$ZMX" run --help
62 [ "$status" -eq 0 ]
63 [[ "$output" == *"Usage:"* ]]
64
65 run "$ZMX" list --short
66 [ "$status" -eq 0 ]
67 [[ "$output" != *"--help"* ]]
68}
69
70@test "subcommands handle --help and -h without side effects" {
71 for cmd in attach send print write kill wait tail history list completions; do
72 run "$ZMX" "$cmd" --help
73 [ "$status" -eq 0 ]
74 [[ "$output" == *"Usage:"* ]]
75
76 run "$ZMX" "$cmd" -h
77 [ "$status" -eq 0 ]
78 [[ "$output" == *"Usage:"* ]]
79 done
80
81 run "$ZMX" list --short
82 [ "$status" -eq 0 ]
83 [[ "$output" != *"--help"* ]]
84 [[ "$output" != *"-h"* ]]
85}
86
87# ============================================================================
88# Send (raw PTY input)
89# ============================================================================
90
91@test "send: does not append CR by default" {
92 "$ZMX" run test-send-raw -d echo ready
93 wait_for_session test-send-raw
94 wait_for_output test-send-raw ready
95
96 # Send text without \r — it should NOT execute as a command
97 run "$ZMX" send test-send-raw "partial-text"
98 [ "$status" -eq 0 ]
99}
100
101@test "send: requires a session name" {
102 run "$ZMX" send
103 [ "$status" -ne 0 ]
104}
105
106@test "send: requires text argument" {
107 "$ZMX" run test-send-notext -d true
108 wait_for_session test-send-notext
109
110 run "$ZMX" send test-send-notext
111 [ "$status" -ne 0 ]
112}
113
114@test "send: accepts piped stdin" {
115 "$ZMX" run test-send-pipe -d echo ready
116 wait_for_session test-send-pipe
117 wait_for_output test-send-pipe ready
118
119 run bash -c 'printf "echo piped-marker-xyz789\r" | "$0" send test-send-pipe' "$ZMX"
120 [ "$status" -eq 0 ]
121
122 wait_for_output test-send-pipe piped-marker-xyz789
123 run "$ZMX" history test-send-pipe
124 [[ "$output" == *"piped-marker-xyz789"* ]]
125}
126
127# ============================================================================
128# Session listing
129# ============================================================================
130
131@test "list: no sessions returns cleanly" {
132 run "$ZMX" list
133 [ "$status" -eq 0 ]
134 [[ "$output" == *"no sessions found"* ]]
135}
136
137@test "ls aliases list" {
138 run "$ZMX" ls
139 [ "$status" -eq 0 ]
140 [[ "$output" == *"no sessions found"* ]]
141}
142
143@test "list: shows session details" {
144 "$ZMX" run test-list -d echo hello
145 wait_for_session test-list
146
147 run "$ZMX" list
148 [ "$status" -eq 0 ]
149 [[ "$output" == *"test-list"* ]]
150 [[ "$output" == *"pid="* ]]
151}
152
153@test "list --short: shows only session names" {
154 "$ZMX" run test-short-a -d true
155 "$ZMX" run test-short-b -d true
156 wait_for_session test-short-a
157 wait_for_session test-short-b
158
159 run "$ZMX" list --short
160 [ "$status" -eq 0 ]
161 [[ "$output" == *"test-short-a"* ]]
162 [[ "$output" == *"test-short-b"* ]]
163}
164
165@test "list --short: empty when no sessions" {
166 run "$ZMX" list --short
167 [ "$status" -eq 0 ]
168 [ -z "$output" ]
169}
170
171# ============================================================================
172# Session kill
173# ============================================================================
174
175@test "kill: removes a session" {
176 "$ZMX" run test-kill -d true
177 wait_for_session test-kill
178
179 run "$ZMX" kill test-kill
180 [ "$status" -eq 0 ]
181 [[ "$output" == *"killed session test-kill"* ]]
182
183 run "$ZMX" list --short
184 [[ "$output" != *"test-kill"* ]]
185}
186
187@test "kill: multiple sessions at once" {
188 "$ZMX" run kill-a -d true
189 "$ZMX" run kill-b -d true
190 wait_for_session kill-a
191 wait_for_session kill-b
192
193 run "$ZMX" kill kill-a kill-b
194 [ "$status" -eq 0 ]
195 [[ "$output" == *"killed session kill-a"* ]]
196 [[ "$output" == *"killed session kill-b"* ]]
197}
198
199@test "kill --force: removes socket file for dead session" {
200 "$ZMX" run test-force -d true
201 wait_for_session test-force
202
203 # Get the daemon PID and kill it directly (simulating a crash)
204 local pid
205 pid=$("$ZMX" list 2>/dev/null | grep test-force | sed 's/.*pid=\([0-9]*\).*/\1/')
206 if [[ -n "$pid" ]]; then
207 kill -9 "$pid" 2>/dev/null || true
208 # Wait for the OS to actually reap the process before relying on
209 # --force to see it as dead.
210 for _ in $(seq 1 50); do
211 kill -0 "$pid" 2>/dev/null || break
212 sleep 0.1
213 done
214 fi
215
216 # Regular kill may fail on the dead session; --force cleans up
217 run "$ZMX" kill --force test-force
218 [ "$status" -eq 0 ]
219}
220
221# ============================================================================
222# Session isolation (ZMX_DIR)
223# ============================================================================
224
225@test "ZMX_DIR isolation: sessions in one dir are invisible to another" {
226 "$ZMX" run test-isolated -d true
227 wait_for_session test-isolated
228
229 # A different ZMX_DIR should see no sessions
230 local other_dir="$BATS_TEST_TMPDIR/zmx-other"
231 mkdir -p "$other_dir"
232 run env ZMX_DIR="$other_dir" "$ZMX" list --short
233 [ "$status" -eq 0 ]
234 [ -z "$output" ]
235}
236
237# ============================================================================
238# History
239# ============================================================================
240
241@test "history: captures session output" {
242 "$ZMX" run test-hist -d echo "bats-marker-xyzzy"
243 wait_for_session test-hist
244 wait_for_output test-hist bats-marker-xyzzy
245
246 run "$ZMX" history test-hist
247 [ "$status" -eq 0 ]
248 [[ "$output" == *"bats-marker-xyzzy"* ]]
249}
250
251# ============================================================================
252# Wait
253# ============================================================================
254
255@test "wait: returns after session command completes" {
256 "$ZMX" run test-wait -d echo done
257 wait_for_session test-wait
258 wait_for_output test-wait done
259
260 # `wait` should return once the command finishes
261 run timeout 10 "$ZMX" wait test-wait
262 [ "$status" -eq 0 ]
263}
264
265# ============================================================================
266# Rapid session churn (stress test for FD handling)
267# ============================================================================
268
269@test "churn: create and kill 5 sessions in sequence" {
270 for i in 1 2 3 4 5; do
271 "$ZMX" run "churn-$i" -d echo "iteration $i"
272 wait_for_session "churn-$i"
273 "$ZMX" kill "churn-$i"
274 done
275
276 run "$ZMX" list --short
277 [ "$status" -eq 0 ]
278 [ -z "$output" ]
279}
280
281
282# ============================================================================
283# Print (inject text into terminal state)
284# ============================================================================
285
286@test "print: text appears in history" {
287 "$ZMX" run test-print-hist -d echo ready
288 wait_for_session test-print-hist
289 wait_for_output test-print-hist ready
290
291 # Caller is responsible for newlines; trailing \r\n ensures the text
292 # lands on its own line before SIGWINCH triggers a prompt redraw.
293 printf "\r\nbats-print-marker-abc123\r\n" | "$ZMX" print test-print-hist
294 wait_for_output test-print-hist bats-print-marker-abc123
295
296 run "$ZMX" history test-print-hist
297 [ "$status" -eq 0 ]
298 [[ "$output" == *"bats-print-marker-abc123"* ]]
299}
300
301@test "print: requires a session name" {
302 run "$ZMX" print
303 [ "$status" -ne 0 ]
304}
305
306@test "run: long command line does not truncate history when creating session" {
307 local longcmd="echo 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"
308 run "$ZMX" run test-long-cmd -d "$longcmd"
309 [ "$status" -eq 0 ]
310 wait_for_session test-long-cmd
311 wait_for_output test-long-cmd 12345678901234567890
312 run "$ZMX" history test-long-cmd
313 [[ "$output" != *"<1234567890"* ]]
314 [[ "$output" == *"12345678901234567890"* ]]
315}
316
317
318# ============================================================================
319# Write
320# ============================================================================
321
322@test "write: a file larger than the PTY input buffer arrives whole" {
323 local src="$BATS_TEST_TMPDIR/src.bin" dst="$BATS_TEST_TMPDIR/dst.bin" i=0
324 head -c 400000 /dev/urandom > "$src"
325
326 run timeout 10 "$ZMX" run test-write-large echo write-ready-marker
327 [ "$status" -eq 0 ]
328 wait_for_output test-write-large write-ready-marker
329
330 run bash -c 'timeout 10 "$0" write test-write-large "$1" < "$2"' "$ZMX" "$dst" "$src"
331 [ "$status" -eq 0 ]
332
333 while (( i < 300 )) && ! cmp -s "$src" "$dst"; do
334 sleep 0.1
335 (( i++ )) || true
336 done
337 cmp "$src" "$dst"
338}