#!/bin/bash # Live check of dingger against a real pi binary, on a tmux server and # dingger daemon of its own, with a fake local model (tests/fake-model.py): # no network, no credentials, nothing of the user's pi touched (pi gets its # own agent directory). # # tests/pi-live.sh [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. set -u PI_BIN=$(cd "$(dirname "$1")" && pwd)/$(basename "$1") ROOT=$(cd "$(dirname "$0")/.." && pwd) DINGGER="${2:-$ROOT/target/release/dingger}" case "$DINGGER" in /*) ;; *) DINGGER="$PWD/$DINGGER" ;; esac D=$(mktemp -d /tmp/dingger-pi-live.XXXXXX) R="dingger-pi-live-$$" PASSES=0; FAILS=0 pass() { PASSES=$((PASSES + 1)); echo " ok $1"; } fail() { FAILS=$((FAILS + 1)); echo " FAIL $1"; } expect() { # description command expected [tries of 0.1s, default 50] local desc=$1 cmd=$2 want=$3 tries=${4:-50} got for _ in $(seq "$tries"); do got=$(eval "$cmd") [ "$got" = "$want" ] && { pass "$desc"; return; } sleep 0.1 done fail "$desc: want [$want] got [$got]" } note() { echo " note $1"; } t() { tmux -u -L "$R" "$@"; } cleanup() { t kill-server 2>/dev/null [ -n "${DAEMON_PID:-}" ] && kill "$DAEMON_PID" 2>/dev/null [ -n "${FAKE_PID:-}" ] && kill "$FAKE_PID" 2>/dev/null rm -rf "$D" } trap cleanup EXIT echo "pi $("$PI_BIN" --version 2>&1 | head -1), dingger $DINGGER" python3 "$ROOT/tests/fake-model.py" >"$D/port" 2>"$D/fake.err" & FAKE_PID=$! for _ in $(seq 50); do [ -s "$D/port" ] && break; sleep 0.1; done PORT=$(cat "$D/port") # Two agent directories: one with dingger's extension (and a test extension # that blocks the way extensions do), one without (screen detection). for agent in agent agent-plain; do mkdir -p "$D/$agent/extensions" cat >"$D/$agent/models.json" <"$D/$agent/settings.json" done PI_CODING_AGENT_DIR="$D/agent" "$DINGGER" install pi >/dev/null cat >"$D/agent/extensions/test-blocked.ts" <<'EOF' export default function (pi) { pi.registerCommand("block", { description: "test: emit blocked", handler: async () => { pi.events.emit("blocked", { active: true, label: "test block" }); } }); pi.registerCommand("unblock", { description: "test: clear blocked", handler: async () => { pi.events.emit("blocked", { active: false }); } }); pi.registerCommand("ask", { description: "test: a confirm dialog", handler: async (_args, ctx) => { await ctx.ui.confirm("Test dialog", "Proceed?"); } }); } EOF mkdir -p "$D/bin" "$D/work" "$D/work2" 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 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" "$DINGGER" watch --interval-secs 1 --no-sounds --no-notifications >"$D/daemon.log" 2>&1 & DAEMON_PID=$! t send-keys -t main:agent "command -v pi > $D/which-pi" Enter for _ in $(seq 30); do [ -s "$D/which-pi" ] && break; sleep 0.1; done if [ "$(cat "$D/which-pi")" != "$D/bin/pi" ]; then echo "the test panes would run $(cat "$D/which-pi"), not $PI_BIN; stopping"; exit 1 fi # --no-mcp (pi 1.0.4+): no MCP servers, whatever the user configured. NO_MCP=$("$PI_BIN" --help 2>&1 | grep -q -- --no-mcp && echo --no-mcp) st() { t show -pv -t "main:$1" @agent_state 2>/dev/null; } type_line() { t send-keys -t "main:$1" -l "$2"; sleep 0.3; t send-keys -t "main:$1" Enter; } echo "1. the extension's reports" t send-keys -t main:agent "clear; PI_CODING_AGENT_DIR=$D/agent pi $NO_MCP" Enter expect "identified as pi" "t show -pv -t main:agent @agent 2>/dev/null" pi 150 expect "idle at start" "st agent" idle 100 expect "the extension reported (state from the hook)" "t show -pv -t main:agent @dingger_hook 2>/dev/null" idle 100 expect "its session file is on the pane" "t show -pv -t main:agent @agent_session 2>/dev/null | grep -c '^$D/agent/sessions/.*\.jsonl$'" 1 100 type_line agent "slow 3 hello" expect "a turn: working" "st agent" working 50 expect "then done (nobody watching)" "st agent" done 80 expect "the session file is written" "[ -s \"\$(t show -pv -t main:agent @agent_session)\" ] && echo yes" yes type_line agent "/reload" sleep 2 type_line agent "slow 2 again" expect "after /reload: working" "st agent" working 50 expect "and done" "st agent" done 60 type_line agent "/block" expect "an extension's blocked event: blocked" "st agent" blocked 50 type_line agent "/unblock" expect "cleared: back to done" "st agent" done 50 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 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 expect "identified as pi" "t show -pv -t main:plain @agent 2>/dev/null" pi 150 expect "idle at start" "st plain" idle 100 expect "no hook state" "t show -pv -t main:plain @dingger_hook 2>/dev/null" "" type_line plain "slow 4 hello" expect "a turn: working, from the screen" "st plain" working 50 expect "then done" "st plain" done 80 echo "3. restore" SESSION=$(t show -pv -t main:agent @agent_session) latest() { ls "$D"/state/snapshots/*.json 2>/dev/null | tail -1; } expect "the snapshot has the agent's session" "grep -c \"\\\"session\\\": \\\"$SESSION\\\"\" \"\$(latest)\" 2>/dev/null" 1 80 t kill-server expect "the layout becomes the restore candidate" "python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))[\"status\"])' $D/state/restore.json 2>/dev/null" offered 80 "$DINGGER" restore >"$D/restore.out" 2>&1 expect "restore resumes it by its session file" "grep -c -- \"resume .*pi --session\" $D/restore.out" 1 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 echo "4. exit" t send-keys -t main:agent C-d expect "the agent is gone from the pane" "t show -pv -t main:agent @agent 2>/dev/null" "" 100 echo echo "$PASSES passed, $FAILS failed" [ "$FAILS" -eq 0 ] || { echo "--- restore output:"; cat "$D/restore.out" 2>/dev/null; echo "--- daemon log tail:"; tail -15 "$D/daemon.log" | sed 's/\x1b\[[0-9;]*m//g' | cut -c29-160; exit 1; }