aboutsummaryrefslogtreecommitdiff
path: root/tests/pi-live.sh
blob: 3f4b4b835a1e9f536044f506ab32242b6e746842 (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
#!/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,
# 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 "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
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; }