Files
devdesk/tests/attach.bats
T
jaredandClaude Opus 5.5 527cb1fda2
build-deb / Lint, test, package (push) Successful in 2m0s
build-deb / Notify on failure (push) Skipped
feat: free-form sessions, rename/end from the panel, live Claude Context (0.2.0)
- Sessions no longer need a project folder: any name works and starts in
  DEVDESK_DEFAULT_DIR unless it matches a project; `devdesk new NAME [PROJECT]`.
- Rename (✎) and end/forget (×) sessions from the Sessions panel, plus
  `devdesk rename`. The browser runs them as `devdesk attach --new|--rename|--end`
  through ttyd. Renames keep the running Claude and its recorded conversation:
  the hook, the run-wrapper and the statusline ask tmux for the current name.
- Claude Context panel is live: devdesk-statusline snapshots model,
  context-window use, cost and edits per session, which --list reports;
  Claude's footer gets a one-line summary. The panel now sits under Sessions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 03:03:09 -04:00

267 lines
10 KiB
Bash

#!/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" .hidden "$(printf 'x%.0s' {1..70})"; do
run devdesk attach "$bad"
[ "$status" -ne 0 ]
[[ $output == *"invalid session name"* ]]
done
run devdesk attach -x
[ "$status" -ne 0 ]
[[ $output == *"unknown option"* ]]
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 "sessions without a project start in the default folder" {
start_server
run devdesk new pbs-upgrade
[ "$status" -eq 0 ]
run tmux -S "$DEVDESK_SOCKET" display -p -t =pbs-upgrade:claude '#{pane_current_path}'
[ "$output" = "$DEVDESK_PROJECTS_ROOT" ]
}
@test "symlinks may not escape the projects root" {
start_server
run devdesk new escape
[ "$status" -eq 0 ]
run tmux -S "$DEVDESK_SOCKET" display -p -t =escape:claude '#{pane_current_path}'
[ "$output" = "$DEVDESK_PROJECTS_ROOT" ]
run devdesk new other escape
[ "$status" -ne 0 ]
[[ $output == *"no project 'escape'"* ]]
}
@test "new NAME PROJECT starts the session in that project" {
start_server
run devdesk new ops-alpha alpha
[ "$status" -eq 0 ]
run tmux -S "$DEVDESK_SOCKET" display -p -t =ops-alpha:claude '#{pane_current_path}'
[ "$output" = "$DEVDESK_PROJECTS_ROOT/alpha" ]
run devdesk new ops-alpha alpha
[ "$status" -ne 0 ]
[[ $output == *"already exists"* ]]
}
@test "rename moves a running session, its registry entry and its context" {
start_server
devdesk new alpha
echo '{"session_id":"abc-123","source":"startup"}' | DEVDESK_SESSION=alpha "$REPO/libexec/devdesk-session-hook"
mkdir -p "$DEVDESK_STATE_DIR/context" && echo '{"used_pct":12}' >"$DEVDESK_STATE_DIR/context/alpha.json"
run devdesk rename alpha ops
[ "$status" -eq 0 ]
run tmux -S "$DEVDESK_SOCKET" has-session -t =ops
[ "$status" -eq 0 ]
run tmux -S "$DEVDESK_SOCKET" show-environment -t =ops DEVDESK_SESSION
[ "$output" = "DEVDESK_SESSION=ops" ]
[ "$(jq -r '.sessions.ops.claude_session_id' "$DEVDESK_STATE_DIR/registry.json")" = abc-123 ]
[ "$(jq -r '.sessions | has("alpha")' "$DEVDESK_STATE_DIR/registry.json")" = false ]
[ -f "$DEVDESK_STATE_DIR/context/ops.json" ]
[ ! -e "$DEVDESK_STATE_DIR/context/alpha.json" ]
}
@test "rename refuses clashes and unknown sessions" {
start_server
devdesk new alpha
devdesk new main
run devdesk rename alpha main
[ "$status" -ne 0 ]
[[ $output == *"already exists"* ]]
run devdesk rename nosuch other
[ "$status" -ne 0 ]
[[ $output == *"no session named"* ]]
run devdesk rename alpha "bad name"
[ "$status" -ne 0 ]
}
@test "rename works for dormant sessions" {
echo '{"session_id":"old-1","source":"resume"}' | DEVDESK_SESSION=gone "$REPO/libexec/devdesk-session-hook"
jq '.sessions.gone.dir = "/tmp"' "$DEVDESK_STATE_DIR/registry.json" >"$TMP/r" && mv "$TMP/r" "$DEVDESK_STATE_DIR/registry.json"
run devdesk rename gone archived
[ "$status" -eq 0 ]
[ "$(jq -r '.sessions.archived.claude_session_id' "$DEVDESK_STATE_DIR/registry.json")" = old-1 ]
}
@test "hooks follow the tmux session name after a rename" {
start_server
devdesk new alpha
devdesk rename alpha renamed
pane=$(tmux -S "$DEVDESK_SOCKET" display -p -t =renamed:shell '#{pane_id}')
echo '{"session_id":"after-rename","source":"clear"}' |
TMUX="$DEVDESK_SOCKET,1,0" TMUX_PANE="$pane" DEVDESK_SESSION=alpha "$REPO/libexec/devdesk-session-hook"
[ "$(jq -r '.sessions.renamed.claude_session_id' "$DEVDESK_STATE_DIR/registry.json")" = after-rename ]
[ "$(jq -r '.sessions | has("alpha")' "$DEVDESK_STATE_DIR/registry.json")" = false ]
}
@test "statusline snapshots context for the session and --list reports it" {
start_server
devdesk new alpha
pane=$(tmux -S "$DEVDESK_SOCKET" display -p -t =alpha:shell '#{pane_id}')
status_json='{"session_id":"s1","model":{"id":"m","display_name":"Opus"},"effort":{"level":"high"},
"context_window":{"context_window_size":1000000,"used_percentage":34.6,
"current_usage":{"input_tokens":1000,"cache_creation_input_tokens":2000,"cache_read_input_tokens":343000}},
"cost":{"total_cost_usd":1.5,"total_lines_added":10,"total_lines_removed":2}}'
run bash -c "echo '$status_json' | TMUX='$DEVDESK_SOCKET,1,0' TMUX_PANE='$pane' '$REPO/libexec/devdesk-statusline'"
[ "$status" -eq 0 ]
[[ $output == *"alpha"* && $output == *"ctx 34%"* ]]
run devdesk attach --list
[ "$(jq -r '.sessions[] | select(.name=="alpha") | .context.used_tokens' <<<"$output")" = 346000 ]
[ "$(jq -r '.sessions[] | select(.name=="alpha") | .context.model' <<<"$output")" = Opus ]
}
@test "web actions run through attach and answer with JSON" {
start_server
run devdesk attach --new web1 alpha
[ "$output" = '{"ok":true}' ]
run devdesk attach --rename web1 web2
[ "$output" = '{"ok":true}' ]
run devdesk attach --end web2
[ "$output" = '{"ok":true}' ]
run tmux -S "$DEVDESK_SOCKET" has-session -t =web2
[ "$status" -ne 0 ]
run devdesk attach --bogus
[ "$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" ]
}