- 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>
267 lines
10 KiB
Bash
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" ]
|
|
}
|