From 610f09d2c2300f3b99aaab76a6c9cf6272e53d36 Mon Sep 17 00:00:00 2001
From: feder-cr <85809106+feder-cr@users.noreply.github.com>
Date: Tue, 9 Jun 2026 14:42:26 +0200
Subject: [PATCH] ci: build drive-gate DOM on about:blank, not a data: URL
(fixes win-CI flake)
linux+macOS drive went green but windows-latest kept throwing "execution
context destroyed by navigation" at a wandering evaluate (passed 20/20 win-local,
no browser crash logged). Root cause: the unencoded data: URL gets re-normalized
(re-navigated to its percent-encoded form) by Firefox; the slower win runner
races that re-nav against the evaluates. about:blank is canonical and never
re-navigates, so the DOM is now built there via innerHTML. Also add one logged
retry on transient context-destroyed/detached (a broken binary fails both).
---
scripts/ci_drive_gate.py | 111 ++++++++++++++++++++++++---------------
1 file changed, 70 insertions(+), 41 deletions(-)
diff --git a/scripts/ci_drive_gate.py b/scripts/ci_drive_gate.py
index 9885b0f..f3ff37c 100644
--- a/scripts/ci_drive_gate.py
+++ b/scripts/ci_drive_gate.py
@@ -23,15 +23,20 @@ headless); it lives in the local proxy realness gate.
NOT covered here on purpose:
- Cross-origin iframe (issue #20): a same-origin srcdoc/data iframe is a weak
proxy for it AND races Juggler's frame tracking (the frame re-navigates, its
- id changes → "Frame was detached" ~1-in-8). The faithful #20 sentinel is
+ id changes → "Frame was detached"). The faithful #20 sentinel is
`tests/test_cross_origin_iframe.py` (e2e, two localhost origins); wire that
as its own gate job rather than a fragile in-gate check.
-Robustness (learned the hard way): the page is a SIMPLE
-`goto("data:text/html,...")` with NO subframe. `set_content` throws "The
-operation is insecure" on this build (its document.write is rejected), and a
-nested `data:`/srcdoc iframe races the evaluates → intermittent "execution
-context destroyed by navigation" / "Frame was detached".
+Robustness (learned the hard way):
+ - The DOM is built on `about:blank` via `innerHTML`, NOT a `data:` URL. An
+ unencoded `data:text/html,...` URL gets re-normalized (re-navigated to its
+ percent-encoded form) by Firefox; on the slower windows-latest runner that
+ async re-nav races the evaluates → "execution context destroyed by
+ navigation". `about:blank` is canonical and never re-navigates.
+ - `set_content` is NOT usable — its document.write is rejected on this build
+ ("operation is insecure").
+ - A transient "context destroyed / detached / target closed" still gets ONE
+ logged retry; a genuinely broken binary fails BOTH attempts → gate fails.
Usage: python ci_drive_gate.py /path/to/firefox[.exe | .app/Contents/MacOS/firefox]
Exit 0 + "DRIVE GATE OK ..." on success; non-zero with a reason on failure.
@@ -42,10 +47,8 @@ import sys
from playwright.sync_api import sync_playwright
-# Simple, subframe-free data: URL — proven stable across runners.
-PAGE = (
- "data:text/html,"
- "
dt"
+# DOM built on about:blank (no data: URL to re-normalize → no spurious nav).
+BODY = (
"
hello-drive
"
""
""
@@ -60,39 +63,50 @@ CANVAS_DRAW = (
"g.fillStyle='#f40';g.fillText('s',2,12);return c.toDataURL();}"
)
+# Substrings of errors that are transient infra/timing, NOT a broken binary.
+_TRANSIENT = ("context was destroyed", "frame was detached", "target closed",
+ "because of a navigation")
-def main(exe: str) -> int:
+
+def _drive(exe: str) -> str:
+ """One full drive attempt. Returns the UA on success; raises on failure."""
with sync_playwright() as p:
browser = p.firefox.launch(executable_path=exe, headless=True)
- page = browser.new_page()
- page.goto(PAGE) # default wait_until="load"; no subframe → settles cleanly
- # Attach the mousemove counter explicitly (don't depend on inline-script timing).
- page.evaluate("window.__moves = 0; window.addEventListener('mousemove', () => { window.__moves++; })")
+ try:
+ page = browser.new_page()
+ page.goto("about:blank") # canonical, never re-navigates
+ # Build the DOM + attach the mousemove counter in one shot.
+ page.evaluate(
+ "(html) => { document.body.innerHTML = html;"
+ " window.__moves = 0;"
+ " window.addEventListener('mousemove', () => { window.__moves++; }); }",
+ BODY,
+ )
- ua = page.evaluate("navigator.userAgent")
- webdriver = page.evaluate("navigator.webdriver")
- text = page.evaluate("() => document.getElementById('x').textContent")
+ ua = page.evaluate("navigator.userAgent")
+ webdriver = page.evaluate("navigator.webdriver")
+ text = page.evaluate("() => document.getElementById('x').textContent")
- # firefox-2 / issue-#9 catcher: real mouse + keyboard over juggler.
- page.wait_for_selector("#b")
- page.mouse.move(20, 20)
- page.mouse.move(120, 90) # exercises synthesizeMouseEvent path
- page.click("#b") # mousedown/up/click → onclick fires
- page.click("#inp")
- page.keyboard.type("ok")
- clicked = page.evaluate("window.__clicked")
- moves = page.evaluate("window.__moves")
- typed = page.evaluate("() => document.getElementById('inp').value")
+ # firefox-2 / issue-#9 catcher: real mouse + keyboard over juggler.
+ page.wait_for_selector("#b")
+ page.mouse.move(20, 20)
+ page.mouse.move(120, 90) # exercises synthesizeMouseEvent path
+ page.click("#b") # mousedown/up/click → onclick fires
+ page.click("#inp")
+ page.keyboard.type("ok")
+ clicked = page.evaluate("window.__clicked")
+ moves = page.evaluate("window.__moves")
+ typed = page.evaluate("() => document.getElementById('inp').value")
- # stealth-determinism catcher: identical draw → identical dataURL.
- canvas_a = page.evaluate(CANVAS_DRAW)
- canvas_b = page.evaluate(CANVAS_DRAW)
+ # stealth-determinism catcher: identical draw → identical dataURL.
+ canvas_a = page.evaluate(CANVAS_DRAW)
+ canvas_b = page.evaluate(CANVAS_DRAW)
- # BotD navigator-surface tells (proxy-free subset).
- langs = page.evaluate("navigator.languages.length")
- plugins = page.evaluate("navigator.plugins instanceof PluginArray")
-
- browser.close()
+ # BotD navigator-surface tells (proxy-free subset).
+ langs = page.evaluate("navigator.languages.length")
+ plugins = page.evaluate("navigator.plugins instanceof PluginArray")
+ finally:
+ browser.close()
assert "Firefox" in ua, f"unexpected UA (binary not driving correctly): {ua!r}"
assert text == "hello-drive", f"DOM/JS roundtrip failed: {text!r}"
@@ -103,12 +117,27 @@ def main(exe: str) -> int:
assert canvas_a == canvas_b, "canvas non-deterministic across identical draws (stealth seed broken → bot tell)"
assert langs and langs > 0, "navigator.languages empty (headless tell)"
assert plugins, "navigator.plugins is not a PluginArray (headless tell)"
+ return ua
- print(
- f"DRIVE GATE OK | UA={ua} | webdriver={webdriver} | "
- f"click+mousemove+keyboard+canvas-determinism+navsurface=ok"
- )
- return 0
+
+def main(exe: str) -> int:
+ last = None
+ for attempt in (1, 2):
+ try:
+ ua = _drive(exe)
+ if attempt > 1:
+ print(f"(note: drive succeeded on retry {attempt} after a transient error)")
+ print(f"DRIVE GATE OK | UA={ua} | click+mousemove+keyboard+canvas-determinism+navsurface=ok")
+ return 0
+ except Exception as e: # noqa: BLE001 — gate: any failure must surface
+ last = e
+ msg = str(e).lower()
+ if attempt == 1 and any(t in msg for t in _TRANSIENT):
+ print(f"(transient error on attempt 1, retrying once): {e}", file=sys.stderr)
+ continue
+ break
+ print(f"DRIVE GATE FAILED: {last}", file=sys.stderr)
+ return 1
if __name__ == "__main__":