diff options
Diffstat (limited to 'tests')
| -rwxr-xr-x | tests/e2e.sh | 108 |
1 files changed, 106 insertions, 2 deletions
diff --git a/tests/e2e.sh b/tests/e2e.sh index 26914f1..c4bcabb 100755 --- a/tests/e2e.sh +++ b/tests/e2e.sh @@ -361,8 +361,18 @@ STATE2="$DIR/state2" # tmux gives new windows the environment of the client that creates them: # both the test and restore run with the fake pi first on PATH. r() { env PATH="$DIR/rbin:$PATH" tmux -u -L "$R" "$@"; } +# A server restore starts gets the same plain shells as the test's own: the +# user's login shell would read their profile, which puts the real agents +# first on PATH. +printf 'set -g default-shell /bin/sh\nset -g default-command /bin/sh\n' >"$DIR/tmux-test.conf" renv() { env PATH="$DIR/rbin:$PATH" DINGGER_TMUX_SOCKET="$R" DINGGER_AGENT_SOCKET="$DIR/hook2.sock" \ - DINGGER_STATE_DIR="$STATE2" DINGGER_TMUX_CONFIG=/dev/null "$@"; } + DINGGER_STATE_DIR="$STATE2" DINGGER_TMUX_CONFIG="$DIR/tmux-test.conf" "$@"; } +# Every pi or claude in a test pane must be a fake (they run from $DIR). +real_agents() { + for tty in $(r list-panes -a -F '#{pane_tty}' 2>/dev/null | sed 's|/dev/||'); do + ps -o comm=,args= -t "$tty" | awk -v dir="$DIR/" '($1 ~ /(^|\/)(pi|claude)$/ || $2 ~ /^(pi|claude)$/) && index($2, dir) != 1' + done +} W="$DIR/w"; mkdir -p "$W/one" "$W/two" "$W/three" # A fake pi named pi (the daemon finds agents by process name) that records # its arguments. @@ -417,7 +427,7 @@ fingerprint() { BEFORE=$(fingerprint) # Started directly, not through a function: $! must be the daemon itself. env PATH="$DIR/rbin:$PATH" DINGGER_TMUX_SOCKET="$R" DINGGER_AGENT_SOCKET="$DIR/hook2.sock" \ - DINGGER_STATE_DIR="$STATE2" DINGGER_TMUX_CONFIG=/dev/null \ + DINGGER_STATE_DIR="$STATE2" DINGGER_TMUX_CONFIG="$DIR/tmux-test.conf" \ "$DINGGER" watch --interval-secs 1 --no-sounds --no-notifications >>"$DIR/daemon2.log" 2>&1 & DAEMON2_PID=$! latest_snapshot() { ls "$STATE2"/snapshots/*.json 2>/dev/null | tail -1; } @@ -436,6 +446,8 @@ AGENT=$(r display -p -t '=work:agent' '#{pane_id}') GONE=$(r display -p -t '=work:gone' '#{pane_id}') expect "the fake pi agents are detected" "r show -pv -t $AGENT @agent 2>/dev/null; r show -pv -t $GONE @agent 2>/dev/null" "pi pi" 80 +# Session files exist: restore resumes only sessions with one. +mkdir -p "$W/sessions" && touch "$W/sessions/a b.jsonl" "$W/sessions/gone.jsonl" hook2 "$AGENT" working "$W/sessions/a b.jsonl" hook2 "$GONE" idle "$W/sessions/gone.jsonl" r set-option -p -t '=work:hooked' @dingger_cmd 'slp 200000 | cat' @@ -473,6 +485,7 @@ cp "$STATE2/restore.log" "$DIR/restore.out" 2>/dev/null expect "restore succeeds" "grep -c FAILED $DIR/restore.out" 0 expect "agents resumed with their session files" "grep -- --session $DIR/pi-args | sort" "--session $W/sessions/a b.jsonl --session $W/sessions/gone.jsonl" +expect "no real agent runs in a test pane" "real_agents" "" expect "both agents run" "r display -p -t '=work:agent' '#{pane_current_command}'; r display -p -t '=work:gone' '#{pane_current_command}'" "pi pi" expect "restore reports them" "grep -c '^resumed 2 of 2 agents' $DIR/restore.out" 1 @@ -508,6 +521,8 @@ expect "a new offer: all four sessions, tmux is not running" \ "python3 -c 'import json,sys; print(len(json.load(open(sys.argv[1]))[\"sessions\"]))' $STATE2/offer.json 2>/dev/null" 4 80 mbr --restore work expect "restoring with no server starts it and rebuilds the session" "names work" "$WORK_BEFORE" +expect "with the test's shell, not the user's login shell" "r display -p -t '=work:agent' '#{pane_current_command}'" bash +expect "no real agent runs in a test pane (no server)" "real_agents" "" expect "the offer shrinks to what is still missing" \ "python3 -c 'import json,sys; print(sorted(s[\"name\"] for s in json.load(open(sys.argv[1]))[\"sessions\"]))' $STATE2/offer.json 2>/dev/null" \ "['0', 'keep', 'viewer']" 80 @@ -524,6 +539,95 @@ echo "17. kill during a write" expect "20 kills right after done: hook and display state agree" \ "\"$ROOT/tests/kill-race.sh\" \"$DINGGER\" 20 | grep -o '^[^:]*: [0-9]* of' | sed 's/.*: //'" "0 of" 1 +# --- 18. claude: install its hook; restore resumes its session --------------- +echo "18. claude" +CC="$DIR/cc-config"; mkdir -p "$CC/real" +cat >"$CC/real/settings.json" <<'JSON' +{ + "env": { + "TRACE_TO_LANGSMITH": "true" + }, + "model": "sonnet", + "hooks": { + "SessionStart": [ + { + "matcher": "^(startup|resume|clear|compact|fork)$", + "hooks": [ + { + "type": "command", + "command": "bash '/x/herdr-agent-state.sh' session", + "timeout": 10 + } + ] + } + ] + }, + "theme": "auto" +} +JSON +cp "$CC/real/settings.json" "$CC/original.json" +ln -s "$CC/real/settings.json" "$CC/settings.json" +ccinstall() { CLAUDE_CONFIG_DIR="$CC" "$DINGGER" install claude; } +ccinstall >"$DIR/ccinstall.out" 2>&1 +expect "install claude succeeds" "grep -c '^installed in' $DIR/ccinstall.out" 1 +expect "settings: the same keys in the same order, herdr's hook first, dingger's added" \ + "python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(list(d), [h[\"command\"].rsplit(\"/\", 1)[-1] for g in d[\"hooks\"][\"SessionStart\"] for h in g[\"hooks\"]])' $CC/settings.json" \ + "['env', 'model', 'hooks', 'theme'] [\"herdr-agent-state.sh' session\", 'dingger hook claude']" +expect "everything else unchanged" \ + "python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); d[\"hooks\"][\"SessionStart\"].pop(); print(d == json.load(open(sys.argv[2])))' $CC/settings.json $CC/original.json" True +expect "written through the symlink" "[ -L $CC/settings.json ] && cmp -s $CC/settings.json $CC/real/settings.json && echo yes" yes +expect "the original is backed up" "cmp -s $CC/real/settings.json.bak-dingger $CC/original.json && echo same" same +ccinstall >"$DIR/ccinstall2.out" 2>&1 +expect "installing again changes nothing" "cat $DIR/ccinstall2.out" "already installed in $(cd "$CC/real" && pwd -P)/settings.json" +echo '{"model": ' >"$CC/broken.json"; cp "$CC/broken.json" "$CC/broken-copy.json" +expect "invalid JSON is not touched" \ + "CLAUDE_CONFIG_DIR=$CC/b $DINGGER install claude >/dev/null 2>&1; mkdir -p $CC/b; cp $CC/broken.json $CC/b/settings.json; CLAUDE_CONFIG_DIR=$CC/b $DINGGER install claude >/dev/null 2>&1; echo \$?; cmp -s $CC/b/settings.json $CC/broken-copy.json && echo unchanged" "1 +unchanged" +# The exact command Claude would run, from the installed settings. +HOOK_CMD=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["hooks"]["SessionStart"][-1]["hooks"][0]["command"])' "$CC/settings.json") +expect "the hook prints nothing and exits 0, even with no daemon or bad input" \ + "printf '{\"hook_event_name\":\"SessionStart\",\"transcript_path\":\"/x.jsonl\"}' | TMUX_PANE=%99 DINGGER_AGENT_SOCKET=$DIR/none.sock sh -c \"$HOOK_CMD\"; echo garbage | sh -c \"$HOOK_CMD\"; echo \"exit \$?\"" "exit 0" +# A fake claude named claude: records its arguments, runs the SessionStart +# hook with Claude's input (`claude <id>` starts session <id>, `claude +# --resume <id>` resumes it), then waits. +cp /bin/sleep "$DIR/rbin/real/claude" && codesign --force --sign - "$DIR/rbin/real/claude" 2>/dev/null +cat >"$DIR/rbin/claude" <<EOF +#!/bin/sh +echo "\$*" >> $DIR/claude-args +id=\${2:-\$1} +printf '{"session_id":"%s","transcript_path":"%s","cwd":"%s","hook_event_name":"SessionStart","source":"startup"}' \ + "\$id" "$W/claude/\$id.jsonl" "\$PWD" | DINGGER_AGENT_SOCKET=$DIR/hook2.sock sh -c "$HOOK_CMD" +exec $DIR/rbin/real/claude 100000 +EOF +chmod +x "$DIR/rbin/claude" +# "talked" has a transcript; "fresh" never got a message, so it has none. +mkdir -p "$W/claude" && touch "$W/claude/talked.jsonl" +r new-session -d -s cc -n talked -c "$W/two" +r new-window -d -t '=cc:' -n fresh -c "$W/three" +r send-keys -t '=cc:talked' 'claude talked' Enter +r send-keys -t '=cc:fresh' 'claude fresh' Enter +r set-option -p -t '=cc:fresh' @dingger_cmd 'claude fresh' +TALKED=$(r display -p -t '=cc:talked' '#{pane_id}') +expect "claude is detected" "r show -pv -t '=cc:talked' @agent 2>/dev/null; r show -pv -t '=cc:fresh' @agent 2>/dev/null" "claude +claude" 80 +expect "its hook's session is kept on its pane" "r show -pv -t $TALKED @agent_session 2>/dev/null" "$W/claude/talked.jsonl" 80 +expect "snapshot: claude's session" "in_snapshot '\"session\": \"$W/claude/talked.jsonl\"'" True 80 +r kill-server +expect "a new restore candidate" \ + "python3 -c 'import json,sys; print(\"cc\" in [s[\"name\"] for s in json.load(open(sys.argv[1]))[\"snapshot\"][\"sessions\"]])' $STATE2/restore.json 2>/dev/null" True 80 +: >"$DIR/claude-args" +renv "$DINGGER" restore --session cc >"$DIR/restore-cc.out" 2>&1 +expect "no real agent runs in a test pane (claude)" "real_agents" "" +expect "claude resumed by id" "cat $DIR/claude-args" "--resume talked" +expect "in its directory" "r display -p -t '=cc:talked' '#{pane_current_path}'" "$(cd "$W/two" && pwd -P)" +expect "restore reports it" "grep -c '^resumed 1 of 1 agents' $DIR/restore-cc.out" 1 +expect "no transcript: its command line is typed, not run" \ + "typed_line '=cc:fresh' | grep -o 'claude fresh\$'; r display -p -t '=cc:fresh' '#{pane_current_command}'" "claude fresh +bash" +expect "the resumed claude reports its session again" \ + "r show -pv -t '=cc:talked' @agent_session 2>/dev/null" "$W/claude/talked.jsonl" 80 +[ "$FAILS" -gt 0 ] && { echo "--- restore-cc:"; cat "$DIR/restore-cc.out"; } + # Every daemon this run started must stop with it. kill "$DAEMON_PID" "$DAEMON2_PID" 2>/dev/null expect "the test daemons stop" "ps -o pid= -p $DAEMON_PID,$DAEMON2_PID | wc -l | tr -d ' '" 0 |
