diff options
| author | Igor Soarez <igor@soarez.org> | 2026-10-26 16:32:18 +0200 |
|---|---|---|
| committer | Igor Soarez <igor@soarez.org> | 2026-10-26 16:32:18 +0200 |
| commit | 185af91b4d525db6c192a050b53615772fb5fe11 (patch) | |
| tree | 6db92db4042dddcc5fee13d02943447ba9df5f2d /tests | |
| parent | 1eb0ac3705276c6f81f44487c7e99c24a302d4d5 (diff) | |
importance: mark an agent high, normal, or low
within a status, more important agents are listed first, and so jumped to
first; status stays first. a high agent shows a bold ! before its name, a
low agent's name is lightly dimmed with "low" on its second line, and it
makes no sound (it still notifies and waits in line).
in pi, /importance high|normal|low (completes the levels; none once one is
typed out, so Enter runs it). the extension saves it in the session as a
custom entry (dingger-importance), so /reload, restarts, /resume and
restore bring it back; /new starts at normal. it shows a level other than
normal in pi's footer, emits dingger:importance { level } for other
extensions, and sends it with every report. for other agents, `dingger
importance [level] [--pane]`; it refuses a pi that reports itself, whose
saved level would win.
the daemon stays the one writer of pane state: it writes
@dingger_importance (unset for normal), which goes with the agent and
survives a daemon restart. `dingger status` marks high and low.
tests: e2e step 21 (order, the option, no sound for low, the menu bar,
the command, the extension through a harness that now matches pi's
extension API), a DINGGER_SOUND seam so sounds are observable, and a
status-line check that read the counts once (flaky) now polls.
pi-live: /importance in real pi 1.4, its footer and session entry, and the
level after a restore; every pi there now uses the test's agent directory
(a restored pi read the user's). negative controls fail each part.
Diffstat (limited to 'tests')
| -rwxr-xr-x | tests/e2e.sh | 146 | ||||
| -rwxr-xr-x | tests/pi-live.sh | 15 |
2 files changed, 148 insertions, 13 deletions
diff --git a/tests/e2e.sh b/tests/e2e.sh index f9fcf18..20ce5dc 100755 --- a/tests/e2e.sh +++ b/tests/e2e.sh @@ -43,11 +43,13 @@ cleanup() { trap cleanup EXIT # Notifications are on, delivered by a fake notifier that takes 3s like the -# real one can; the UI must not wait for it. +# real one can; the UI must not wait for it. Sounds are on, played by a fake +# player that logs them. NOTES="$DIR/notifications" +SOUNDS="$DIR/sounds" start_daemon() { - DINGGER_NOTIFIER="sleep 3; echo \"\$0|\$1\" >> $NOTES" RUST_LOG=debug \ - "$DINGGER" watch --interval-secs 30 --no-sounds >>"$DIR/daemon.log" 2>&1 & + DINGGER_NOTIFIER="sleep 3; echo \"\$0|\$1\" >> $NOTES" DINGGER_SOUND="echo \"\$0\" >> $SOUNDS" RUST_LOG=debug \ + "$DINGGER" watch --interval-secs 30 >>"$DIR/daemon.log" 2>&1 & DAEMON_PID=$! for _ in $(seq 50); do [ -S "$HOOK" ] && return; sleep 0.1; done echo "daemon did not start"; cat "$DIR/daemon.log"; exit 1 @@ -223,7 +225,12 @@ hook "$A1" working; hook "$A1" idle expect "A2 blocked" "opt $A2 @agent_state" blocked expect "A1 done" "opt $A1 @agent_state" done t source-file "$ROOT/packaging/tmux/dingger.tmux" -segment=$(t display -p -c "$CLIENT" '#{T:status-right}' | sed 's/#\[[^]]*\]//g') +# The counts are published right after the state: poll, like expect. +for _ in $(seq 40); do + segment=$(t display -p -c "$CLIENT" '#{T:status-right}' | sed 's/#\[[^]]*\]//g') + case "$segment" in *"×1 ✓1") break ;; esac + sleep 0.1 +done 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 "focus hook installed: learns the terminal window" "t show-hooks -g client-focus-in | grep -c 'dingger terminal focused'" 1 @@ -272,13 +279,43 @@ fi # --- 12. the shipped pi extension reports for its own pane ------------------- echo "12. pi extension" cat >"$DIR/ext-harness.mjs" <<'JS' -// Loads the shipped extension with a fake pi and fires lifecycle events. +// Loads the shipped extension with a fake pi and fires lifecycle events +// ("agent_start"), runs commands ("/importance high"), and asks for argument +// completions ("complete:importance h"). Custom session entries persist in +// $SESSION_ENTRIES, so a later run reads them back as the session; what the +// extension shows and emits goes to $EXT_LOG. +import fs from "node:fs"; const ext = await import(process.env.EXTENSION); -const on = {}; -ext.default({ on: (name, fn) => (on[name] = fn), events: { on: () => {} } }); -const ctx = { mode: "tui", isIdle: () => true }; -for (const event of process.argv.slice(2)) { - await on[event]?.({ type: event, reason: "reload" }, ctx); +const on = {}, commands = {}; +const store = process.env.SESSION_ENTRIES; +const entries = store && fs.existsSync(store) ? JSON.parse(fs.readFileSync(store, "utf8")) : []; +const log = (line) => process.env.EXT_LOG && fs.appendFileSync(process.env.EXT_LOG, line + "\n"); +ext.default({ + on: (name, fn) => (on[name] = fn), + registerCommand: (name, command) => (commands[name] = command), + appendEntry: (customType, data) => { + entries.push({ type: "custom", customType, data }); + if (store) fs.writeFileSync(store, JSON.stringify(entries)); + }, + events: { on: () => {}, emit: (name, data) => log(`event ${name} ${JSON.stringify(data)}`) }, +}); +const ctx = { + mode: "tui", + isIdle: () => true, + sessionManager: { getEntries: () => entries, getSessionFile: () => undefined }, + ui: { setStatus: (key, text) => log(`status ${key} ${text ?? ""}`), notify: (text) => log(`notify ${text}`) }, +}; +for (const arg of process.argv.slice(2)) { + if (arg.startsWith("/")) { + const [name, ...rest] = arg.slice(1).split(" "); + await commands[name]?.handler(rest.join(" "), ctx); + } else if (arg.startsWith("complete:")) { + const [name, ...rest] = arg.slice("complete:".length).split(" "); + const items = await commands[name]?.getArgumentCompletions?.(rest.join(" ")); + log(`complete ${(items ?? []).map((item) => item.value).join(" ")}`); + } else { + await on[arg]?.({ type: arg, reason: "reload" }, ctx); + } await new Promise((r) => setTimeout(r, 300)); } JS @@ -793,6 +830,95 @@ renv "$DINGGER" focus --client "$TTY1" --pane "$(r display -p -t '=grp:agent' '# expect "a window on a session group stays on its own session" "client_session $TTY1" grp-b expect "showing the agent" "r display -p -c $TTY1 '#{window_name}'" agent +# --- 21. importance -------------------------------------------------------- +# Fresh agents on the first server (its daemon polls every 30 s: updates +# arrive by reports and pokes). Nobody looks at session imp. +echo "21. importance" +report() { # pane state importance (either may be empty) + python3 - "$HOOK" "$1" "$2" "$3" <<'PY' +import json, socket, sys +params = {"pane_id": sys.argv[2], "agent": "pi"} +if sys.argv[3]: params["state"] = sys.argv[3] +if sys.argv[4]: params["importance"] = sys.argv[4] +s = socket.socket(socket.AF_UNIX) +s.connect(sys.argv[1]) +s.sendall((json.dumps({"method": "pane.report_agent", "params": params}) + "\n").encode()) +s.close() +PY +} +t new-session -d -s imp -n i1 -c "$DIR" -x 120 -y 40 "$DIR/bin/pi" +t new-window -d -t imp: -n i2 -c "$DIR" "$DIR/bin/pi" +t new-window -d -t imp: -n i3 -c "$DIR" "$DIR/bin/pi" +t new-window -d -t imp: -n i4 -c "$DIR" "$DIR/bin/pi" +I1=$(t display -p -t imp:i1 '#{pane_id}'); I2=$(t display -p -t imp:i2 '#{pane_id}') +I3=$(t display -p -t imp:i3 '#{pane_id}'); I4=$(t display -p -t imp:i4 '#{pane_id}') +iqueue() { gopt @dingger_queue | tr ',' '\n' | grep -xE "$I1|$I2|$I3" | tr '\n' ' '; } +for p in "$I1" "$I2" "$I3"; do report "$p" working ""; expect "imp: $p working" "opt $p @agent_state" working; done +for p in "$I1" "$I2" "$I3"; do report "$p" idle ""; expect "imp: $p done" "opt $p @agent_state" done; sleep 0.05; done +expect "same importance: oldest done first" "iqueue" "$I1 $I2 $I3 " +report "$I3" "" high +expect "a high agent's pane option" "opt $I3 @dingger_importance" high +expect "importance before time: the high agent first" "iqueue" "$I3 $I1 $I2 " +expect "and its status unchanged (a report without state)" "opt $I3 @agent_state" done +report "$I1" "" low +expect "a low agent last" "iqueue" "$I3 $I2 $I1 " +report "$I3" "" normal +expect "normal leaves the option unset" "opt $I3 @dingger_importance" "" +expect "and the order by time again" "iqueue" "$I2 $I3 $I1 " +report "$I3" "" high +expect "imp: high again" "iqueue" "$I3 $I2 $I1 " +report "$I1" blocked "" +expect "imp: a low agent blocked" "opt $I1 @agent_state" blocked +expect "status before importance: the blocked low agent first" "iqueue" "$I1 $I3 $I2 " +report "$I1" working "" +expect "imp: working again" "opt $I1 @agent_state" working +# Let every alert so far land (the fake notifier takes 3 s). +sleep 3.5; : >"$SOUNDS"; : >"$NOTES" +report "$I1" working ""; report "$I1" idle "" +expect "a low agent finishing" "opt $I1 @agent_state" done +sleep 0.5 +expect "makes no sound" "cat $SOUNDS" "" +expect "but still notifies" "grep -c '^pi finished|imp:i1' $NOTES" 1 60 +report "$I2" working ""; report "$I2" idle "" +expect "a normal agent finishing sounds" "cat $SOUNDS" done +mb() { DINGGER_TMUX_SOCKET="$SOCK" DINGGER_BIN="$DINGGER" "$MENUBAR" "$@"; } +expect "menu: a high agent is marked" "mb --print | grep -F 'imp · i3' | cut -f2" "! pi" +expect "menu: a low agent says so" "mb --print | grep -F 'imp · i1' | cut -f3" "imp · i1 · pi · low · done now" +expect "menu: high, normal, low among the done" \ + "mb --print | grep -F 'imp · ' | grep '^✓' | cut -f3 | cut -d' ' -f3 | tr '\n' ' '" "i3 i2 i1 " +expect "dingger importance shows it" "$DINGGER importance --pane $I1" low +expect "a pi that reports itself is set in pi" \ + "$DINGGER importance high --pane $I1 2>&1" "dingger: this pi reports to dingger: use /importance in it (pi saves it with the session)" +expect "dingger importance needs an agent" "$DINGGER importance high --pane \$(t display -p -t viewer: '#{pane_id}') 2>&1 | cut -c1-24" "dingger: no agent in pan" +$DINGGER poke; expect "imp: the plain pi is identified" "opt $I4 @agent" pi +expect "dingger importance sets another agent's" "$DINGGER importance high --pane $I4" "importance: high" +expect "the daemon writes it" "opt $I4 @dingger_importance" high +t respawn-pane -k -t "$I4" "sleep 100000"; $DINGGER poke +expect "it goes with the agent" "opt $I4 @dingger_importance" "" +kill "$DAEMON_PID"; sleep 0.3; start_daemon; $DINGGER poke +sleep 1 +expect "a daemon restart keeps it" "opt $I3 @dingger_importance" high +expect "and the order" "iqueue" "$I3 $I2 $I1 " +if command -v node >/dev/null; then + ENTRIES="$DIR/entries.json"; EXTLOG="$DIR/ext.log" + iext() { SESSION_ENTRIES="$ENTRIES" EXT_LOG="$EXTLOG" ext "$@"; } + iext "$I2" session_start "/importance high" + expect "/importance high reaches dingger" "opt $I2 @dingger_importance" high + expect "saved in the session" "grep -c '\"customType\":\"dingger-importance\",\"data\":{\"level\":\"high\"}' $ENTRIES" 1 + expect "shown in pi's footer" "grep -c '^status dingger-importance importance: high$' $EXTLOG" 1 + expect "announced to other extensions" "grep -c '^event dingger:importance {\"level\":\"high\"}$' $EXTLOG" 1 + report "$I2" "" normal + expect "imp: set back by hand" "opt $I2 @dingger_importance" "" + iext "$I2" session_start + expect "a new pi on the session reports it again" "opt $I2 @dingger_importance" high + # pi starts a session before any command runs. + : >"$EXTLOG"; iext "$I2" session_start "/importance urgent" "complete:importance h" "/importance normal" + expect "an unknown level is refused" "grep -c '^notify importance is high, normal, or low$' $EXTLOG" 1 + expect "levels complete" "grep '^complete' $EXTLOG" "complete high" + expect "normal reaches dingger" "opt $I2 @dingger_importance" "" + expect "and leaves the footer" "grep -c '^status dingger-importance $' $EXTLOG" 1 +fi + # 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 diff --git a/tests/pi-live.sh b/tests/pi-live.sh index de35bc5..3f4b4b8 100755 --- a/tests/pi-live.sh +++ b/tests/pi-live.sh @@ -7,8 +7,9 @@ # tests/pi-live.sh <pi binary> [dingger binary] # # Covers: identification, the extension's state and session reports, a turn -# (working, then done), /reload, an extension's blocked event, screen -# detection for a pi without the extension, restore (`pi --session`), exit. +# (working, then done), /reload, an extension's blocked event, /importance, +# screen detection for a pi without the extension, restore (`pi --session`, +# importance included), exit. set -u PI_BIN=$(cd "$(dirname "$1")" && pwd)/$(basename "$1") ROOT=$(cd "$(dirname "$0")/.." && pwd) @@ -65,7 +66,9 @@ ln -s "$PI_BIN" "$D/bin/pi" # Plain shells: a login shell would read the user's profile, which puts the # user's own pi first on PATH. printf 'set -g default-shell /bin/sh\nset -g default-command /bin/sh\n' >"$D/tmux.conf" -export PATH="$D/bin:$PATH" PI_OFFLINE=1 PI_SKIP_VERSION_CHECK=1 PI_TELEMETRY=0 +# Every pi here uses the test's agent directory, including one a restore +# starts (`pi --session`, without the command line's own variables). +export PATH="$D/bin:$PATH" PI_CODING_AGENT_DIR="$D/agent" PI_OFFLINE=1 PI_SKIP_VERSION_CHECK=1 PI_TELEMETRY=0 export DINGGER_TMUX_SOCKET="$R" DINGGER_AGENT_SOCKET="$D/hook.sock" DINGGER_STATE_DIR="$D/state" DINGGER_TMUX_CONFIG="$D/tmux.conf" t -f "$D/tmux.conf" new-session -d -s main -n agent -x 140 -y 40 -c "$D/work" t new-window -d -t main: -n plain -c "$D/work2" @@ -104,6 +107,11 @@ type_line agent "/ask" sleep 1.5 note "during a pi confirm dialog dingger shows: $(st agent) (pi gives extensions no dialog event)" t send-keys -t main:agent Escape +sleep 0.5 +type_line agent "/importance high" +expect "/importance high: the pane option" "t show -pv -t main:agent @dingger_importance 2>/dev/null" high 50 +expect "and pi's footer (its last line)" "t capture-pane -p -t main:agent | grep -v '^ *$' | tail -1 | sed 's/^ *//'" "importance: high" 30 +expect "saved in the session file" "grep -c '\"customType\":\"dingger-importance\"' \"\$(t show -pv -t main:agent @agent_session)\"" 1 30 echo "2. screen detection (pi without the extension)" t send-keys -t main:plain "PI_CODING_AGENT_DIR=$D/agent-plain pi $NO_MCP" Enter @@ -125,6 +133,7 @@ expect "restore resumes it by its session file" "grep -c -- \"resume .*pi --sess expect "resumed 1 of 1 agents" "grep -c '^resumed 1 of 1 agents' $D/restore.out" 1 expect "it runs again, reporting the same session" "t show -pv -t main:agent @agent_session 2>/dev/null" "$SESSION" 150 expect "with its conversation" "t capture-pane -p -t main:agent -S - | grep -c 'slow 3 hello'" 1 50 +expect "and its importance" "t show -pv -t main:agent @dingger_importance 2>/dev/null" high 50 echo "4. exit" t send-keys -t main:agent C-d |
