aboutsummaryrefslogtreecommitdiff
path: root/tests/e2e.sh
diff options
context:
space:
mode:
Diffstat (limited to 'tests/e2e.sh')
-rwxr-xr-xtests/e2e.sh108
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