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