master
John Lauer Add the churn ralph harness for #46: opens real windows, drives rescan + quit-idle + takeover-close + respawn churn, an… e93aa85 2mo ago
#!/usr/bin/env bash
# churn-ralph.sh — the destructive-kill safety harness for pup-bridge#46.
#
# The #46 incident: pup's janitor killed the browser hosting John's live, visible windows after a
# FALSE "no open windows" verdict during heavy session churn. The 2.0.130 fix made every
# destructive path verify OS ground truth (a visible top-level window = never an orphan). This
# harness proves that invariant: it opens real windows, records their OS hwnds, then drives the
# exact churn that triggered #46 — rescan (adoption), takeover-close, quit-idle, reconcile — and
# after each cycle asserts EVERY window the test still wants is alive + visible on the desktop.
# A single vanished live window = FAIL.
#
# Usage:  tools/churn-ralph.sh [TARGET]      (TARGET defaults to AdomLapper)
# Runs against the live desktop; opens BACKGROUND windows and cleans them up at the end.
set -u
TARGET="${1:-AdomLapper}"
export ADOM_AI_THREAD="${ADOM_AI_THREAD:-churn-ralph}"
export ADOM_CONTAINER_NAME="${ADOM_CONTAINER_NAME:-galliaApril}"
CLI="adom-bridge-cli --target $TARGET"
PASS=0; FAIL=0

jqget() { python3 -c "import sys,json;d=json.load(sys.stdin);print(d$1)" 2>/dev/null; }
say()  { echo -e "$*"; }
ok()   { PASS=$((PASS+1)); echo "  PASS: $*"; }
bad()  { FAIL=$((FAIL+1)); echo "  FAIL: $*"; }

# Is this window alive + present on the desktop? GROUND TRUTH, independent of pup's session model:
# ask the bridge for the window's OS hwnd (pup_window_info), then capture that hwnd directly with
# ab's desktop_screenshot_window. If #46's bug killed the browser process, the hwnd is gone and the
# OS capture fails — exactly the failure the user experiences (window vanished, cannot be shot).
# The title-tag search this used before was unreliable because the title tag is OFF by default.
win_visible() {
  local sid="$1"
  local hw
  hw=$(timeout 30 $CLI pup_window_info "{\"sessionId\":\"$sid\"}" 2>/dev/null | jqget "['hwnd'] or ''")
  [ -z "$hw" ] && { echo "no"; return; }
  timeout 30 $CLI desktop_screenshot_window "{\"hwnd\":$hw}" 2>/dev/null | python3 -c "import sys,json
d=json.load(sys.stdin)
o=d.get('output')
try: o=json.loads(o) if isinstance(o,str) else (o or {})
except: o={}
shot = d.get('savedTo') or (o or {}).get('savedTo') or [s for s in ((o or {}).get('screenshots') or []) if s.get('savedTo')]
print('yes' if shot else 'no')" 2>/dev/null
}

open_win() {
  local sid="$1" url="$2"
  timeout 90 $CLI pup_open_window "{\"sessionId\":\"$sid\",\"owner\":\"churn-ralph\",\"url\":\"$url\"}" >/dev/null 2>&1
}

SIDS=(churn-a churn-b churn-c)
URLS=(https://example.com https://example.org https://example.net)

say "== churn-ralph (pup-bridge#46 safety harness) =="
say "Opening ${#SIDS[@]} windows..."
for i in 0 1 2; do open_win "${SIDS[$i]}" "${URLS[$i]}"; done
sleep 6

# Baseline: all three must be visible on the desktop.
for s in "${SIDS[@]}"; do
  [ "$(win_visible "$s")" = "yes" ] && ok "$s visible at baseline" || bad "$s NOT visible at baseline (open failed?)"
done

churn_cycle() {
  local label="$1"
  say "-- churn cycle: $label --"
  # (a) rescan: exercises the adoption/reclaim path that must never adopt a live window twice
  timeout 40 $CLI pup_rescan '{}' >/dev/null 2>&1
  # (b) quit-idle: the verb that in #46 judged a live-window browser idle and killed it
  timeout 60 $CLI pup_quit_idle_browsers '{}' >/dev/null 2>&1
  # (c) takeover-close ONE window, then immediately reopen it — the adopt/close/respawn race
  timeout 45 $CLI pup_close_window "{\"sessionId\":\"churn-c\",\"takeover\":true}" >/dev/null 2>&1
  open_win churn-c "${URLS[2]}"
  sleep 6
  # ASSERT: the two we never touched (a,b) MUST still be alive+visible — the #46 regression is them
  # vanishing when churn made the model briefly think they were orphaned.
  for s in churn-a churn-b; do
    [ "$(win_visible "$s")" = "yes" ] && ok "[$label] $s survived churn (alive+visible)" \
                                       || bad "[$label] $s WAS KILLED during churn (#46 regression!)"
  done
  # And the reopened one is back.
  [ "$(win_visible churn-c)" = "yes" ] && ok "[$label] churn-c reopened fine" \
                                        || bad "[$label] churn-c did not come back"
}

churn_cycle "round-1"
churn_cycle "round-2"
churn_cycle "round-3"

say "Cleaning up..."
for s in "${SIDS[@]}"; do timeout 45 $CLI pup_close_window "{\"sessionId\":\"$s\",\"takeover\":true}" >/dev/null 2>&1; done

say ""
say "== RESULT: $PASS passed, $FAIL failed =="
[ "$FAIL" -eq 0 ] && { say "GREEN — no live window was ever killed under churn (#46 invariant holds)."; exit 0; } \
                  || { say "RED — a live window vanished under churn; #46 has regressed."; exit 1; }