Files
small-scripts/tests/test-claude-tmux.sh
Paul O'Reilly 448091ecf4 feat(claude-tmux): spawn detached tmux Claude Code session with Remote Control
New script that launches Claude Code (via claude-profile) inside a detached
tmux session with --remote-control enabled, in a project's directory, named
after the project (Title-cased, auto-incrementing #N on collision). Lets a
session be started for later attach over VPN or control from the Claude app.

Spec-first per repo conventions: specs/claude-tmux.spec.md, script, and
tests/test-claude-tmux.sh (18 dryrun assertions, all passing).

Claude-Session: https://claude.ai/code/session_014oa7T7z1h5n34vu8vX9zGT
2026-07-25 16:29:46 +12:00

114 lines
4.2 KiB
Bash
Executable File

#!/usr/bin/env bash
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
CLAUDE_TMUX="$SCRIPT_DIR/scripts/claude-tmux"
RED='\033[0;31m'; GREEN='\033[0;32m'; NC='\033[0m'
pass=0; fail=0
assert_eq() {
local desc="$1" expected="$2" actual="$3"
if [[ "$expected" == "$actual" ]]; then
echo -e "${GREEN}PASS${NC}: $desc"; ((pass++))
else
echo -e "${RED}FAIL${NC}: $desc"; echo " expected: $expected"; echo " actual: $actual"; ((fail++))
fi
}
assert_contains() {
local desc="$1" needle="$2" haystack="$3"
if [[ "$haystack" == *"$needle"* ]]; then
echo -e "${GREEN}PASS${NC}: $desc"; ((pass++))
else
echo -e "${RED}FAIL${NC}: $desc"; echo " needle: $needle"; echo " haystack: $haystack"; ((fail++))
fi
}
assert_exit() {
local desc="$1" expected="$2" actual="$3"
if [[ "$expected" -eq "$actual" ]]; then
echo -e "${GREEN}PASS${NC}: $desc"; ((pass++))
else
echo -e "${RED}FAIL${NC}: $desc"; echo " expected exit: $expected"; echo " actual exit: $actual"; ((fail++))
fi
}
# === Hermetic environment ===
TEST_HOME="$(mktemp -d)"
STUB_BIN="$(mktemp -d)"
cleanup() {
rm -rf "$TEST_HOME" "$STUB_BIN"
tmux kill-session -t "=Agent Runtimes" 2>/dev/null || true
}
trap cleanup EXIT
# Stub claude-profile on PATH so LAUNCH_CMD renders deterministically.
cat > "$STUB_BIN/claude-profile" <<'EOF'
#!/usr/bin/env bash
exit 0
EOF
chmod +x "$STUB_BIN/claude-profile"
export HOME="$TEST_HOME"
export PATH="$STUB_BIN:$PATH"
mkdir -p "$HOME/.claude-oreillyit-anthropic"
echo "deep" > "$HOME/.claude-oreillyit-anthropic/last-mode"
mkdir -p "$HOME/dev/claude/projects/agent-runtimes"
mkdir -p "$HOME/dev/claude/octopus"
echo "=== claude-tmux tests ==="
echo
# --- Basic dryrun: title-case name, mode from last-mode ---
out=$("$CLAUDE_TMUX" agent-runtimes --dryrun 2>&1)
assert_contains "dryrun shows title-cased session name" "Session name: Agent Runtimes" "$out"
assert_contains "dryrun mode from last-mode (deep)" "Mode: deep" "$out"
assert_contains "dryrun resolves projects/ folder" "Project: agent-runtimes (" "$out"
assert_contains "dryrun launch cmd has remote-control name" '--remote-control Agent\ Runtimes' "$out"
assert_contains "dryrun launch cmd passes --project basename" "--project agent-runtimes" "$out"
assert_contains "dryrun attach hint quotes the name" 'tmux attach -t "Agent Runtimes"' "$out"
# --- Mode override ---
out=$("$CLAUDE_TMUX" agent-runtimes --mode quick --dryrun 2>&1)
assert_contains "dryrun honours --mode override" "Mode: quick" "$out"
# --- Name override ---
out=$("$CLAUDE_TMUX" agent-runtimes --name "Custom Name" --dryrun 2>&1)
assert_contains "dryrun honours --name override" "Session name: Custom Name" "$out"
# --- Top-level (non-projects) folder resolves ---
out=$("$CLAUDE_TMUX" octopus --dryrun 2>&1)
assert_contains "dryrun resolves top-level ~/dev/claude folder" "Session name: Octopus" "$out"
# --- Unknown project errors ---
out=$("$CLAUDE_TMUX" no-such-project --dryrun 2>&1); rc=$?
assert_exit "unknown project exits 1" 1 "$rc"
assert_contains "unknown project error mentions search locations" "Project folder not found" "$out"
# --- No project, non-TTY errors ---
out=$("$CLAUDE_TMUX" --dryrun </dev/null 2>&1); rc=$?
assert_exit "no project (non-TTY) exits 1" 1 "$rc"
assert_contains "no project error is helpful" "No project given" "$out"
# --- Missing profile errors ---
out=$("$CLAUDE_TMUX" agent-runtimes --profile no-such-profile --dryrun 2>&1); rc=$?
assert_exit "missing profile exits 1" 1 "$rc"
assert_contains "missing profile error names dir" "Profile directory not found" "$out"
# --- Help ---
out=$("$CLAUDE_TMUX" --help 2>&1); rc=$?
assert_exit "--help exits 0" 0 "$rc"
assert_contains "--help shows script name" "claude-tmux" "$out"
# --- Auto-increment against a live session ---
if tmux new-session -d -s "Agent Runtimes" -c /tmp 2>/dev/null; then
out=$("$CLAUDE_TMUX" agent-runtimes --dryrun 2>&1)
assert_contains "auto-increments to #2 when base name taken" "Session name: Agent Runtimes #2" "$out"
tmux kill-session -t "=Agent Runtimes" 2>/dev/null || true
else
echo -e "${GREEN}SKIP${NC}: auto-increment (tmux server unavailable)"
fi
echo
echo "=== $pass passed, $fail failed ==="
[[ "$fail" -eq 0 ]]