Commit d63cf5b

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