repos / zmx

session persistence for terminal processes
git clone https://github.com/neurosnap/zmx.git

zmx / test
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}