ttyd serves a single-file xterm.js UI styled with the LotusGuild TDS (web_template base.css). Sessions live in a dedicated tmux server unit (devdesk-tmux.service), so restarting or upgrading the web unit never stops Claude. Each named session maps to a project directory. A Claude SessionStart hook records the conversation ID, so after a reboot the session resumes that exact conversation. - bin/devdesk: attach / new / ls / end / pick, with an fzf picker - ttyd requires a credential that NPM injects; origin check enabled - debian/: dh 13 with --no-stop-on-upgrade for the tmux unit, bundled sha256-verified ttyd 1.7.7, lintian overrides, man page - CI: lint, test, build, smoke-test, artifact; v* tags publish to the Gitea apt registry and create a release Refs: tinker ticket 887531976 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
163 lines
5.5 KiB
Bash
163 lines
5.5 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" -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" ]
|
|
}
|