Files
openswarm/e2e/browser-v3/stack.sh
ciregenzandClaude Opus 5 3c50359146 [eric] browser-v3: fix the instrument first, then the two composer bugs it could finally see
Six of the eleven defects here were in the MEASUREMENT, not the product, and they
were wrong in both directions.

Harness, all of which silently produced wrong numbers:
- coverage.py preflight refused every sweep on a box holding exactly one backend:
  stack.sh's supervisor is a `bash -c` quoting the whole uvicorn line, so it carries
  both "-m uvicorn backend.main" AND the venv python path. Discriminate on POSITION.
- stack.sh status reported 2 backends over 1 and 0 webpack over a live dev server
  (webpack retitles its process). A status check whose job is preventing a second
  stack, failing in the direction that lets one land.
- c7_run.sh/c8_run.sh slice r6_be.log while stack.sh names logs by TAG: a stack under
  any other tag hands every trial an empty slice and the sweep reports a confident
  0/108. Now refuses loudly; it caught this exact mistake on first use.
- "Browser command timed out" was bucketed infra. It is ONE command blowing its own
  budget, not a dead webview: all 4 such rows were BrowserFindComposer at exactly its
  30s cap, every run completed after, zero card-gone markers in the whole log. Filed
  as infra it read as 11.8% flake AND lifted holdout reach 70% -> 84%.
- api_retry / rate_limit_error now grade as infra. A provider 429 storm turned clean
  15-21s exclusions into 188s product_no_composer rows.
- bench.py prints reach BOTH ways when a row is UNVERIFIED. An exclusion resting on
  the agent's own word quietly flatters the score, and coverage.py's own instruction
  to confirm it by hand goes unread (I quoted a 100% that excluded onlinegdb).

Timing was measuring 0.2% of the run: prestage completes BEFORE metrics_started_at,
so other_ms was 25ms of a 12700ms median while prestage (4146ms, ~61%) sat in no
bucket at all. prestage_ms/task_ms are now recorded; total_ms is deliberately NOT
redefined, which would invalidate every before/after already taken against it.

Product:
- find_composer rejected ACE/CodeMirror-5/Monaco composers. Their input is a ~1x1
  offscreen textarea that paints into a sibling div, so it can never pass a size
  gate. Accept it when a VISIBLE ancestor is composer-sized; honeypots stay out
  because the input itself must not be display:none/visibility:hidden/opacity:0.
  anon reach 80% -> 100%, holdout 89% -> 90%, p95 38.6s -> 9.7s.
- the composer poll slept a blind 0+1.2+1.4 = 2.6s whenever prestage staged nothing,
  which is nearly every run, and it was the whole of other_ms's suspicious constancy
  (2610-2613ms regardless of tools_ms). Stop when two reads are identical, the rule
  the opener poll 40 lines below already applies. other_ms -53.7%, tools_ms flat.
- prestage no longer navigates to the page it is already on, nor sleeps 0.35s before
  its first settle probe.
- is_replay_boundary reasoned from the NAME alone, so x.com's composer textbox named
  "Post text" was ruled an irreversible send and truncated its replay to a bare
  navigate. Excluded by ROLE; first_unsafe_step now passes role through at all.

Measured on this box: reach 100% (83% if onlinegdb's unverified exclusion is bogus),
0 false successes in ~155 runs, prestage tier-0/1 2702ms, other_ms -53.7%, infra
flake 0/158, holdout 18/20. Criteria 2/4/9 need live writes and are untouched.
Full evidence, including what did NOT work, in e2e/browser-v3/RESULTS_2026-08-06.md.

Also drops the tracked electron/node_modules symlink pointing at another machine's
Downloads folder; it is dangling on every other checkout and re-breaks the install on
any stash or checkout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 23:21:05 -07:00

138 lines
6.4 KiB
Bash
Executable File

#!/bin/bash
# The isolated v3 measurement stack: backend :8326, webpack :3026, Electron on its own profile.
#
# One script because rebuilding it by hand three times cost three different half-booted stacks, and a
# half-booted stack does not fail loudly, it just measures nothing and blames the product.
#
# ./stack.sh up dry backend refuses the irreversible click (coverage sweeps)
# ./stack.sh up live backend really clicks send (canary write tests)
# ./stack.sh down everything, SIGTERM then SIGKILL, ports verified free
# ./stack.sh status what is up right now
#
# Never touches :8324 / :3000. Those belong to whatever else is on this box.
# Where logs, profiles and run output go. Defaults to runs/ beside this harness; override with
# OSW_BENCH_DIR to keep multi-gigabyte browser profiles off the repo disk.
SP="${OSW_BENCH_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/runs}"
mkdir -p "$SP"
# The harness itself lives beside this script; SP is only for run OUTPUT.
HARNESS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TREE="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
TAG="${TAG:-run}"
status() {
# Two bugs lived in the first version of these four lines, and both printed a reassuring 0 over a
# fully live stack. macOS pgrep has no -c flag at all, so `pgrep -fc` exits on a usage error and
# the count renders empty; and the patterns were the words I type rather than the words ps prints
# (uvicorn runs as `python -m uvicorn`). A status check that reads clean while the box is busy is
# the worst instrument in this directory, because its whole job is to stop a second stack landing
# on the first. Count with grep on real ps output.
echo "=== procs ==="
local ps_out
ps_out="$(ps -Ao pid,command)"
# The backend pattern is anchored on POSITION, not just the words: this script's own supervisor is
# a `bash -c` whose command line quotes the whole uvicorn invocation, so a bare match on
# "-m uvicorn backend.main" counts the supervisor as a second backend and reports 2 on a box
# holding exactly 1. Overcounting here is the same class of lie as the pgrep -fc bug above, and it
# sends you hunting a stack that does not exist. The interpreter's line starts with the python
# binary right after the pid; the supervisor's starts with bash.
for entry in "\-m uvicorn backend.main|^ *[0-9]+ +[^ ]*python[^ ]* +-m uvicorn backend\.main" \
"bin/webpack|(bin/webpack|^ *[0-9]+ +webpack\b)" "MacOS/Electron|MacOS/Electron" \
"keep_renderer|keep_renderer" "bench.py|bench\.py" "browser_canary|browser_canary"; do
label="${entry%%|*}"; pat="${entry#*|}"
printf " %-34s %s\n" "$label" "$(echo "$ps_out" | grep -cE "$pat")"
done
echo "=== ports ==="
for pt in 8324 8326 3000 3026 20128; do
printf " %-6s %s\n" "$pt" "$(lsof -ti tcp:$pt 2>/dev/null | tr '\n' ' ')"
done
# Anything on :8324 is the OTHER checkout. Say so out loud: while it is up, the single 9router on
# :20128 is contended and every timing this stack produces is noise (a sweep once scored 1/9 vs
# 4/9 with zero code change, purely on that contention).
if [ -n "$(lsof -ti tcp:8324 2>/dev/null)" ]; then
echo " !! another OpenSwarm is on :8324. Do NOT measure, and do NOT kill it."
fi
}
down() {
# SCOPED TO THIS STACK ONLY. The first version matched on `uvicorn backend.main`, which is exactly
# what the OTHER OpenSwarm checkout on this box runs too: one careless `stack.sh down` would have
# killed a colleague's backend on :8324 mid-session. Nothing here may match a process this script
# did not start, so identify them by MY ports and MY profile directory, never by a generic name.
pkill -f "keep_renderer" 2>/dev/null
pkill -f "BACKEND RESTARTED" 2>/dev/null
pkill -f "user-data-dir=$SP/udd" 2>/dev/null
pkill -f "browser_canary" 2>/dev/null
pkill -f "$HARNESS/bench.py" 2>/dev/null
sleep 3
for pt in 8326 3026; do
pids=$(lsof -ti tcp:$pt 2>/dev/null)
[ -n "$pids" ] && kill -9 $pids 2>/dev/null
done
pkill -9 -f "user-data-dir=$SP/udd" 2>/dev/null
sleep 1
status
}
up() {
local mode="${1:-dry}"
local dry=0
[ "$mode" = "dry" ] && dry=1
# A stale stack under a fresh one is the single most expensive failure here: two backends fight
# over the one 9router and every number becomes a coin flip. Always start from nothing.
down >/dev/null 2>&1
# Supervised, because the backend has died mid-measurement on a clean SIGTERM with no error in
# its log (2026-08-05 07:44, 52 minutes in, no other stack on the box). An unsupervised death does
# not announce itself: the harness just starts recording connection errors as product failures.
# The restart marker goes into the same log the harness slices, so any trial that spans a restart
# can be excluded instead of counted.
cd "$TREE" || exit 1
nohup bash -c '
while true; do
OPENSWARM_PORT=8326 OSW_SENDSCRIPT_DRYRUN='"$dry"' OPENSWARM_DEV=1 \
./backend/.venv/bin/python -m uvicorn backend.main:app --port 8326 --host 127.0.0.1 \
>> "'"$SP/${TAG}_be.log"'" 2>&1
echo "[stack] BACKEND RESTARTED at $(date +%H:%M:%S)" >> "'"$SP/${TAG}_be.log"'"
sleep 4
done' > /dev/null 2>&1 &
disown
cd "$TREE/frontend" || exit 1
OPENSWARM_DEV_PORT=3026 OPENSWARM_PORT=8326 \
./node_modules/.bin/webpack serve --mode development \
> "$SP/${TAG}_wp.log" 2>&1 &
# A real authenticated 200, not just "something accepted a TCP connection". /api/health does not
# exist (it 404s), and curl calls a 404 a success, so the old check passed the instant the socket
# opened and handed the next step a backend that had not finished booting.
echo "waiting for backend :8326 ..."
for i in $(seq 1 90); do
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 \
-H "Authorization: Bearer $(cat "$TREE/backend/data/auth.token" 2>/dev/null)" \
"http://127.0.0.1:8326/api/dashboards/list")
[ "$code" = "200" ] && break
sleep 2
done
echo "waiting for webpack :3026 ..."
for i in $(seq 1 120); do
curl -s -o /dev/null --max-time 3 "http://localhost:3026" && break
sleep 2
done
OPENSWARM_DEV_PORT=3026 OPENSWARM_PORT=8326 nohup "$HARNESS/keep_renderer.sh" \
>> "$SP/renderer.log" 2>&1 &
disown
sleep 25
echo "mode=$mode (OSW_SENDSCRIPT_DRYRUN=$dry) tag=$TAG"
status
}
case "$1" in
up) up "$2" ;;
down) down ;;
status) status ;;
*) echo "usage: stack.sh {up dry|up live|down|status}"; exit 1 ;;
esac