#!/usr/bin/env bats # Exercises bin/devdesk and the libexec helpers against a throwaway tmux server. REPO=$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd) setup() { TMP=$(mktemp -d /tmp/devdesk-test.XXXXXX) # short path: unix socket limit export DEVDESK_ENV=/dev/null export DEVDESK_PROJECTS_ROOT="$TMP/projects" export DEVDESK_STATE_DIR="$TMP/state" export DEVDESK_SOCKET="$TMP/tmux.sock" export DEVDESK_LIBEXEC="$REPO/libexec" export DEVDESK_SHARE="$REPO/share" export DEVDESK_NO_SYSTEMD=1 export FAKE_LOG="$TMP/claude-args" export CLAUDE_BIN="$TMP/fake-claude" export PATH="$REPO/bin:$PATH" mkdir -p "$DEVDESK_PROJECTS_ROOT/alpha" "$DEVDESK_PROJECTS_ROOT/test code" "$TMP/outside" "$FAKE_LOG" ln -s "$TMP/outside" "$DEVDESK_PROJECTS_ROOT/escape" cat >"$CLAUDE_BIN" <<'EOF' #!/bin/bash printf '%s\n' "$*" >>"$FAKE_LOG/$DEVDESK_SESSION" exec sleep 300 EOF chmod +x "$CLAUDE_BIN" } teardown() { tmux -S "$DEVDESK_SOCKET" kill-server 2>/dev/null || true rm -rf "$TMP" } start_server() { tmux -u -D -S "$DEVDESK_SOCKET" -f "$REPO/share/tmux.conf" & "$REPO/libexec/devdesk-tmux-ready" } # Wait for the fake claude in SESSION to record its arguments. claude_args() { for _ in $(seq 1 50); do [ -s "$FAKE_LOG/$1" ] && { tail -n1 "$FAKE_LOG/$1"; return 0; } sleep 0.1 done return 1 } @test "rejects invalid session names" { start_server for bad in ../etc "a b" -x .hidden "$(printf 'x%.0s' {1..70})"; do run devdesk attach "$bad" [ "$status" -ne 0 ] [[ $output == *"invalid session name"* ]] done run devdesk attach one two [ "$status" -ne 0 ] } @test "fails clearly when the tmux server is down" { run devdesk new alpha [ "$status" -ne 0 ] [[ $output == *"tmux server is not running"* ]] } @test "new session gets claude and shell windows in the project dir" { start_server run devdesk new alpha [ "$status" -eq 0 ] run tmux -S "$DEVDESK_SOCKET" list-windows -t =alpha -F '#{window_name}:#{pane_current_path}' [ "${lines[0]}" = "claude:$DEVDESK_PROJECTS_ROOT/alpha" ] [ "${lines[1]}" = "shell:$DEVDESK_PROJECTS_ROOT/alpha" ] run claude_args alpha [ "$output" = "-n alpha --settings $REPO/share/claude-settings.json" ] run jq -r '.sessions.alpha.dir' "$DEVDESK_STATE_DIR/registry.json" [ "$output" = "$DEVDESK_PROJECTS_ROOT/alpha" ] } @test "creating an existing session is a no-op" { start_server devdesk new alpha run devdesk new alpha [ "$status" -eq 0 ] run tmux -S "$DEVDESK_SOCKET" list-sessions -F '#{session_name}' [ "$output" = "alpha" ] } @test "default session maps to the projects root" { start_server devdesk new main run tmux -S "$DEVDESK_SOCKET" display -p -t =main:claude '#{pane_current_path}' [ "$output" = "$DEVDESK_PROJECTS_ROOT" ] } @test "directories with spaces are reachable by slug" { start_server run devdesk new test-code [ "$status" -eq 0 ] run tmux -S "$DEVDESK_SOCKET" display -p -t =test-code:claude '#{pane_current_path}' [ "$output" = "$DEVDESK_PROJECTS_ROOT/test code" ] } @test "symlinks may not escape the projects root" { start_server run devdesk new escape [ "$status" -ne 0 ] [[ $output == *"no project directory"* ]] run devdesk new nonexistent [ "$status" -ne 0 ] } @test "session hook records the Claude session and the next start resumes it" { start_server echo '{"session_id":"abc-123","source":"startup"}' | DEVDESK_SESSION=alpha "$REPO/libexec/devdesk-session-hook" run jq -r '.sessions.alpha.claude_session_id' "$DEVDESK_STATE_DIR/registry.json" [ "$output" = "abc-123" ] devdesk new alpha run claude_args alpha [ "$output" = "--resume abc-123 --settings $REPO/share/claude-settings.json" ] } @test "session hook ignores sessions it does not own" { echo '{"session_id":"zzz"}' | DEVDESK_SESSION= "$REPO/libexec/devdesk-session-hook" echo '{"session_id":"zzz"}' | DEVDESK_SESSION='../x' "$REPO/libexec/devdesk-session-hook" [ ! -e "$DEVDESK_STATE_DIR/registry.json" ] } @test "--list reports running, dormant and project entries" { start_server devdesk new alpha echo '{"session_id":"old-1","source":"resume"}' | DEVDESK_SESSION=gone "$REPO/libexec/devdesk-session-hook" run devdesk attach --list [ "$status" -eq 0 ] [ "$(jq -r '.default' <<<"$output")" = main ] [ "$(jq -r '.sessions[] | select(.name=="alpha") | .running' <<<"$output")" = true ] [ "$(jq -r '.sessions[] | select(.name=="gone") | .running' <<<"$output")" = false ] [ "$(jq -r '.sessions[] | select(.name=="gone") | .claude_session_id' <<<"$output")" = old-1 ] [ "$(jq -r '[.projects[].name] | sort | join(",")' <<<"$output")" = "alpha,main,test-code" ] } @test "--list works with the server down" { run devdesk attach --list [ "$status" -eq 0 ] [ "$(jq -r '.sessions | length' <<<"$output")" = 0 ] } @test "end kills the session and forgets it" { start_server devdesk new alpha run devdesk end alpha [ "$status" -eq 0 ] run tmux -S "$DEVDESK_SOCKET" has-session -t =alpha [ "$status" -ne 0 ] run jq -r '.sessions | has("alpha")' "$DEVDESK_STATE_DIR/registry.json" [ "$output" = false ] } @test "remote control flag is passed when enabled" { start_server tmux -S "$DEVDESK_SOCKET" set-environment -g DEVDESK_REMOTE_CONTROL 1 devdesk new alpha run claude_args alpha [ "$output" = "-n alpha --settings $REPO/share/claude-settings.json --remote-control devdesk-alpha" ] }