@seflless/ghosttown 1.5.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -1
- package/package.json +2 -1
- package/scripts/cli-publish.js +11 -2
- package/src/cli.js +2430 -170
- package/src/cli.test.sh +605 -0
- package/src/cli.test.ts +75 -0
package/src/cli.test.sh
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Ghosttown CLI Test Suite
|
|
5
|
+
# =============================================================================
|
|
6
|
+
#
|
|
7
|
+
# This test file validates the session management functionality of the
|
|
8
|
+
# ghosttown CLI. It covers session creation, naming, renaming, and the
|
|
9
|
+
# stable ID system that enables rename-resilient URLs.
|
|
10
|
+
#
|
|
11
|
+
# IMPORTANT FOR AI AGENTS:
|
|
12
|
+
# ------------------------
|
|
13
|
+
# When making changes to session management code, run this test suite to
|
|
14
|
+
# verify your changes don't break existing functionality. If you add new
|
|
15
|
+
# features, add corresponding tests here.
|
|
16
|
+
#
|
|
17
|
+
# Run with: bash src/cli.test.sh
|
|
18
|
+
#
|
|
19
|
+
# The tests use tmux directly to verify behavior, so tmux must be installed.
|
|
20
|
+
# Tests clean up after themselves by killing any gt-test-* sessions created.
|
|
21
|
+
#
|
|
22
|
+
# =============================================================================
|
|
23
|
+
|
|
24
|
+
set -e
|
|
25
|
+
|
|
26
|
+
# Colors for output
|
|
27
|
+
RED='\033[0;31m'
|
|
28
|
+
GREEN='\033[0;32m'
|
|
29
|
+
YELLOW='\033[1;33m'
|
|
30
|
+
CYAN='\033[0;36m'
|
|
31
|
+
NC='\033[0m' # No Color
|
|
32
|
+
|
|
33
|
+
# Test counters
|
|
34
|
+
TESTS_RUN=0
|
|
35
|
+
TESTS_PASSED=0
|
|
36
|
+
TESTS_FAILED=0
|
|
37
|
+
|
|
38
|
+
# Prefix for test sessions (to avoid conflicts with real sessions)
|
|
39
|
+
TEST_PREFIX="gt-test-"
|
|
40
|
+
|
|
41
|
+
# =============================================================================
|
|
42
|
+
# Test Utilities
|
|
43
|
+
# =============================================================================
|
|
44
|
+
|
|
45
|
+
# Print a test description
|
|
46
|
+
test_start() {
|
|
47
|
+
TESTS_RUN=$((TESTS_RUN + 1))
|
|
48
|
+
echo -e "${CYAN}TEST $TESTS_RUN:${NC} $1"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Mark test as passed
|
|
52
|
+
test_pass() {
|
|
53
|
+
TESTS_PASSED=$((TESTS_PASSED + 1))
|
|
54
|
+
echo -e " ${GREEN}✓ PASSED${NC}"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Mark test as failed
|
|
58
|
+
test_fail() {
|
|
59
|
+
TESTS_FAILED=$((TESTS_FAILED + 1))
|
|
60
|
+
echo -e " ${RED}✗ FAILED:${NC} $1"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Assert that a condition is true
|
|
64
|
+
assert() {
|
|
65
|
+
if eval "$1"; then
|
|
66
|
+
return 0
|
|
67
|
+
else
|
|
68
|
+
return 1
|
|
69
|
+
fi
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Assert string contains substring
|
|
73
|
+
assert_contains() {
|
|
74
|
+
if [[ "$1" == *"$2"* ]]; then
|
|
75
|
+
return 0
|
|
76
|
+
else
|
|
77
|
+
return 1
|
|
78
|
+
fi
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Assert string matches regex
|
|
82
|
+
assert_matches() {
|
|
83
|
+
if [[ "$1" =~ $2 ]]; then
|
|
84
|
+
return 0
|
|
85
|
+
else
|
|
86
|
+
return 1
|
|
87
|
+
fi
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Clean up any test sessions
|
|
91
|
+
cleanup_test_sessions() {
|
|
92
|
+
# Kill all sessions starting with gt-test-
|
|
93
|
+
tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^gt-test-" | while read -r session; do
|
|
94
|
+
tmux kill-session -t "$session" 2>/dev/null || true
|
|
95
|
+
done
|
|
96
|
+
# Also clean up temp sessions
|
|
97
|
+
tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^gt-temp-" | while read -r session; do
|
|
98
|
+
tmux kill-session -t "$session" 2>/dev/null || true
|
|
99
|
+
done
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Create a test session directly via tmux (bypassing CLI for setup)
|
|
103
|
+
create_test_session() {
|
|
104
|
+
local name="$1"
|
|
105
|
+
tmux new-session -d -s "$name" -x 80 -y 24
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Get session info in format: session_name|session_id
|
|
109
|
+
get_session_info() {
|
|
110
|
+
local name="$1"
|
|
111
|
+
tmux list-sessions -F "#{session_name}|#{session_id}" 2>/dev/null | grep "^$name|" || echo ""
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# =============================================================================
|
|
115
|
+
# Test: Session Name Validation
|
|
116
|
+
# =============================================================================
|
|
117
|
+
|
|
118
|
+
test_session_name_validation() {
|
|
119
|
+
test_start "Session name validation - valid names"
|
|
120
|
+
|
|
121
|
+
# Valid names should pass (we test the validation logic indirectly)
|
|
122
|
+
# Valid: alphanumeric, hyphens, underscores
|
|
123
|
+
local valid_names=("my-session" "test_session" "Session123" "a" "a-b_c")
|
|
124
|
+
local all_valid=true
|
|
125
|
+
|
|
126
|
+
for name in "${valid_names[@]}"; do
|
|
127
|
+
# Names that are purely numeric should fail, others should pass
|
|
128
|
+
if [[ "$name" =~ ^[0-9]+$ ]]; then
|
|
129
|
+
all_valid=false
|
|
130
|
+
break
|
|
131
|
+
fi
|
|
132
|
+
# Names with invalid chars should fail
|
|
133
|
+
if [[ ! "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
134
|
+
all_valid=false
|
|
135
|
+
break
|
|
136
|
+
fi
|
|
137
|
+
done
|
|
138
|
+
|
|
139
|
+
if $all_valid; then
|
|
140
|
+
test_pass
|
|
141
|
+
else
|
|
142
|
+
test_fail "Valid names were rejected"
|
|
143
|
+
fi
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
test_session_name_validation_invalid() {
|
|
147
|
+
test_start "Session name validation - invalid names rejected"
|
|
148
|
+
|
|
149
|
+
# These should all be invalid:
|
|
150
|
+
# - Purely numeric (conflicts with auto-generated IDs)
|
|
151
|
+
# - Contains spaces or special chars
|
|
152
|
+
# - Empty
|
|
153
|
+
local invalid_names=("123" "my session" "test@session" "")
|
|
154
|
+
local all_rejected=true
|
|
155
|
+
|
|
156
|
+
for name in "${invalid_names[@]}"; do
|
|
157
|
+
# Purely numeric should be rejected
|
|
158
|
+
if [[ "$name" =~ ^[0-9]+$ ]]; then
|
|
159
|
+
continue # This is correctly invalid
|
|
160
|
+
fi
|
|
161
|
+
# Empty should be rejected
|
|
162
|
+
if [[ -z "$name" ]]; then
|
|
163
|
+
continue # This is correctly invalid
|
|
164
|
+
fi
|
|
165
|
+
# Invalid chars should be rejected
|
|
166
|
+
if [[ ! "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
167
|
+
continue # This is correctly invalid
|
|
168
|
+
fi
|
|
169
|
+
# If we get here, the name passed validation but shouldn't have
|
|
170
|
+
all_rejected=false
|
|
171
|
+
done
|
|
172
|
+
|
|
173
|
+
if $all_rejected; then
|
|
174
|
+
test_pass
|
|
175
|
+
else
|
|
176
|
+
test_fail "Invalid names were accepted"
|
|
177
|
+
fi
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# =============================================================================
|
|
181
|
+
# Test: Session Naming Format
|
|
182
|
+
# =============================================================================
|
|
183
|
+
#
|
|
184
|
+
# BEHAVIOR: Sessions use format gt-<stableId>-<displayName>
|
|
185
|
+
# - stableId: Numeric part of tmux's internal $N session ID
|
|
186
|
+
# - displayName: User-facing name (custom or auto-generated number)
|
|
187
|
+
#
|
|
188
|
+
# This enables rename-resilient URLs: even if displayName changes,
|
|
189
|
+
# the stableId remains constant.
|
|
190
|
+
#
|
|
191
|
+
# =============================================================================
|
|
192
|
+
|
|
193
|
+
test_session_naming_format() {
|
|
194
|
+
test_start "Session naming format - gt-<stableId>-<displayName>"
|
|
195
|
+
|
|
196
|
+
cleanup_test_sessions
|
|
197
|
+
|
|
198
|
+
# Create a session via the API simulation
|
|
199
|
+
# We'll create it manually with the expected format
|
|
200
|
+
local temp_name="gt-temp-$$"
|
|
201
|
+
tmux new-session -d -s "$temp_name" -x 80 -y 24
|
|
202
|
+
|
|
203
|
+
# Get the session ID
|
|
204
|
+
local session_id
|
|
205
|
+
session_id=$(tmux list-sessions -F "#{session_name}|#{session_id}" | grep "^$temp_name|" | cut -d'|' -f2 | tr -d '$')
|
|
206
|
+
|
|
207
|
+
# Rename to final format
|
|
208
|
+
local final_name="gt-${session_id}-testname"
|
|
209
|
+
tmux rename-session -t "$temp_name" "$final_name"
|
|
210
|
+
|
|
211
|
+
# Verify the session exists with correct format
|
|
212
|
+
if tmux has-session -t "$final_name" 2>/dev/null; then
|
|
213
|
+
test_pass
|
|
214
|
+
tmux kill-session -t "$final_name" 2>/dev/null || true
|
|
215
|
+
else
|
|
216
|
+
test_fail "Session not created with expected format"
|
|
217
|
+
fi
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
test_parse_session_name() {
|
|
221
|
+
test_start "Parse session name - extracts stableId and displayName"
|
|
222
|
+
|
|
223
|
+
# Test parsing logic
|
|
224
|
+
# Format: gt-<stableId>-<displayName>
|
|
225
|
+
local test_name="gt-42-my-cool-session"
|
|
226
|
+
|
|
227
|
+
# Extract stableId (first number after gt-)
|
|
228
|
+
local stable_id
|
|
229
|
+
if [[ "$test_name" =~ ^gt-([0-9]+)-(.+)$ ]]; then
|
|
230
|
+
stable_id="${BASH_REMATCH[1]}"
|
|
231
|
+
local display_name="${BASH_REMATCH[2]}"
|
|
232
|
+
|
|
233
|
+
if [[ "$stable_id" == "42" ]] && [[ "$display_name" == "my-cool-session" ]]; then
|
|
234
|
+
test_pass
|
|
235
|
+
else
|
|
236
|
+
test_fail "Parsed values incorrect: stableId=$stable_id, displayName=$display_name"
|
|
237
|
+
fi
|
|
238
|
+
else
|
|
239
|
+
test_fail "Regex did not match"
|
|
240
|
+
fi
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
test_legacy_session_name_parsing() {
|
|
244
|
+
test_start "Legacy session name parsing - handles old gt-<name> format"
|
|
245
|
+
|
|
246
|
+
# BEHAVIOR: Old sessions without stableId should still work
|
|
247
|
+
# They get stableId=null and displayName=<everything after gt->
|
|
248
|
+
|
|
249
|
+
local legacy_name="gt-old-session"
|
|
250
|
+
|
|
251
|
+
# This should NOT match the new format
|
|
252
|
+
if [[ "$legacy_name" =~ ^gt-([0-9]+)-(.+)$ ]]; then
|
|
253
|
+
test_fail "Legacy name incorrectly matched new format"
|
|
254
|
+
else
|
|
255
|
+
# Extract display name for legacy format
|
|
256
|
+
local display_name="${legacy_name#gt-}"
|
|
257
|
+
if [[ "$display_name" == "old-session" ]]; then
|
|
258
|
+
test_pass
|
|
259
|
+
else
|
|
260
|
+
test_fail "Legacy display name extraction failed"
|
|
261
|
+
fi
|
|
262
|
+
fi
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
# =============================================================================
|
|
266
|
+
# Test: Session Rename Preserves Stable ID
|
|
267
|
+
# =============================================================================
|
|
268
|
+
#
|
|
269
|
+
# BEHAVIOR: When renaming a session, the stableId prefix is preserved.
|
|
270
|
+
# This allows URLs using the stableId to continue working after rename.
|
|
271
|
+
#
|
|
272
|
+
# =============================================================================
|
|
273
|
+
|
|
274
|
+
test_rename_preserves_stable_id() {
|
|
275
|
+
test_start "Rename preserves stable ID"
|
|
276
|
+
|
|
277
|
+
cleanup_test_sessions
|
|
278
|
+
|
|
279
|
+
# Create a session with stable ID format
|
|
280
|
+
local temp_name="gt-temp-$$"
|
|
281
|
+
tmux new-session -d -s "$temp_name" -x 80 -y 24
|
|
282
|
+
|
|
283
|
+
local session_id
|
|
284
|
+
session_id=$(tmux list-sessions -F "#{session_name}|#{session_id}" | grep "^$temp_name|" | cut -d'|' -f2 | tr -d '$')
|
|
285
|
+
|
|
286
|
+
local original_name="gt-${session_id}-original"
|
|
287
|
+
tmux rename-session -t "$temp_name" "$original_name"
|
|
288
|
+
|
|
289
|
+
# Now rename to a new display name, preserving stable ID
|
|
290
|
+
local new_name="gt-${session_id}-renamed"
|
|
291
|
+
tmux rename-session -t "$original_name" "$new_name"
|
|
292
|
+
|
|
293
|
+
# Verify the session exists with new name but same stable ID
|
|
294
|
+
if tmux has-session -t "$new_name" 2>/dev/null; then
|
|
295
|
+
# Extract stable ID from new name
|
|
296
|
+
if [[ "$new_name" =~ ^gt-([0-9]+)- ]]; then
|
|
297
|
+
local extracted_id="${BASH_REMATCH[1]}"
|
|
298
|
+
if [[ "$extracted_id" == "$session_id" ]]; then
|
|
299
|
+
test_pass
|
|
300
|
+
else
|
|
301
|
+
test_fail "Stable ID changed after rename: expected $session_id, got $extracted_id"
|
|
302
|
+
fi
|
|
303
|
+
else
|
|
304
|
+
test_fail "New name doesn't match expected format"
|
|
305
|
+
fi
|
|
306
|
+
tmux kill-session -t "$new_name" 2>/dev/null || true
|
|
307
|
+
else
|
|
308
|
+
test_fail "Session not found after rename"
|
|
309
|
+
fi
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
# =============================================================================
|
|
313
|
+
# Test: Find Session by Stable ID
|
|
314
|
+
# =============================================================================
|
|
315
|
+
#
|
|
316
|
+
# BEHAVIOR: Sessions can be looked up by their stable ID regardless of
|
|
317
|
+
# their current display name. This is how the web UI connects after rename.
|
|
318
|
+
#
|
|
319
|
+
# =============================================================================
|
|
320
|
+
|
|
321
|
+
test_find_session_by_stable_id() {
|
|
322
|
+
test_start "Find session by stable ID"
|
|
323
|
+
|
|
324
|
+
cleanup_test_sessions
|
|
325
|
+
|
|
326
|
+
# Create a session
|
|
327
|
+
local temp_name="gt-temp-$$"
|
|
328
|
+
tmux new-session -d -s "$temp_name" -x 80 -y 24
|
|
329
|
+
|
|
330
|
+
local session_id
|
|
331
|
+
session_id=$(tmux list-sessions -F "#{session_name}|#{session_id}" | grep "^$temp_name|" | cut -d'|' -f2 | tr -d '$')
|
|
332
|
+
|
|
333
|
+
local session_name="gt-${session_id}-findme"
|
|
334
|
+
tmux rename-session -t "$temp_name" "$session_name"
|
|
335
|
+
|
|
336
|
+
# Search for session by stable ID
|
|
337
|
+
local found=""
|
|
338
|
+
while IFS= read -r line; do
|
|
339
|
+
if [[ "$line" =~ ^gt-${session_id}- ]]; then
|
|
340
|
+
found="$line"
|
|
341
|
+
break
|
|
342
|
+
fi
|
|
343
|
+
done < <(tmux list-sessions -F "#{session_name}" 2>/dev/null)
|
|
344
|
+
|
|
345
|
+
if [[ -n "$found" ]]; then
|
|
346
|
+
test_pass
|
|
347
|
+
tmux kill-session -t "$session_name" 2>/dev/null || true
|
|
348
|
+
else
|
|
349
|
+
test_fail "Could not find session by stable ID $session_id"
|
|
350
|
+
fi
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
test_find_session_by_display_name() {
|
|
354
|
+
test_start "Find session by display name (legacy support)"
|
|
355
|
+
|
|
356
|
+
cleanup_test_sessions
|
|
357
|
+
|
|
358
|
+
# Create a session
|
|
359
|
+
local temp_name="gt-temp-$$"
|
|
360
|
+
tmux new-session -d -s "$temp_name" -x 80 -y 24
|
|
361
|
+
|
|
362
|
+
local session_id
|
|
363
|
+
session_id=$(tmux list-sessions -F "#{session_name}|#{session_id}" | grep "^$temp_name|" | cut -d'|' -f2 | tr -d '$')
|
|
364
|
+
|
|
365
|
+
local display_name="unique-test-name-$$"
|
|
366
|
+
local session_name="gt-${session_id}-${display_name}"
|
|
367
|
+
tmux rename-session -t "$temp_name" "$session_name"
|
|
368
|
+
|
|
369
|
+
# Search for session by display name
|
|
370
|
+
local found=""
|
|
371
|
+
while IFS= read -r line; do
|
|
372
|
+
# Parse the session name
|
|
373
|
+
if [[ "$line" =~ ^gt-[0-9]+-(.+)$ ]]; then
|
|
374
|
+
local parsed_display="${BASH_REMATCH[1]}"
|
|
375
|
+
if [[ "$parsed_display" == "$display_name" ]]; then
|
|
376
|
+
found="$line"
|
|
377
|
+
break
|
|
378
|
+
fi
|
|
379
|
+
fi
|
|
380
|
+
done < <(tmux list-sessions -F "#{session_name}" 2>/dev/null)
|
|
381
|
+
|
|
382
|
+
if [[ -n "$found" ]]; then
|
|
383
|
+
test_pass
|
|
384
|
+
tmux kill-session -t "$session_name" 2>/dev/null || true
|
|
385
|
+
else
|
|
386
|
+
test_fail "Could not find session by display name $display_name"
|
|
387
|
+
fi
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
# =============================================================================
|
|
391
|
+
# Test: Auto-generated Display Names
|
|
392
|
+
# =============================================================================
|
|
393
|
+
#
|
|
394
|
+
# BEHAVIOR: When no custom name is provided, sessions get auto-generated
|
|
395
|
+
# numeric display names (1, 2, 3, etc.) based on the highest existing number.
|
|
396
|
+
#
|
|
397
|
+
# =============================================================================
|
|
398
|
+
|
|
399
|
+
test_auto_generated_display_names() {
|
|
400
|
+
test_start "Auto-generated display names increment correctly"
|
|
401
|
+
|
|
402
|
+
cleanup_test_sessions
|
|
403
|
+
|
|
404
|
+
# Create sessions with numeric display names
|
|
405
|
+
# Simulate gt-<id>-1, gt-<id>-2 format
|
|
406
|
+
|
|
407
|
+
# Session 1
|
|
408
|
+
local temp1="gt-temp-1-$$"
|
|
409
|
+
tmux new-session -d -s "$temp1" -x 80 -y 24
|
|
410
|
+
local id1
|
|
411
|
+
id1=$(tmux list-sessions -F "#{session_name}|#{session_id}" | grep "^$temp1|" | cut -d'|' -f2 | tr -d '$')
|
|
412
|
+
tmux rename-session -t "$temp1" "gt-${id1}-1"
|
|
413
|
+
|
|
414
|
+
# Session 2
|
|
415
|
+
local temp2="gt-temp-2-$$"
|
|
416
|
+
tmux new-session -d -s "$temp2" -x 80 -y 24
|
|
417
|
+
local id2
|
|
418
|
+
id2=$(tmux list-sessions -F "#{session_name}|#{session_id}" | grep "^$temp2|" | cut -d'|' -f2 | tr -d '$')
|
|
419
|
+
tmux rename-session -t "$temp2" "gt-${id2}-2"
|
|
420
|
+
|
|
421
|
+
# Now find the next available number
|
|
422
|
+
local max_num=0
|
|
423
|
+
while IFS= read -r line; do
|
|
424
|
+
if [[ "$line" =~ ^gt-[0-9]+-([0-9]+)$ ]]; then
|
|
425
|
+
local num="${BASH_REMATCH[1]}"
|
|
426
|
+
if (( num > max_num )); then
|
|
427
|
+
max_num=$num
|
|
428
|
+
fi
|
|
429
|
+
fi
|
|
430
|
+
done < <(tmux list-sessions -F "#{session_name}" 2>/dev/null)
|
|
431
|
+
|
|
432
|
+
local next_num=$((max_num + 1))
|
|
433
|
+
|
|
434
|
+
if [[ "$next_num" == "3" ]]; then
|
|
435
|
+
test_pass
|
|
436
|
+
else
|
|
437
|
+
test_fail "Expected next number to be 3, got $next_num"
|
|
438
|
+
fi
|
|
439
|
+
|
|
440
|
+
# Cleanup
|
|
441
|
+
tmux kill-session -t "gt-${id1}-1" 2>/dev/null || true
|
|
442
|
+
tmux kill-session -t "gt-${id2}-2" 2>/dev/null || true
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
# =============================================================================
|
|
446
|
+
# Test: Display Name Uniqueness
|
|
447
|
+
# =============================================================================
|
|
448
|
+
#
|
|
449
|
+
# BEHAVIOR: Two sessions cannot have the same display name, even if they
|
|
450
|
+
# have different stable IDs.
|
|
451
|
+
#
|
|
452
|
+
# =============================================================================
|
|
453
|
+
|
|
454
|
+
test_display_name_uniqueness() {
|
|
455
|
+
test_start "Display names must be unique"
|
|
456
|
+
|
|
457
|
+
cleanup_test_sessions
|
|
458
|
+
|
|
459
|
+
# Create a session with a specific display name
|
|
460
|
+
local temp1="gt-temp-1-$$"
|
|
461
|
+
tmux new-session -d -s "$temp1" -x 80 -y 24
|
|
462
|
+
local id1
|
|
463
|
+
id1=$(tmux list-sessions -F "#{session_name}|#{session_id}" | grep "^$temp1|" | cut -d'|' -f2 | tr -d '$')
|
|
464
|
+
local display_name="unique-name-$$"
|
|
465
|
+
tmux rename-session -t "$temp1" "gt-${id1}-${display_name}"
|
|
466
|
+
|
|
467
|
+
# Check if display name already exists (simulating validation)
|
|
468
|
+
local exists=false
|
|
469
|
+
while IFS= read -r line; do
|
|
470
|
+
if [[ "$line" =~ ^gt-[0-9]+-(.+)$ ]]; then
|
|
471
|
+
if [[ "${BASH_REMATCH[1]}" == "$display_name" ]]; then
|
|
472
|
+
exists=true
|
|
473
|
+
break
|
|
474
|
+
fi
|
|
475
|
+
fi
|
|
476
|
+
done < <(tmux list-sessions -F "#{session_name}" 2>/dev/null)
|
|
477
|
+
|
|
478
|
+
if $exists; then
|
|
479
|
+
test_pass
|
|
480
|
+
else
|
|
481
|
+
test_fail "Display name uniqueness check failed"
|
|
482
|
+
fi
|
|
483
|
+
|
|
484
|
+
# Cleanup
|
|
485
|
+
tmux kill-session -t "gt-${id1}-${display_name}" 2>/dev/null || true
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
# =============================================================================
|
|
489
|
+
# Test: URL Format
|
|
490
|
+
# =============================================================================
|
|
491
|
+
#
|
|
492
|
+
# BEHAVIOR: URLs use format /?session=<displayName>&id=<stableId>
|
|
493
|
+
# - session: Human-readable display name
|
|
494
|
+
# - id: Stable ID for rename resilience
|
|
495
|
+
#
|
|
496
|
+
# The system tries to look up by stable ID first, then by display name.
|
|
497
|
+
#
|
|
498
|
+
# =============================================================================
|
|
499
|
+
|
|
500
|
+
test_url_format() {
|
|
501
|
+
test_start "URL format includes both display name and stable ID"
|
|
502
|
+
|
|
503
|
+
# Simulate URL construction
|
|
504
|
+
local display_name="my-session"
|
|
505
|
+
local stable_id="42"
|
|
506
|
+
|
|
507
|
+
local url="/?session=${display_name}&id=${stable_id}"
|
|
508
|
+
|
|
509
|
+
# Parse it back
|
|
510
|
+
# Extract session param
|
|
511
|
+
if [[ "$url" =~ session=([^&]+) ]]; then
|
|
512
|
+
local parsed_session="${BASH_REMATCH[1]}"
|
|
513
|
+
fi
|
|
514
|
+
|
|
515
|
+
# Extract id param
|
|
516
|
+
if [[ "$url" =~ id=([^&]+) ]]; then
|
|
517
|
+
local parsed_id="${BASH_REMATCH[1]}"
|
|
518
|
+
fi
|
|
519
|
+
|
|
520
|
+
if [[ "$parsed_session" == "$display_name" ]] && [[ "$parsed_id" == "$stable_id" ]]; then
|
|
521
|
+
test_pass
|
|
522
|
+
else
|
|
523
|
+
test_fail "URL parsing failed: session=$parsed_session, id=$parsed_id"
|
|
524
|
+
fi
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
# =============================================================================
|
|
528
|
+
# Run All Tests
|
|
529
|
+
# =============================================================================
|
|
530
|
+
|
|
531
|
+
main() {
|
|
532
|
+
echo ""
|
|
533
|
+
echo -e "${YELLOW}==============================================================================${NC}"
|
|
534
|
+
echo -e "${YELLOW} Ghosttown CLI Test Suite${NC}"
|
|
535
|
+
echo -e "${YELLOW}==============================================================================${NC}"
|
|
536
|
+
echo ""
|
|
537
|
+
|
|
538
|
+
# Ensure tmux is installed
|
|
539
|
+
if ! command -v tmux &> /dev/null; then
|
|
540
|
+
echo -e "${RED}ERROR: tmux is not installed. Tests require tmux.${NC}"
|
|
541
|
+
exit 1
|
|
542
|
+
fi
|
|
543
|
+
|
|
544
|
+
# Clean up any leftover test sessions
|
|
545
|
+
cleanup_test_sessions
|
|
546
|
+
|
|
547
|
+
echo -e "${CYAN}Running tests...${NC}"
|
|
548
|
+
echo ""
|
|
549
|
+
|
|
550
|
+
# Session Name Validation
|
|
551
|
+
test_session_name_validation
|
|
552
|
+
test_session_name_validation_invalid
|
|
553
|
+
|
|
554
|
+
# Session Naming Format
|
|
555
|
+
test_session_naming_format
|
|
556
|
+
test_parse_session_name
|
|
557
|
+
test_legacy_session_name_parsing
|
|
558
|
+
|
|
559
|
+
# Rename Behavior
|
|
560
|
+
test_rename_preserves_stable_id
|
|
561
|
+
|
|
562
|
+
# Session Lookup
|
|
563
|
+
test_find_session_by_stable_id
|
|
564
|
+
test_find_session_by_display_name
|
|
565
|
+
|
|
566
|
+
# Auto-generated Names
|
|
567
|
+
test_auto_generated_display_names
|
|
568
|
+
|
|
569
|
+
# Display Name Uniqueness
|
|
570
|
+
test_display_name_uniqueness
|
|
571
|
+
|
|
572
|
+
# URL Format
|
|
573
|
+
test_url_format
|
|
574
|
+
|
|
575
|
+
# Final cleanup
|
|
576
|
+
cleanup_test_sessions
|
|
577
|
+
|
|
578
|
+
# Print summary
|
|
579
|
+
echo ""
|
|
580
|
+
echo -e "${YELLOW}==============================================================================${NC}"
|
|
581
|
+
echo -e "${YELLOW} Test Summary${NC}"
|
|
582
|
+
echo -e "${YELLOW}==============================================================================${NC}"
|
|
583
|
+
echo ""
|
|
584
|
+
echo -e " Tests run: ${TESTS_RUN}"
|
|
585
|
+
echo -e " ${GREEN}Passed: ${TESTS_PASSED}${NC}"
|
|
586
|
+
if [[ $TESTS_FAILED -gt 0 ]]; then
|
|
587
|
+
echo -e " ${RED}Failed: ${TESTS_FAILED}${NC}"
|
|
588
|
+
else
|
|
589
|
+
echo -e " Failed: ${TESTS_FAILED}"
|
|
590
|
+
fi
|
|
591
|
+
echo ""
|
|
592
|
+
|
|
593
|
+
if [[ $TESTS_FAILED -gt 0 ]]; then
|
|
594
|
+
echo -e "${RED}SOME TESTS FAILED${NC}"
|
|
595
|
+
exit 1
|
|
596
|
+
else
|
|
597
|
+
echo -e "${GREEN}ALL TESTS PASSED${NC}"
|
|
598
|
+
exit 0
|
|
599
|
+
fi
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
# Run main if script is executed directly
|
|
603
|
+
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
604
|
+
main "$@"
|
|
605
|
+
fi
|
package/src/cli.test.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the ghosttown CLI command-line interface.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from 'bun:test';
|
|
8
|
+
import { spawn } from 'child_process';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
const __dirname = join(fileURLToPath(import.meta.url), '..');
|
|
13
|
+
const CLI_PATH = join(__dirname, '..', 'bin', 'gt.js');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Run the CLI with given arguments and return stdout/stderr/exit code
|
|
17
|
+
*/
|
|
18
|
+
function runCli(args: string[]): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const proc = spawn('node', [CLI_PATH, ...args], {
|
|
21
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
let stdout = '';
|
|
25
|
+
let stderr = '';
|
|
26
|
+
|
|
27
|
+
proc.stdout.on('data', (data) => {
|
|
28
|
+
stdout += data.toString();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
proc.stderr.on('data', (data) => {
|
|
32
|
+
stderr += data.toString();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
proc.on('close', (code) => {
|
|
36
|
+
resolve({ stdout, stderr, code: code ?? 1 });
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('CLI', () => {
|
|
42
|
+
describe('version flag', () => {
|
|
43
|
+
test('-v should print version and exit', async () => {
|
|
44
|
+
const result = await runCli(['-v']);
|
|
45
|
+
expect(result.code).toBe(0);
|
|
46
|
+
expect(result.stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/);
|
|
47
|
+
expect(result.stderr).toBe('');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('--version should print version and exit', async () => {
|
|
51
|
+
const result = await runCli(['--version']);
|
|
52
|
+
expect(result.code).toBe(0);
|
|
53
|
+
expect(result.stdout.trim()).toMatch(/^\d+\.\d+\.\d+$/);
|
|
54
|
+
expect(result.stderr).toBe('');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('help flag', () => {
|
|
59
|
+
test('-h should print help and exit', async () => {
|
|
60
|
+
const result = await runCli(['-h']);
|
|
61
|
+
expect(result.code).toBe(0);
|
|
62
|
+
expect(result.stdout).toContain('Usage: ghosttown');
|
|
63
|
+
expect(result.stdout).toContain('--version');
|
|
64
|
+
expect(result.stdout).toContain('--help');
|
|
65
|
+
expect(result.stderr).toBe('');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('--help should print help and exit', async () => {
|
|
69
|
+
const result = await runCli(['--help']);
|
|
70
|
+
expect(result.code).toBe(0);
|
|
71
|
+
expect(result.stdout).toContain('Usage: ghosttown');
|
|
72
|
+
expect(result.stderr).toBe('');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|