diff options
| author | Igor Soarez <igor@soarez.org> | 2026-10-23 13:01:52 +0300 |
|---|---|---|
| committer | Igor Soarez <igor@soarez.org> | 2026-10-23 13:01:52 +0300 |
| commit | 0f06b0b15d6b064703fa1166fb8e7ceda209aaa8 (patch) | |
| tree | 4ecb5cf8daa390ef131962d64925ab0d325b6c39 /tests/e2e.sh | |
| parent | de1a3ce438d31df262a026bfb104ca07e656ee30 (diff) | |
restore, step 1: layout snapshots and dingger restore
the daemon saves the tmux layout when it changes: sessions, windows
(index, name, automatic-rename, layout, size, zoom, active), and panes
(directory, active, agent). a new history file for layout changes, a
rewrite for details, the last 50 kept, nothing saved while tmux is down.
when a different server runs (or none), the old server's last snapshot is
pinned as the restore candidate before the new server's snapshots start.
dingger restore rebuilds missing sessions by tmux id: panes split from the
newest so they keep their order, the saved layout applied at the saved
size, then the size released so windows follow clients again. a bare
session of the same name (a terminal's `new-session -As 0`) is replaced,
its clients switched over first; used sessions are kept. --dry-run,
--list, --from, --session.
e2e step 16 kills a server with a client attached like ghostty's, restarts
it, restores, and compares every window and pane; splitting the wrong
pane, not replacing bare sessions, not pinning, or keeping the saved size
each fail it. e2e keeps its state in its temp dir.
Diffstat (limited to 'tests/e2e.sh')
| -rwxr-xr-x | tests/e2e.sh | 73 |
1 files changed, 72 insertions, 1 deletions
diff --git a/tests/e2e.sh b/tests/e2e.sh index 983912c..425fdf3 100755 --- a/tests/e2e.sh +++ b/tests/e2e.sh @@ -29,13 +29,15 @@ PASSES=0 # Every dingger invocation below (daemon, focus, poke, menu bar) targets the # isolated server and the test daemon's hook socket. -export DINGGER_TMUX_SOCKET="$SOCK" DINGGER_AGENT_SOCKET="$HOOK" +export DINGGER_TMUX_SOCKET="$SOCK" DINGGER_AGENT_SOCKET="$HOOK" DINGGER_STATE_DIR="$DIR/state" t() { tmux -u -L "$SOCK" "$@"; } 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 rm -rf "$DIR" } trap cleanup EXIT @@ -345,6 +347,75 @@ else fail "menu bar binary not found at $MENUBAR" fi +# --- 16. restore after the tmux server goes away ------------------------------ +# 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" +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 "$@"; } +W="$DIR/w"; mkdir -p "$W/one" "$W/two" "$W/three" +start_viewer() { + r -f /dev/null new-session -d -s viewer -x 120 -y 40 \; set -g default-shell /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" +} +start_viewer +P0=$(r display -p -t '=0:' '#{pane_id}') +r rename-window -t "$P0" edit +P1=$(r split-window -h -t "$P0" -c "$W/two" -P -F '#{pane_id}') +r split-window -v -t "$P1" -c "$W/three" +r resize-pane -t "$P0" -x 30 +r new-window -d -t '=0:5' -c "$W/two" +r split-window -d -t '=0:5' -c "$W/three" +r resize-pane -Z -t '=0:5.1' +r select-window -t '=0:edit' +r new-session -d -s work -n grid -c "$W/three" -x 100 -y 30 +for d in one two three; do r split-window -d -t '=work:grid' -c "$W/$d"; r select-layout -t '=work:grid' tiled; done +r select-pane -t '=work:grid.2' +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 +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)\|' +} +BEFORE=$(fingerprint) +renv "$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 +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 +expect "the last snapshot is kept while tmux is down" "grep -c '\"name\": \"work\"' \"\$(latest_snapshot)\"" 1 +start_viewer +r new-session -d -s keep -n mine "sleep 100000" +expect "the new server has a bare 0" "r list-windows -t =0 -F '#{window_panes}' | tr '\n' ' '" "1 " +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 >"$DIR/restore.out" 2>&1 +expect "restore succeeds" "grep -c FAILED $DIR/restore.out" 0 +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 +expect "the candidate is marked restored" \ + "python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))[\"status\"])' $STATE2/restore.json" restored +r resize-window -t '=viewer:' -x 100 -y 30 +expect "restored windows follow the terminal's size again" "r display -p -t '=0:edit' '#{window_width}'" 100 +r resize-window -t '=viewer:' -x 120 -y 40 +expect "and back" "r display -p -t '=0:edit' '#{window_width}'" 120 +renv "$DINGGER" restore >"$DIR/restore2.out" 2>&1 +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; } + echo echo "$PASSES passed, $FAILS failed" [ "$FAILS" -eq 0 ] || { echo "--- daemon log tail"; tail -30 "$DIR/daemon.log"; exit 1; } |
