* studio/tests: AbortSignal-bound in-page fetches + wall-clock watchdog
Run 25696797934 / job 75446949358 on PR #5387 cancelled the
"Chat UI Tests" macos-14 job at 30 min: studio.log went idle after
the chat surface mounted, no further requests reached the server,
and Playwright silently sat on a `page.evaluate(async () => fetch(
/api/inference/load))` for 27+ minutes before the runner-level
timeout fired. The two other Chat UI Tests jobs on the same SHA
passed in 5-17 min, so this was a transient renderer wedge under
--single-process Chromium, not a regression from the security
bumps in that PR.
Root cause: Playwright's `page.evaluate(...)` has no `timeout=`
argument. If the JS body awaits a fetch whose promise never
settles (the renderer's network thread stalls behind the busy main
thread on the free macos-14 runner), the entire Python script
hangs until something external kills it.
Add two helpers in `_playwright_robust.py`:
- `evaluate_fetch(page, url, *, method, headers, body, timeout_ms)`
wraps `fetch()` in an `AbortController` so the JS resolves
either with a real response or with `{status: 0, error:
"AbortError..."}` after the budget elapses. Callers fail loud
on a non-None `error` and the wedge surfaces as a one-line
diagnostic instead of a 30-min cancel.
- `install_wall_clock_watchdog(deadline_s)` starts a daemon
Timer that hard-exits the process at the deadline. Belt-and-
suspenders for any wedge inside the browser that the per-
action timeouts cannot bound. Default 720s (12 min); healthy
runs measure 5-9 min on macos-14 so the headroom is small
without amplifying a wedge to the 30-min runner cap.
Wire both into `playwright_chat_ui.py` and `playwright_extra_ui.py`:
- Replace every `page.evaluate(async () => fetch(...))` site
with `evaluate_fetch(...)`: refresh-token exchange, defaults
fetch, inference load, health probe, post-rotation refresh.
Five sites in chat_ui, two in extra_ui.
- Arm the watchdog at the top of `with sync_playwright()` and
cancel it on clean exit.
Knobs (all default-safe, override only for slow runners):
STUDIO_UI_WALL_TIMEOUT_S (default 720s)
STUDIO_UI_FETCH_TIMEOUT_MS (default 30000ms)
STUDIO_UI_LOAD_TIMEOUT_MS (default 180000ms)
Verified locally with `python -c "ast.parse(...)"` on all three
files and a unit smoke that confirms `evaluate_fetch`'s JS argument
shape and that `install_wall_clock_watchdog` returns a daemonised
Timer that responds to `.cancel()`.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>