aboutsummaryrefslogtreecommitdiff
path: root/tests/pi-live.sh
blob: 059a92b51a4fa006be43578da93873f08cfa3be7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#!/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 <pi binary> [dingger binary]
#
# Covers: identification, the extension's state and session reports, a turn
# (working, then done), /reload, an extension's blocked event, /importance, /unread,
# 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)
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" <<EOF
{"providers": {"fake": {"baseUrl": "http://127.0.0.1:$PORT/v1", "api": "openai-completions", "apiKey": "fake", "models": [{"id": "fake-model"}]}}}
EOF
  echo '{"defaultProvider": "fake", "defaultModel": "fake-model"}' >"$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"
# 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"
"$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
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 "pi says so" "t capture-pane -p -t main:agent | grep -c 'importance: high'" 1 30
expect "and adds no footer line" "t capture-pane -p -t main:agent | grep -v '^ *$' | tail -1 | grep -c 'importance'" 0
expect "saved in the session file" "grep -c '\"customType\":\"dingger-importance\"' \"\$(t show -pv -t main:agent @agent_session)\"" 1 30
# Seen (what a jump does; no client is attached here), then marked unread.
t set -p -t main:agent @dingger_ack 1; "$DINGGER" poke
expect "seen: idle" "st agent" idle 50
type_line agent "/unread"
expect "/unread: done again" "st agent" done 50
# Not a turn (an unknown command would be sent to the model, and also end
# done): the daemon applied a mark, and pi said so.
expect "by a mark" "sed 's/\x1b\[[0-9;]*m//g' $D/daemon.log | grep -c 'mark unread target=main:agent applied=true'" 1 30
expect "pi says so" "t capture-pane -p -t main:agent | grep -c 'marked unread: dingger shows it as done'" 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
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
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
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; }