Amir B.
·
2026-05-16
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: requires a command argument" {
49 run "$ZMX" run test-nocmd
50 [ "$status" -ne 0 ]
51}
52
53@test "run --help shows help without creating a session" {
54 run "$ZMX" run --help
55 [ "$status" -eq 0 ]
56 [[ "$output" == *"Usage:"* ]]
57
58 run "$ZMX" list --short
59 [ "$status" -eq 0 ]
60 [[ "$output" != *"--help"* ]]
61}
62
63@test "subcommands handle --help and -h without side effects" {
64 for cmd in attach send print write kill wait tail history list completions; do
65 run "$ZMX" "$cmd" --help
66 [ "$status" -eq 0 ]
67 [[ "$output" == *"Usage:"* ]]
68
69 run "$ZMX" "$cmd" -h
70 [ "$status" -eq 0 ]
71 [[ "$output" == *"Usage:"* ]]
72 done
73
74 run "$ZMX" list --short
75 [ "$status" -eq 0 ]
76 [[ "$output" != *"--help"* ]]
77 [[ "$output" != *"-h"* ]]
78}
79
80# ============================================================================
81# Send (raw PTY input)
82# ============================================================================
83
84@test "send: does not append CR by default" {
85 "$ZMX" run test-send-raw -d echo ready
86 wait_for_session test-send-raw
87 sleep 0.5
88
89 # Send text without \r — it should NOT execute as a command
90 run "$ZMX" send test-send-raw "partial-text"
91 [ "$status" -eq 0 ]
92}
93
94@test "send: requires a session name" {
95 run "$ZMX" send
96 [ "$status" -ne 0 ]
97}
98
99@test "send: requires text argument" {
100 "$ZMX" run test-send-notext -d true
101 wait_for_session test-send-notext
102
103 run "$ZMX" send test-send-notext
104 [ "$status" -ne 0 ]
105}
106
107@test "send: accepts piped stdin" {
108 "$ZMX" run test-send-pipe -d echo ready
109 wait_for_session test-send-pipe
110 sleep 0.5
111
112 run bash -c 'printf "echo piped-marker-xyz789\r" | "$0" send test-send-pipe' "$ZMX"
113 [ "$status" -eq 0 ]
114
115 sleep 0.5
116 run "$ZMX" history test-send-pipe
117 [[ "$output" == *"piped-marker-xyz789"* ]]
118}
119
120# ============================================================================
121# Session listing
122# ============================================================================
123
124@test "list: no sessions returns cleanly" {
125 run "$ZMX" list
126 [ "$status" -eq 0 ]
127 [[ "$output" == *"no sessions found"* ]]
128}
129
130@test "ls aliases list" {
131 run "$ZMX" ls
132 [ "$status" -eq 0 ]
133 [[ "$output" == *"no sessions found"* ]]
134}
135
136@test "list: shows session details" {
137 "$ZMX" run test-list -d echo hello
138 wait_for_session test-list
139
140 run "$ZMX" list
141 [ "$status" -eq 0 ]
142 [[ "$output" == *"test-list"* ]]
143 [[ "$output" == *"pid="* ]]
144}
145
146@test "list --short: shows only session names" {
147 "$ZMX" run test-short-a -d true
148 "$ZMX" run test-short-b -d true
149 wait_for_session test-short-a
150 wait_for_session test-short-b
151
152 run "$ZMX" list --short
153 [ "$status" -eq 0 ]
154 [[ "$output" == *"test-short-a"* ]]
155 [[ "$output" == *"test-short-b"* ]]
156}
157
158@test "list --short: empty when no sessions" {
159 run "$ZMX" list --short
160 [ "$status" -eq 0 ]
161 [ -z "$output" ]
162}
163
164# ============================================================================
165# Session kill
166# ============================================================================
167
168@test "kill: removes a session" {
169 "$ZMX" run test-kill -d true
170 wait_for_session test-kill
171
172 run "$ZMX" kill test-kill
173 [ "$status" -eq 0 ]
174 [[ "$output" == *"killed session test-kill"* ]]
175
176 run "$ZMX" list --short
177 [[ "$output" != *"test-kill"* ]]
178}
179
180@test "kill: multiple sessions at once" {
181 "$ZMX" run kill-a -d true
182 "$ZMX" run kill-b -d true
183 wait_for_session kill-a
184 wait_for_session kill-b
185
186 run "$ZMX" kill kill-a kill-b
187 [ "$status" -eq 0 ]
188 [[ "$output" == *"killed session kill-a"* ]]
189 [[ "$output" == *"killed session kill-b"* ]]
190}
191
192@test "kill --force: removes socket file for dead session" {
193 "$ZMX" run test-force -d true
194 wait_for_session test-force
195
196 # Get the daemon PID and kill it directly (simulating a crash)
197 local pid
198 pid=$("$ZMX" list 2>/dev/null | grep test-force | sed 's/.*pid=\([0-9]*\).*/\1/')
199 if [[ -n "$pid" ]]; then
200 kill -9 "$pid" 2>/dev/null || true
201 sleep 0.5
202 fi
203
204 # Regular kill may fail on the dead session; --force cleans up
205 run "$ZMX" kill --force test-force
206 [ "$status" -eq 0 ]
207}
208
209# ============================================================================
210# Session isolation (ZMX_DIR)
211# ============================================================================
212
213@test "ZMX_DIR isolation: sessions in one dir are invisible to another" {
214 "$ZMX" run test-isolated -d true
215 wait_for_session test-isolated
216
217 # A different ZMX_DIR should see no sessions
218 local other_dir="$BATS_TEST_TMPDIR/zmx-other"
219 mkdir -p "$other_dir"
220 run env ZMX_DIR="$other_dir" "$ZMX" list --short
221 [ "$status" -eq 0 ]
222 [ -z "$output" ]
223}
224
225# ============================================================================
226# History
227# ============================================================================
228
229@test "history: captures session output" {
230 "$ZMX" run test-hist -d echo "bats-marker-xyzzy"
231 wait_for_session test-hist
232 sleep 0.5 # give the command time to produce output
233
234 run "$ZMX" history test-hist
235 [ "$status" -eq 0 ]
236 [[ "$output" == *"bats-marker-xyzzy"* ]]
237}
238
239# ============================================================================
240# Wait
241# ============================================================================
242
243@test "wait: returns after session command completes" {
244 "$ZMX" run test-wait -d echo done
245 wait_for_session test-wait
246 sleep 1 # give the command time to finish
247
248 # `wait` should return once the command finishes
249 run timeout 10 "$ZMX" wait test-wait
250 [ "$status" -eq 0 ]
251}
252
253# ============================================================================
254# Rapid session churn (stress test for FD handling)
255# ============================================================================
256
257@test "churn: create and kill 5 sessions in sequence" {
258 for i in 1 2 3 4 5; do
259 "$ZMX" run "churn-$i" -d echo "iteration $i"
260 wait_for_session "churn-$i"
261 "$ZMX" kill "churn-$i"
262 done
263
264 run "$ZMX" list --short
265 [ "$status" -eq 0 ]
266 [ -z "$output" ]
267}
268
269
270# ============================================================================
271# Print (inject text into terminal state)
272# ============================================================================
273
274@test "print: text appears in history" {
275 "$ZMX" run test-print-hist -d echo ready
276 wait_for_session test-print-hist
277 sleep 0.3
278
279 # Caller is responsible for newlines; trailing \r\n ensures the text
280 # lands on its own line before SIGWINCH triggers a prompt redraw.
281 printf "\r\nbats-print-marker-abc123\r\n" | "$ZMX" print test-print-hist
282 sleep 0.3
283
284 run "$ZMX" history test-print-hist
285 [ "$status" -eq 0 ]
286 [[ "$output" == *"bats-print-marker-abc123"* ]]
287}
288
289@test "print: requires a session name" {
290 run "$ZMX" print
291 [ "$status" -ne 0 ]
292}