aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-10-23 13:59:59 +0300
committerIgor Soarez <igor@soarez.org>2026-10-23 13:59:59 +0300
commit9c9c9dad93b16781ec2fc40bd7582db62240171c (patch)
treecf85f3a53f8441eae36982d06244041cfe0ff6f3 /tests
parentc082f5044eedd2294d72f920d1018e40d73aab33 (diff)
restore, step 3: offer it in the panel, menu, and status line
when tmux stops (and its sessions are missing from the running server, or no server runs), the daemon publishes the restore offer: offer.json for the menu bar app and @dingger_restore for the status line, withdrawn once restored or dismissed. - panel: a first row "↺ Restore 9 sessions · 22 agents" with when it was saved and how many agents were mid-turn; clicking restores, "Not now" dismisses. an offer shows the panel like a waiting agent; off stays off. - menu: the offer on top; restore all, one session, or not now. - status line: a clickable blue ↺N before the clock. - restore runs through the user's login shell, as a terminal starts tmux, so restored panes get a real environment, not launchd's; --report writes restore.log and notifies a summary. restore --dismiss. - commands log only warnings by default; SIGPIPE ends them quietly. e2e: the offer through the real menu bar binary, the summary notification, the offer clearing, a restore with no tmux running, and not now staying dismissed when a terminal starts tmux. the second test daemon is started directly (it leaked through a function's $! before, and dozens of leaked daemons fought over the test socket and loaded the machine); each run uses its own socket and checks its daemons stop.
Diffstat (limited to 'tests')
-rwxr-xr-xtests/e2e.sh51
1 files changed, 47 insertions, 4 deletions
diff --git a/tests/e2e.sh b/tests/e2e.sh
index 29d900b..b79c748 100755
--- a/tests/e2e.sh
+++ b/tests/e2e.sh
@@ -37,7 +37,7 @@ cleanup() {
[ -n "${DAEMON_PID:-}" ] && kill "$DAEMON_PID" 2>/dev/null
[ -n "${DAEMON2_PID:-}" ] && kill "$DAEMON2_PID" 2>/dev/null
t kill-server 2>/dev/null
- tmux -L "$SOCK-restore" kill-server 2>/dev/null
+ tmux -L "$SOCK-restore-$$" kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup EXIT
@@ -226,6 +226,11 @@ t source-file "$ROOT/packaging/tmux/dingger.tmux"
segment=$(t display -p -c "$CLIENT" '#{T:status-right}' | sed 's/#\[[^]]*\]//g')
case "$segment" in *"×1"*"✓1"*) pass "segment shows ×1 ✓1 ($segment)";; *) fail "segment: [$segment]";; esac
expect "mouse binding installed" "t list-keys -T root | grep MouseDown1StatusRight | grep -c 'dingger focus'" 1
+expect "clicking the restore offer runs restore" "t list-keys -T root | grep MouseDown1StatusRight | grep -c 'restore --report'" 1
+t set -g @dingger_restore 3
+segment=$(t display -p -c "$CLIENT" '#{T:status-right}' | sed 's/#\[[^]]*\]//g')
+case "$segment" in "↺3 "*"×1"*) pass "segment offers to restore 3 sessions ($segment)";; *) fail "restore segment: [$segment]";; esac
+t set -gu @dingger_restore
# same run-shell + #{client_name} expansion as the mouse binding, via a key
t bind -n F12 run-shell "DINGGER_TMUX_SOCKET=$SOCK $DINGGER focus --client '#{client_name}'"
t send-keys -t viewer: F12
@@ -351,7 +356,7 @@ fi
# A second server and daemon. "viewer" hosts a client the way a terminal
# does: Ghostty runs `tmux new-session -As 0`.
echo "16. restore"
-R="$SOCK-restore"
+R="$SOCK-restore-$$"
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.
@@ -410,7 +415,10 @@ fingerprint() {
| grep -E '^(0|work)\|'
}
BEFORE=$(fingerprint)
-renv "$DINGGER" watch --interval-secs 1 --no-sounds --no-notifications >>"$DIR/daemon2.log" 2>&1 &
+# 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" 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; }
expect "a snapshot of both sessions" "grep -c '\"name\": \"work\"' \"\$(latest_snapshot)\" 2>/dev/null" 1 80
@@ -452,7 +460,16 @@ 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 | 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
+mbr() { renv DINGGER_BIN="$DINGGER" DINGGER_RESTORE_DIRECT=1 \
+ DINGGER_NOTIFIER="echo \"\$0|\$1\" >> $DIR/notes2" "$MENUBAR" "$@"; }
+expect "the daemon offers to restore 0 and work" \
+ "python3 -c 'import json,sys; o=json.load(open(sys.argv[1])); print([s[\"name\"] for s in o[\"sessions\"]], o[\"agents\"], o[\"interrupted\"])' $STATE2/offer.json 2>/dev/null" \
+ "['0', 'work'] 2 1" 80
+expect "and tells the status line" "r show -gv @dingger_restore" 2
+expect "the panel offers it first" "mbr --print-panel | head -1 | cut -f1,3" "↺ Restore 2 sessions · 2 agents [Not now]"
+expect "with what was interrupted" "mbr --print-panel | head -1 | cut -f2 | grep -o '1 was mid-turn'" "1 was mid-turn"
+mbr --restore
+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"
@@ -463,6 +480,8 @@ expect "the interrupted turn is reported" "grep -c '^interrupted work:agent' $D
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 "a notification summarizes" "cat $DIR/notes2" "Restored 2 tmux sessions|resumed 2 of 2 agents · 1 was mid-turn · 2 commands typed, not run"
+expect "the offer is gone" "ls $STATE2/offer.json 2>/dev/null; r show -gv @dingger_restore 2>/dev/null; mbr --print-panel | grep -c '↺'" "0"
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"
@@ -480,6 +499,30 @@ expect "restoring again changes nothing" "fingerprint" "$BEFORE"
expect "restoring again plans nothing" "grep -cvE '^keep ' $DIR/restore2.out" 0
[ "$FAILS" -gt 0 ] && { echo "--- restore output:"; cat "$DIR/restore.out"; echo "--- before:"; echo "$BEFORE"; echo "--- after:"; fingerprint; }
+# Again, now with no tmux running at all (after a reboot, before a terminal
+# opens): restoring starts the server. Then "Not now" stays dismissed.
+names() { r list-panes -a -F '#{session_name}|#{window_index}|#{?automatic-rename,(auto),#{window_name}}|#{pane_index}|#{pane_current_path}' 2>/dev/null | grep "^$1|"; }
+WORK_BEFORE=$(names work)
+r kill-server
+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 "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
+mbr --dismiss-offer >/dev/null
+expect "Not now: the offer is gone" "ls $STATE2/offer.json 2>/dev/null | wc -l | tr -d ' '" 0
+expect "and the status line is told" "r show -gv @dingger_restore 2>/dev/null" ""
+start_viewer
+sleep 2
+expect "it stays dismissed when a terminal starts tmux" "ls $STATE2/offer.json 2>/dev/null | wc -l | tr -d ' '" 0
+expect "the layout can still be restored by hand" "renv \"$DINGGER\" restore --dry-run | grep -c '^create keep'" 1
+
+# 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
+
echo
echo "$PASSES passed, $FAILS failed"
[ "$FAILS" -eq 0 ] || { echo "--- daemon log tail"; tail -30 "$DIR/daemon.log"; exit 1; }