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