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).
This commit is contained in:
+48
-19
@@ -23,15 +23,20 @@ headless); it lives in the local proxy realness gate.
|
|||||||
NOT covered here on purpose:
|
NOT covered here on purpose:
|
||||||
- Cross-origin iframe (issue #20): a same-origin srcdoc/data iframe is a weak
|
- 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
|
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
|
`tests/test_cross_origin_iframe.py` (e2e, two localhost origins); wire that
|
||||||
as its own gate job rather than a fragile in-gate check.
|
as its own gate job rather than a fragile in-gate check.
|
||||||
|
|
||||||
Robustness (learned the hard way): the page is a SIMPLE
|
Robustness (learned the hard way):
|
||||||
`goto("data:text/html,...")` with NO subframe. `set_content` throws "The
|
- The DOM is built on `about:blank` via `innerHTML`, NOT a `data:` URL. An
|
||||||
operation is insecure" on this build (its document.write is rejected), and a
|
unencoded `data:text/html,...` URL gets re-normalized (re-navigated to its
|
||||||
nested `data:`/srcdoc iframe races the evaluates → intermittent "execution
|
percent-encoded form) by Firefox; on the slower windows-latest runner that
|
||||||
context destroyed by navigation" / "Frame was detached".
|
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]
|
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.
|
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
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
# Simple, subframe-free data: URL — proven stable across runners.
|
# DOM built on about:blank (no data: URL to re-normalize → no spurious nav).
|
||||||
PAGE = (
|
BODY = (
|
||||||
"data:text/html,"
|
|
||||||
"<title>dt</title>"
|
|
||||||
"<h1 id=x>hello-drive</h1>"
|
"<h1 id=x>hello-drive</h1>"
|
||||||
"<button id=b onclick=\"window.__clicked=1\">go</button>"
|
"<button id=b onclick=\"window.__clicked=1\">go</button>"
|
||||||
"<input id=inp>"
|
"<input id=inp>"
|
||||||
@@ -60,14 +63,25 @@ CANVAS_DRAW = (
|
|||||||
"g.fillStyle='#f40';g.fillText('s',2,12);return c.toDataURL();}"
|
"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:
|
with sync_playwright() as p:
|
||||||
browser = p.firefox.launch(executable_path=exe, headless=True)
|
browser = p.firefox.launch(executable_path=exe, headless=True)
|
||||||
|
try:
|
||||||
page = browser.new_page()
|
page = browser.new_page()
|
||||||
page.goto(PAGE) # default wait_until="load"; no subframe → settles cleanly
|
page.goto("about:blank") # canonical, never re-navigates
|
||||||
# Attach the mousemove counter explicitly (don't depend on inline-script timing).
|
# Build the DOM + attach the mousemove counter in one shot.
|
||||||
page.evaluate("window.__moves = 0; window.addEventListener('mousemove', () => { window.__moves++; })")
|
page.evaluate(
|
||||||
|
"(html) => { document.body.innerHTML = html;"
|
||||||
|
" window.__moves = 0;"
|
||||||
|
" window.addEventListener('mousemove', () => { window.__moves++; }); }",
|
||||||
|
BODY,
|
||||||
|
)
|
||||||
|
|
||||||
ua = page.evaluate("navigator.userAgent")
|
ua = page.evaluate("navigator.userAgent")
|
||||||
webdriver = page.evaluate("navigator.webdriver")
|
webdriver = page.evaluate("navigator.webdriver")
|
||||||
@@ -91,7 +105,7 @@ def main(exe: str) -> int:
|
|||||||
# BotD navigator-surface tells (proxy-free subset).
|
# BotD navigator-surface tells (proxy-free subset).
|
||||||
langs = page.evaluate("navigator.languages.length")
|
langs = page.evaluate("navigator.languages.length")
|
||||||
plugins = page.evaluate("navigator.plugins instanceof PluginArray")
|
plugins = page.evaluate("navigator.plugins instanceof PluginArray")
|
||||||
|
finally:
|
||||||
browser.close()
|
browser.close()
|
||||||
|
|
||||||
assert "Firefox" in ua, f"unexpected UA (binary not driving correctly): {ua!r}"
|
assert "Firefox" in ua, f"unexpected UA (binary not driving correctly): {ua!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 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 langs and langs > 0, "navigator.languages empty (headless tell)"
|
||||||
assert plugins, "navigator.plugins is not a PluginArray (headless tell)"
|
assert plugins, "navigator.plugins is not a PluginArray (headless tell)"
|
||||||
|
return ua
|
||||||
|
|
||||||
print(
|
|
||||||
f"DRIVE GATE OK | UA={ua} | webdriver={webdriver} | "
|
def main(exe: str) -> int:
|
||||||
f"click+mousemove+keyboard+canvas-determinism+navsurface=ok"
|
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
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user