app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
master
John Lauer
selftest T10: name the window that took foreground. Running on John's own laptop while he worked, every click on his Ed…
53c5575
2mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
#!/usr/bin/env bash
# selftest.sh — THE full pup self-test (John, 2026-08-16: "do you have a FULL testing dev skill so
# you always know how to test yourself? build it and constantly run it.")
#
# Run AFTER EVERY SHIP + RESPAWN. Every check states its expectation and verdict; the run is RED if
# any check fails. Companion doc: dev-skills/pup-selftest/SKILL.md (what each check proves + the
# pixel-gate rules this script cannot automate).
#
# Usage: tools/selftest.sh [TARGET] (default AdomLapper)
set -u
TARGET="${1:-AdomLapper}"
export ADOM_AI_THREAD="${ADOM_AI_THREAD:-pup-selftest}"
export ADOM_CONTAINER_NAME="${ADOM_CONTAINER_NAME:-galliaApril}"
CLI="adom-bridge --target $TARGET"
REPO="$(cd "$(dirname "$0")/.." && pwd)"
PASS=0; FAIL=0
ok() { PASS=$((PASS+1)); echo " PASS: $*"; }
bad() { FAIL=$((FAIL+1)); echo " FAIL: $*"; }
J() { python3 -c "import sys,json;raw=sys.stdin.read();i=raw.find('{');d=json.loads(raw[i:]) if i>=0 else {};print(eval(\"$1\",{'d':d,'json':__import__('json')}))" 2>/dev/null; }
echo "== T1 STATIC: server parses + served dashboard script parses =="
node --check "$REPO/src/server.js" >/dev/null 2>&1 && ok "server.js parses" || bad "server.js does not parse"
node "$REPO/tests/check_dashboard_script.js" >/dev/null 2>&1 && ok "served dashboard <script> parses" || bad "dashboard script broken (the class that killed the Settings dialog twice)"
echo "== T2 UP-GATE: a real verb answers =="
UPV=""
for i in $(seq 1 15); do
UPV=$(timeout 25 $CLI pup_status '{}' 2>&1 | J "d.get('version') or ''")
[ -n "$UPV" ] && break; sleep 4
done
[ -n "$UPV" ] && ok "bridge answers, version=$UPV" || bad "bridge did not answer pup_status with a version (old build or down)"
LOCALV=$(cat "$REPO/src/BRIDGE_VERSION" 2>/dev/null)
[ "$UPV" = "$LOCALV" ] && ok "running version matches repo ($LOCALV)" || bad "running=$UPV repo=$LOCALV — STALE BRIDGE, install+respawn before trusting any other check"
echo "== T3 VERB SMOKE: open/navigate/eval/screenshot/close on own thread window =="
timeout 90 $CLI pup_open_window '{"sessionId":"st-web","url":"https://example.com","foreground":false}' >/dev/null 2>&1
R=$(timeout 60 $CLI pup_eval '{"sessionId":"st-web","expression":"document.title"}' 2>&1 | J "d.get('result') or ''")
echo "$R" | grep -qi example && ok "open+eval reads live DOM ('$R')" || bad "eval: '$R'"
SK=$(timeout 60 $CLI pup_screenshot '{"sessionId":"st-web"}' 2>&1 | J "d.get('sizeKB') or 0")
[ "${SK:-0}" -gt 5 ] 2>/dev/null && ok "screenshot captured (${SK}KB)" || bad "screenshot"
echo "== T4 SCROLL-REFLECT (#50 regression): eval scroll changes the screenshot =="
timeout 90 $CLI pup_open_window '{"sessionId":"st-tall","url":"https://en.wikipedia.org/wiki/Windows_11","foreground":false}' >/dev/null 2>&1
# A heavy page needs time to lay out before the FIRST shot; the old 5s only worked on a warm cache,
# and when a shot came back empty this reported "could not capture the scroll pair" with no evidence
# — the same blind-failure pattern T5 had. Now: longer settle, one retry, and it prints what it got.
shot() {
for a in 1 2; do
R=$(timeout 90 $CLI pup_screenshot '{"sessionId":"st-tall"}' 2>&1 | J "d.get('savedTo') or ''")
[ -n "$R" ] && { echo "$R"; return; }
sleep 4
done
echo ""
}
sleep 12
P1=$(shot)
timeout 60 $CLI pup_eval '{"sessionId":"st-tall","expression":"(window.scrollTo(0,4000),window.scrollY)"}' >/dev/null 2>&1
sleep 3
P2=$(shot)
if [ -n "$P1" ] && [ -n "$P2" ]; then
python3 - "$P1" "$P2" <<'SHOTCMP' && ok "scrolled screenshot differs from top" || bad "screenshots identical after scroll (#50 regressed)"
import sys,hashlib
from PIL import Image
a,b=(hashlib.md5(Image.open(p).tobytes()).hexdigest() for p in sys.argv[1:3])
sys.exit(0 if a!=b else 1)
SHOTCMP
else bad "could not capture the scroll pair (P1=${P1:-empty} P2=${P2:-empty}) — the screenshot verb returned no savedTo"; fi
echo "== T5 IDENTITY KEY: own window gets a pup AUMID registry key (mode-aware) =="
MODE=$(timeout 40 $CLI pup_status '{}' 2>&1 | J "(d.get('settings') or {}).get('taskbarMode') or 'unknown'")
echo " (live taskbarMode at check time: $MODE — the user may flip it mid-run, that is legitimate)"
# In tiles mode the key is the thread AUMID (…thread.pup-selftest…); in aumid mode it is the
# per-window category id (Adom.Pup.st-web.…). Either proves the stamp/registry path. In plain
# mode there is deliberately no key, so pass with a note. Never flip the user's mode to test.
sleep 20
# NOTE: keep this probe simple. A previous version piped reg through findstr and read the result
# through the J() eval helper; when it reported False the check gave NO evidence, and the finding
# ("stamp/registry path broken") was wrong — the keys were there. A check that can fail must print
# what it saw.
KEYRAW=$(timeout 60 $CLI shell_execute '{"command":"reg query HKCU\\Software\\Classes\\AppUserModelId /f Adom.Pup /k 2>nul","reason":"selftest: list pup AUMID keys"}' 2>&1 | python3 -c "
import sys,json
raw=sys.stdin.read();i=raw.find('{')
d=json.loads(raw[i:]) if i>=0 else {}
print((d.get('stdout') or '').replace(chr(13),' ').replace(chr(10),' '))" 2>/dev/null)
# The expected key carries the slug of the THREAD that opened the windows — and that thread is
# whatever ADOM_AI_THREAD is in force, which the CALLER may have exported (this check used to
# hardcode "pup-selftest" and reported "stamp/registry path broken" whenever the harness was run
# from a shell with its own thread name exported — a false alarm that cost a debugging cycle).
THREAD_SLUG=$(printf '%s' "$ADOM_AI_THREAD" | tr 'A-Z' 'a-z' | sed 's/[^a-z0-9]\+/-/g; s/^-//; s/-$//')
case "$KEYRAW" in *"$THREAD_SLUG"*|*st-web*|*st-tall*) KEY=True ;; *) KEY=False ;; esac
[ "$KEY" = "False" ] && { echo " expected a key containing: $THREAD_SLUG"; echo " probe saw: $KEYRAW"; }
if [ "$MODE" = "plain" ]; then
ok "plain mode: key state not asserted (registry keys without window properties are inert; what matters is the window property, which the converge pass owns)"
else
[ "$KEY" = "True" ] && ok "pup AUMID registry key exists for the selftest window ($MODE mode)" || bad "no pup AUMID key for the selftest window in $MODE mode (stamp/registry path broken)"
fi
echo "== T6 JUMP LIST: commit log line for the thread AUMID exists =="
timeout 60 $CLI pup_configure '{"jumpLists":"on"}' >/dev/null 2>&1
sleep 8
JL=$(timeout 60 $CLI bridge_log_read '{"name":"puppeteer","tail":600}' 2>&1 | python3 -c "
import sys,json
d=json.load(sys.stdin); log=str(d.get('log') or '')
print('YES' if any(('jumplist' in l and 'set ' in l and 'task' in l) for l in log.splitlines()[-600:]) else 'NO')")
MODE6=$(timeout 40 $CLI pup_status '{}' 2>&1 | J "(d.get('settings') or {}).get('taskbarMode') or 'unknown'")
if [ "$MODE6" = "plain" ]; then ok "plain mode at check time: no pup AUMIDs, jump lists correctly quiet"
elif [ "$JL" = "YES" ]; then ok "jump-list commits present in log"
else
# plain mode has NO pup AUMIDs, so zero commits is the CORRECT outcome — detect it by the registry
PK=$(timeout 60 $CLI shell_execute '{"command":"reg query HKCU\\Software\\Classes\\AppUserModelId /f Adom.Pup /k 2>nul","reason":"selftest: any pup AUMID keys registered?"}' 2>&1 | J "('Adom.Pup' in (d.get('stdout') or ''))")
if [ "$PK" = "False" ]; then ok "plain mode (zero pup AUMID keys): no jump lists to commit — correctly quiet"
else bad "no jump-list commit lines despite registered pup AUMIDs (menus will be bare Pin/Close — John's screenshot bug)"; fi
fi
echo "== T7 ENUM RATE: lastMin sane; steady-state near zero =="
E1=$(timeout 40 $CLI pup_status '{}' 2>&1 | J "(d.get('enumWindows') or {}).get('total',-1)")
sleep 45
E2=$(timeout 40 $CLI pup_status '{}' 2>&1 | J "(d.get('enumWindows') or {}).get('total',-1)")
D=$((E2-E1))
[ "$D" -le 2 ] 2>/dev/null && ok "steady-state enum: $D calls in 45s" || bad "enum churn: $D calls in 45s idle (spam class regressed)"
echo "== T8 OVERLAY TOGGLE: off clears, on repaints (bookkeeping check; PIXEL gate is manual) =="
timeout 60 $CLI pup_configure '{"overlayBadges":"off"}' >/dev/null 2>&1; sleep 6
timeout 60 $CLI pup_configure '{"overlayBadges":"on"}' >/dev/null 2>&1
ok "toggle roundtrip issued (verify pixels per the skill before claiming to John)"
echo "== T10 FOREGROUND STEAL (#56 guard): a COLD pup_dashboard must not take focus =="
# #56 was reported live (a window jumped in front of John while he typed) and no longer reproduces,
# so this check exists to make sure it cannot come back quietly. Focus a window WE own, cold the
# dashboard profile, call pup_dashboard as an AI caller, and assert the focused window never changed.
timeout 90 $CLI pup_close_window '{"sessionId":"pup-dashboard"}' >/dev/null 2>&1
timeout 60 $CLI shell_execute '{"command":"powershell -NoProfile -Command \"Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like \\\"*pup-dashboard*\\\" } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }\"","reason":"selftest T10: cold the dashboard profile so the launch is a genuine cold start"}' >/dev/null 2>&1
timeout 120 $CLI pup_open_window '{"sessionId":"st-fg","url":"https://example.com","foreground":false}' >/dev/null 2>&1
sleep 4
timeout 90 $CLI pup_raise_os_window '{"sessionId":"st-fg","foregroundReason":"selftest T10 guard for issue 56: this window must remain focused across a cold dashboard launch"}' >/dev/null 2>&1
sleep 3
FG_BEFORE=$(timeout 60 $CLI shell_execute '{"command":"powershell -NoProfile -Command \"Add-Type -Name FGA -Namespace W -MemberDefinition '"'"'[DllImport(\\\"user32.dll\\\")] public static extern IntPtr GetForegroundWindow();'"'"'; [W.FGA]::GetForegroundWindow()\"","reason":"selftest T10: foreground window before the cold dashboard launch"}' 2>&1 | J "(d.get('stdout') or '').strip()")
timeout 120 $CLI pup_dashboard '{}' >/dev/null 2>&1
sleep 6
FG_AFTER=$(timeout 60 $CLI shell_execute '{"command":"powershell -NoProfile -Command \"Add-Type -Name FGB -Namespace W -MemberDefinition '"'"'[DllImport(\\\"user32.dll\\\")] public static extern IntPtr GetForegroundWindow();'"'"'; [W.FGB]::GetForegroundWindow()\"","reason":"selftest T10: foreground window after the cold dashboard launch"}' 2>&1 | J "(d.get('stdout') or '').strip()")
# hwnd 0 means NOTHING was focused, so "unchanged" proves nothing — that is a vacuous pass, the
# same class of lie this harness keeps having to unlearn. Say so instead of banking it.
# A CHANGED foreground is not automatically a steal. This check ran on John's own laptop while he
# was working, and every time he clicked his Edge window mid-test it reported "#56 REGRESSED" — a
# false alarm naming a shipped fix, which is worse than no check at all. The accusation is specific
# ("stolen BY A COLD pup_dashboard"), so only pup taking the foreground can support it: name the new
# foreground window and fail only if it is pup's. Anything else is the human using their machine.
FG_AFTER_TITLE=$(timeout 60 $CLI shell_execute '{"command":"powershell -NoProfile -Command \"Add-Type -Name FGT -Namespace W -MemberDefinition '"'"'[DllImport(\\\"user32.dll\\\")] public static extern IntPtr GetForegroundWindow(); [DllImport(\\\"user32.dll\\\", CharSet=CharSet.Unicode)] public static extern int GetWindowTextW(IntPtr h, System.Text.StringBuilder s, int n);'"'"'; $h=[W.FGT]::GetForegroundWindow(); $sb=New-Object System.Text.StringBuilder 300; [void][W.FGT]::GetWindowTextW($h,$sb,300); $sb.ToString()\"","reason":"selftest T10: name the foreground window so a human focus change is not misreported as a pup steal"}' 2>&1 | J "(d.get('stdout') or '').strip()")
if [ "$FG_BEFORE" = "0" ] || [ -z "$FG_BEFORE" ]; then
bad "T10 INCONCLUSIVE: no window held focus before the launch (hwnd '${FG_BEFORE:-empty}'), so a steal could not be detected — raise a window and re-run"
elif [ "$FG_BEFORE" = "$FG_AFTER" ]; then ok "cold pup_dashboard did NOT steal focus (foreground hwnd $FG_BEFORE unchanged)"
elif printf '%s' "$FG_AFTER_TITLE" | grep -qi "Chrome for Testing"; then
bad "FOREGROUND STOLEN by a cold pup_dashboard (#56 regressed): $FG_BEFORE -> $FG_AFTER (\"$FG_AFTER_TITLE\")"
else
ok "cold pup_dashboard did NOT steal focus (focus moved to a NON-pup window, \"$FG_AFTER_TITLE\" — the human was using the machine, which is not a steal)"
fi
timeout 60 $CLI pup_close_window '{"sessionId":"st-fg"}' >/dev/null 2>&1
timeout 60 $CLI pup_close_window '{"sessionId":"pup-dashboard"}' >/dev/null 2>&1
echo "== T9 CLEANUP: close test windows; nothing owned by pup-selftest remains; keys reaped =="
timeout 60 $CLI pup_close_window '{"sessionId":"st-web"}' >/dev/null 2>&1
timeout 60 $CLI pup_close_window '{"sessionId":"st-tall"}' >/dev/null 2>&1
sleep 5
LEFT=$(timeout 40 $CLI pup_list_windows '{}' 2>&1 | python3 -c "
import sys,json;raw=sys.stdin.read();i=raw.find('{');d=json.loads(raw[i:]) if i>=0 else {}
print(sum(1 for s in (d.get('sessions') or []) if s.get('owner')=='pup-selftest'))")
[ "$LEFT" = "0" ] && ok "no selftest windows left open" || bad "$LEFT selftest window(s) LEFT OPEN (John's rule: never leave test windows)"
echo
echo "== RESULT: $PASS passed, $FAIL failed =="
[ "$FAIL" -eq 0 ] && echo "GREEN — pup core features verified on $TARGET" || echo "RED — fix before telling the user anything works"
exit $FAIL