repos / zmx

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

commit
d63cf5b
parent
e70e8b2
author
x1f9
date
2026-04-02 17:32:46 -0400 EDT
test: add BATS session lifecycle tests and mise.toml

14 tests covering session creation, listing, killing, history,
wait, ZMX_DIR isolation, and rapid churn. These tests create real
daemon processes — without the inherited-FD close fix, every test
that calls `zmx run` would hang indefinitely.

mise.toml declares bats 1.13.0 as a dev dependency, establishing
the pattern for managing project tooling.
3 files changed,  +226, -0
A mise.toml
+2, -0
1@@ -0,0 +1,2 @@
2+[tools]
3+bats = "1.13.0"
A test/session.bats
+184, -0
  1@@ -0,0 +1,184 @@
  2+#!/usr/bin/env bats
  3+# Session lifecycle tests for zmx.
  4+#
  5+# These tests create real zmx sessions — forking daemon processes, allocating
  6+# PTYs, running commands. Without the inherited-FD close fix, every test that
  7+# calls `zmx run` would hang indefinitely because bats waits for its internal
  8+# FDs (3+) to close, and the daemon inherits them.
  9+#
 10+# If this test suite completes at all, the FD fix is working.
 11+
 12+load test_helper
 13+
 14+# ============================================================================
 15+# Session creation
 16+# ============================================================================
 17+
 18+@test "run: creates a session" {
 19+  run "$ZMX" run test-create echo hello
 20+  [ "$status" -eq 0 ]
 21+  [[ "$output" == *"session \"test-create\" created"* ]]
 22+
 23+  wait_for_session test-create
 24+  run "$ZMX" list --short
 25+  [[ "$output" == "test-create" ]]
 26+}
 27+
 28+@test "run: sends command to existing session" {
 29+  "$ZMX" run test-send echo first
 30+  wait_for_session test-send
 31+
 32+  run "$ZMX" run test-send echo second
 33+  [ "$status" -eq 0 ]
 34+  [[ "$output" == *"command sent"* ]]
 35+  # Should NOT say "created" — session already exists
 36+  [[ "$output" != *"created"* ]]
 37+}
 38+
 39+@test "run: requires a command argument" {
 40+  run "$ZMX" run test-nocmd
 41+  [ "$status" -ne 0 ]
 42+}
 43+
 44+# ============================================================================
 45+# Session listing
 46+# ============================================================================
 47+
 48+@test "list: no sessions returns cleanly" {
 49+  run "$ZMX" list
 50+  [ "$status" -eq 0 ]
 51+  [[ "$output" == *"no sessions found"* ]]
 52+}
 53+
 54+@test "list: shows session details" {
 55+  "$ZMX" run test-list echo hello
 56+  wait_for_session test-list
 57+
 58+  run "$ZMX" list
 59+  [ "$status" -eq 0 ]
 60+  [[ "$output" == *"test-list"* ]]
 61+  [[ "$output" == *"pid="* ]]
 62+  [[ "$output" == *"cmd=echo hello"* ]]
 63+}
 64+
 65+@test "list --short: shows only session names" {
 66+  "$ZMX" run test-short-a true
 67+  "$ZMX" run test-short-b true
 68+  wait_for_session test-short-a
 69+  wait_for_session test-short-b
 70+
 71+  run "$ZMX" list --short
 72+  [ "$status" -eq 0 ]
 73+  [[ "$output" == *"test-short-a"* ]]
 74+  [[ "$output" == *"test-short-b"* ]]
 75+}
 76+
 77+@test "list --short: empty when no sessions" {
 78+  run "$ZMX" list --short
 79+  [ "$status" -eq 0 ]
 80+  [ -z "$output" ]
 81+}
 82+
 83+# ============================================================================
 84+# Session kill
 85+# ============================================================================
 86+
 87+@test "kill: removes a session" {
 88+  "$ZMX" run test-kill true
 89+  wait_for_session test-kill
 90+
 91+  run "$ZMX" kill test-kill
 92+  [ "$status" -eq 0 ]
 93+  [[ "$output" == *"killed session test-kill"* ]]
 94+
 95+  run "$ZMX" list --short
 96+  [[ "$output" != *"test-kill"* ]]
 97+}
 98+
 99+@test "kill: multiple sessions at once" {
100+  "$ZMX" run kill-a true
101+  "$ZMX" run kill-b true
102+  wait_for_session kill-a
103+  wait_for_session kill-b
104+
105+  run "$ZMX" kill kill-a kill-b
106+  [ "$status" -eq 0 ]
107+  [[ "$output" == *"killed session kill-a"* ]]
108+  [[ "$output" == *"killed session kill-b"* ]]
109+}
110+
111+@test "kill --force: removes socket file for dead session" {
112+  "$ZMX" run test-force true
113+  wait_for_session test-force
114+
115+  # Get the daemon PID and kill it directly (simulating a crash)
116+  local pid
117+  pid=$("$ZMX" list 2>/dev/null | grep test-force | sed 's/.*pid=\([0-9]*\).*/\1/')
118+  if [[ -n "$pid" ]]; then
119+    kill -9 "$pid" 2>/dev/null || true
120+    sleep 0.5
121+  fi
122+
123+  # Regular kill may fail on the dead session; --force cleans up
124+  run "$ZMX" kill --force test-force
125+  [ "$status" -eq 0 ]
126+}
127+
128+# ============================================================================
129+# Session isolation (ZMX_DIR)
130+# ============================================================================
131+
132+@test "ZMX_DIR isolation: sessions in one dir are invisible to another" {
133+  "$ZMX" run test-isolated true
134+  wait_for_session test-isolated
135+
136+  # A different ZMX_DIR should see no sessions
137+  local other_dir="$BATS_TEST_TMPDIR/zmx-other"
138+  mkdir -p "$other_dir"
139+  run env ZMX_DIR="$other_dir" "$ZMX" list --short
140+  [ "$status" -eq 0 ]
141+  [ -z "$output" ]
142+}
143+
144+# ============================================================================
145+# History
146+# ============================================================================
147+
148+@test "history: captures session output" {
149+  "$ZMX" run test-hist echo "bats-marker-xyzzy"
150+  wait_for_session test-hist
151+  sleep 0.5  # give the command time to produce output
152+
153+  run "$ZMX" history test-hist
154+  [ "$status" -eq 0 ]
155+  [[ "$output" == *"bats-marker-xyzzy"* ]]
156+}
157+
158+# ============================================================================
159+# Wait
160+# ============================================================================
161+
162+@test "wait: returns after session command completes" {
163+  "$ZMX" run test-wait echo done
164+  wait_for_session test-wait
165+
166+  # `wait` should return once the command finishes
167+  run timeout 10 "$ZMX" wait test-wait
168+  [ "$status" -eq 0 ]
169+}
170+
171+# ============================================================================
172+# Rapid session churn (stress test for FD handling)
173+# ============================================================================
174+
175+@test "churn: create and kill 5 sessions in sequence" {
176+  for i in 1 2 3 4 5; do
177+    "$ZMX" run "churn-$i" echo "iteration $i"
178+    wait_for_session "churn-$i"
179+    "$ZMX" kill "churn-$i"
180+  done
181+
182+  run "$ZMX" list --short
183+  [ "$status" -eq 0 ]
184+  [ -z "$output" ]
185+}
A test/test_helper.bash
+40, -0
 1@@ -0,0 +1,40 @@
 2+# test_helper.bash — shared setup/teardown for zmx BATS tests
 3+
 4+REPO_DIR="$(cd "$BATS_TEST_DIRNAME/.." && pwd)"
 5+
 6+setup() {
 7+  # Build once per test suite (skips if already built)
 8+  if [[ ! -x "$REPO_DIR/zig-out/bin/zmx" ]]; then
 9+    cd "$REPO_DIR" && zig build
10+  fi
11+  ZMX="$REPO_DIR/zig-out/bin/zmx"
12+
13+  # Isolate socket dir so tests don't interfere with real sessions or each other
14+  export ZMX_DIR="$BATS_TEST_TMPDIR/zmx-sockets"
15+  mkdir -p "$ZMX_DIR"
16+}
17+
18+teardown() {
19+  # Kill any sessions created during this test
20+  if [[ -d "$ZMX_DIR" ]]; then
21+    local sessions
22+    sessions=$("$ZMX" list --short 2>/dev/null) || true
23+    if [[ -n "$sessions" ]]; then
24+      echo "$sessions" | xargs "$ZMX" kill --force 2>/dev/null || true
25+    fi
26+  fi
27+}
28+
29+# Helper: wait for a session to appear in list (up to N seconds)
30+wait_for_session() {
31+  local name="$1" timeout="${2:-5}" i=0
32+  while (( i < timeout * 10 )); do
33+    if "$ZMX" list --short 2>/dev/null | grep -qx "$name"; then
34+      return 0
35+    fi
36+    sleep 0.1
37+    (( i++ )) || true
38+  done
39+  echo "Timed out waiting for session '$name'" >&2
40+  return 1
41+}