main zmx / test / session.bats
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}