diff options
| author | Igor Soarez <igor@soarez.org> | 2026-10-23 13:20:11 +0300 |
|---|---|---|
| committer | Igor Soarez <igor@soarez.org> | 2026-10-23 13:20:11 +0300 |
| commit | c082f5044eedd2294d72f920d1018e40d73aab33 (patch) | |
| tree | 54420e9c0fcb366e8db776c9463c4530737704d7 /tests/e2e.sh | |
| parent | 0f06b0b15d6b064703fa1166fb8e7ceda209aaa8 (diff) | |
restore, step 2: resume agents, retype command lines
- the pi extension reports its session file; the daemon keeps it in the
pane option @agent_session, and snapshots carry it. restore runs
`pi --session <file>` in the rebuilt pane, one agent every 0.5s, then
reports which agents run and which were interrupted mid-turn.
- other panes: the command line running in them is typed at the new
prompt but not run. exactly as typed with the zsh hook (dingger install
zsh: preexec/precmd keep @dingger_cmd), else the foreground program's
exact argv from the kernel (KERN_PROCARGS2), shell-quoted.
- "at a prompt" is the pane's own shell owning the terminal's foreground
process group, not a name check: `sh -c ...` is a running program.
- an agent or command that exits keeps its place in snapshots for 60s, so
a shutdown that stops programs before tmux still restores them.
- typing waits for each new prompt (text typed earlier echoes twice).
e2e step 16 covers resume with a session path containing a space, the
interrupted report, an agent that exits just before the server dies, a
quoted command retyped not run, and the hook's text; dropping the grace
period, judging prompts by name, running retyped commands, or not storing
the session reference each fail it. the test's fake pi is guarded so a
real pi never starts.
Diffstat (limited to 'tests/e2e.sh')
| -rwxr-xr-x | tests/e2e.sh | 74 |
1 files changed, 69 insertions, 5 deletions
diff --git a/tests/e2e.sh b/tests/e2e.sh index 425fdf3..29d900b 100755 --- a/tests/e2e.sh +++ b/tests/e2e.sh @@ -353,12 +353,21 @@ fi echo "16. restore" R="$SOCK-restore" STATE2="$DIR/state2" -r() { tmux -u -L "$R" "$@"; } -renv() { env DINGGER_TMUX_SOCKET="$R" DINGGER_AGENT_SOCKET="$DIR/hook2.sock" DINGGER_STATE_DIR="$STATE2" \ - DINGGER_TMUX_CONFIG=/dev/null "$@"; } +# 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" "$@"; } +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 "$@"; } 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. +mkdir -p "$DIR/rbin/real" +cp /bin/sleep "$DIR/rbin/real/pi" && codesign --force --sign - "$DIR/rbin/real/pi" 2>/dev/null +printf '#!/bin/sh\necho "$*" >> %s\nexec %s 100000\n' "$DIR/pi-args" "$DIR/rbin/real/pi" >"$DIR/rbin/pi" +chmod +x "$DIR/rbin/pi" start_viewer() { - r -f /dev/null new-session -d -s viewer -x 120 -y 40 \; set -g default-shell /bin/sh \; \ + env PATH="$DIR/rbin:$PATH" tmux -u -L "$R" -f /dev/null new-session -d -s viewer -x 120 -y 40 \; set -g default-shell /bin/sh \; \ + set -g default-command /bin/sh \; \ send-keys -t viewer "exec env -u TMUX tmux -u -L $R new-session -As 0 -c $W/one" Enter for _ in $(seq 50); do r has-session -t =0 2>/dev/null && [ -n "$(r list-clients)" ] && return; sleep 0.1; done echo "viewer client did not attach" @@ -380,6 +389,22 @@ r new-window -d -t '=work:7' -n logs -c "$W/one" r select-window -t '=work:7' r new-session -d -s keep -n saved -c "$W/one" r new-window -d -t '=keep:' -n saved2 +# Agents, a running command, and a command the zsh hook recorded. +r new-window -d -t '=work:3' -n agent -c "$W/two" +r new-window -d -t '=work:4' -n gone -c "$W/one" +r new-window -d -t '=work:5' -n cmd -c "$W/one" +r new-window -d -t '=work:6' -n hooked -c "$W/one" +# Never start the real pi: the panes must find the fake one first. +r send-keys -t '=work:agent' "command -v pi > $DIR/which-pi" Enter +for _ in $(seq 30); do [ -s "$DIR/which-pi" ] && break; sleep 0.1; done +if [ "$(cat "$DIR/which-pi" 2>/dev/null)" != "$DIR/rbin/pi" ]; then + fail "the restore server's panes would run $(cat "$DIR/which-pi" 2>/dev/null), not the fake pi; stopping" + echo "$PASSES passed, $FAILS failed"; exit 1 +fi +r send-keys -t '=work:agent' 'clear; pi' Enter +r send-keys -t '=work:gone' 'pi' Enter +r send-keys -t '=work:cmd' "sh -c 'sleep 100000; :' 'two words'" Enter +r send-keys -t '=work:hooked' 'sleep 200000' Enter fingerprint() { r list-panes -a -F '#{session_name}|#{window_index}|#{?automatic-rename,(auto),#{window_name}}|#{window_active}|#{window_zoomed_flag}|#{automatic-rename}|#{pane_index}|#{pane_active}|#{pane_left},#{pane_top},#{pane_width}x#{pane_height}|#{pane_current_path}' \ | grep -E '^(0|work)\|' @@ -389,6 +414,33 @@ renv "$DINGGER" watch --interval-secs 1 --no-sounds --no-notifications >>"$DIR/d DAEMON2_PID=$! latest_snapshot() { ls "$STATE2"/snapshots/*.json 2>/dev/null | tail -1; } expect "a snapshot of both sessions" "grep -c '\"name\": \"work\"' \"\$(latest_snapshot)\" 2>/dev/null" 1 80 +hook2() { # pane state session-file + python3 - "$DIR/hook2.sock" "$1" "$2" "$3" <<'PY' +import json, socket, sys +s = socket.socket(socket.AF_UNIX) +s.connect(sys.argv[1]) +s.sendall((json.dumps({"method": "pane.report_agent", "params": {"pane_id": sys.argv[2], + "agent": "pi", "state": sys.argv[3], "agent_session_path": sys.argv[4]}}) + "\n").encode()) +s.close() +PY +} +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 +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' +expect "an agent's session file is kept on its pane" "r show -pv -t $AGENT @agent_session 2>/dev/null" "$W/sessions/a b.jsonl" +in_snapshot() { python3 -c 'import sys; print(sys.argv[2] in open(sys.argv[1]).read())' "$(latest_snapshot)" "$1"; } +expect "snapshot: the agent's session" "in_snapshot '\"session\": \"$W/sessions/a b.jsonl\"'" True 80 +expect "snapshot: a running command's exact arguments" "in_snapshot \"'sleep 100000; :' 'two words'\"" True 80 +expect "snapshot: the zsh hook's text" "in_snapshot '\"command\": \"slp 200000 | cat\"'" True 80 +# An agent that exits just before tmux dies (a shutdown stops programs first). +kill "$(ps -o tpgid= -p "$(r display -p -t "$GONE" '#{pane_pid}')" | tr -d ' ')" +expect "the exited agent is noticed" "r show -pv -t $GONE @agent 2>/dev/null" "" +sleep 1.5 +expect "snapshot: the exited agent is kept for a while" "in_snapshot gone.jsonl" True r kill-server expect "the dead server's layout becomes the restore candidate" \ "python3 -c 'import json,sys; c=json.load(open(sys.argv[1])); print(c[\"status\"], len(c[\"snapshot\"][\"sessions\"]))' $STATE2/restore.json 2>/dev/null" "offered 4" 80 @@ -399,9 +451,21 @@ expect "the new server has a bare 0" "r list-windows -t =0 -F '#{window_panes}' expect "the candidate survives the new server's snapshots" \ "python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))[\"snapshot\"][\"server\"] != json.load(open(sys.argv[2]))[\"server\"])' $STATE2/restore.json \"\$(latest_snapshot)\"" True 80 expect "dry run plans: replace 0, keep keep and viewer, create work" \ - "renv \"$DINGGER\" restore --dry-run | cut -c1-14 | sort | tr '\n' ';'" "create work:;keep keep:;keep viewe;replace 0: 2 ;" + "renv \"$DINGGER\" restore --dry-run | grep -v '^ ' | cut -c1-14 | sort | tr '\n' ';'" "create work:;keep keep:;keep viewe;replace 0: 2 ;" renv "$DINGGER" restore >"$DIR/restore.out" 2>&1 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 "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 +expect "the interrupted turn is reported" "grep -c '^interrupted work:agent' $DIR/restore.out" 1 +typed_line() { r capture-pane -p -t "$1" | grep -v '^ *$' | tail -1; } +expect "a command is retyped with its quoting" "typed_line '=work:cmd' | grep -o \"sh -c 'sleep 100000; :' 'two words'\$\"" "sh -c 'sleep 100000; :' 'two words'" +expect "the zsh hook's text is retyped" "typed_line '=work:hooked' | grep -o 'slp 200000 | cat\$'" "slp 200000 | cat" +expect "neither runs: the shell waits for Enter" \ + "r display -p -t '=work:cmd' '#{pane_current_command}'; r display -p -t '=work:hooked' '#{pane_current_command}'" "bash +bash" expect "windows, panes, layouts, directories, focus, zoom are back" "fingerprint" "$BEFORE" expect "the terminal's client moved to the restored 0, still attached" "r list-clients -F '#{session_name}'" 0 expect "a used session is left alone" "r list-windows -t =keep -F '#{window_name}'" mine |
