ci: de-flake drive gate — drop the racy iframe probe, keep input/canvas checks
Re-running the enriched gate on the real binaries exposed a ~1-in-5 flake: the
iframe probe (nested data: / srcdoc) re-navigates, so Juggler's frame id changes
mid-check ("execution context destroyed" / "Frame was detached"). set_content
isn't an option either — this build rejects its document.write ("operation is
insecure").
Drop the iframe from the gate: a same-origin srcdoc iframe is a weak proxy for
the cross-origin issue #20 anyway. The page is now a plain subframe-free
goto(data:), and the mouse/keyboard/canvas-determinism/navigator-surface checks
(the firefox-2 class + stealth smoke) stay. 20/20 clean locally. The faithful
#20 sentinel (tests/test_cross_origin_iframe.py, two localhost origins) should
be wired as its own e2e gate job.
This commit is contained in:
+22
-21
@@ -10,17 +10,28 @@ It deliberately covers the failure modes that HISTORICALLY shipped green:
|
|||||||
- juggler missing entirely → TargetClosedError on launch (firefox-8)
|
- juggler missing entirely → TargetClosedError on launch (firefox-8)
|
||||||
- mouse/keyboard input broken → click/move/type assertions (firefox-2 #9:
|
- mouse/keyboard input broken → click/move/type assertions (firefox-2 #9:
|
||||||
jugglerSendMouseEvent / synthesizeMouseEvent)
|
jugglerSendMouseEvent / synthesizeMouseEvent)
|
||||||
- cross-origin iframe broken → content_frame() reachable (issue #20)
|
|
||||||
- canvas non-deterministic → identical draw → identical dataURL (stealth
|
- canvas non-deterministic → identical draw → identical dataURL (stealth
|
||||||
seed must be per-session, not per-readback)
|
seed must be per-session, not per-readback)
|
||||||
- headless navigator tells → navigator.webdriver falsy, languages
|
- headless navigator tells → navigator.webdriver falsy, languages
|
||||||
non-empty, plugins is a real PluginArray
|
non-empty, plugins is a real PluginArray
|
||||||
|
|
||||||
All of this is headless, NO screenshot → GPU-free (can't false-fail on the
|
All of this is headless, NO screenshot → GPU-free (can't false-fail on the
|
||||||
GPU-less hosted runners), and fully offline — data: URLs only, NO network, NO
|
GPU-less hosted runners), and fully offline → safe in public CI. WebGL
|
||||||
proxy, NO secrets → safe in public CI. WebGL determinism is intentionally NOT
|
determinism is intentionally NOT checked here (it needs SWGL and can false-fail
|
||||||
checked here (it needs SWGL and can false-fail headless); it lives in the local
|
headless); it lives in the local proxy realness gate.
|
||||||
proxy realness gate, alongside the fingerprint/WebRTC-vs-vanilla checks.
|
|
||||||
|
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
|
||||||
|
`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".
|
||||||
|
|
||||||
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.
|
||||||
@@ -31,18 +42,13 @@ import sys
|
|||||||
|
|
||||||
from playwright.sync_api import sync_playwright
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
# Single offline page that wires up every probe: a clickable button, a text
|
# Simple, subframe-free data: URL — proven stable across runners.
|
||||||
# input, a same-document iframe, and a mousemove counter. data: URLs execute
|
|
||||||
# inline scripts and are same-origin, so this needs no server.
|
|
||||||
PAGE = (
|
PAGE = (
|
||||||
"data:text/html,"
|
"data:text/html,"
|
||||||
"<title>dt</title>"
|
"<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>"
|
||||||
"<iframe id=f src=\"data:text/html,<b id=ok>ok</b>\"></iframe>"
|
|
||||||
"<script>window.__moves=0;"
|
|
||||||
"addEventListener('mousemove',function(){window.__moves++})</script>"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Identical 2D draw, evaluated twice in one session. The stealth canvas spoof is
|
# Identical 2D draw, evaluated twice in one session. The stealth canvas spoof is
|
||||||
@@ -59,13 +65,16 @@ def main(exe: str) -> int:
|
|||||||
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)
|
||||||
page = browser.new_page()
|
page = browser.new_page()
|
||||||
page.goto(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++; })")
|
||||||
|
|
||||||
ua = page.evaluate("navigator.userAgent")
|
ua = page.evaluate("navigator.userAgent")
|
||||||
webdriver = page.evaluate("navigator.webdriver")
|
webdriver = page.evaluate("navigator.webdriver")
|
||||||
text = page.evaluate("() => document.getElementById('x').textContent")
|
text = page.evaluate("() => document.getElementById('x').textContent")
|
||||||
|
|
||||||
# firefox-2 / issue-#9 catcher: real mouse + keyboard over juggler.
|
# firefox-2 / issue-#9 catcher: real mouse + keyboard over juggler.
|
||||||
|
page.wait_for_selector("#b")
|
||||||
page.mouse.move(20, 20)
|
page.mouse.move(20, 20)
|
||||||
page.mouse.move(120, 90) # exercises synthesizeMouseEvent path
|
page.mouse.move(120, 90) # exercises synthesizeMouseEvent path
|
||||||
page.click("#b") # mousedown/up/click → onclick fires
|
page.click("#b") # mousedown/up/click → onclick fires
|
||||||
@@ -75,13 +84,6 @@ def main(exe: str) -> int:
|
|||||||
moves = page.evaluate("window.__moves")
|
moves = page.evaluate("window.__moves")
|
||||||
typed = page.evaluate("() => document.getElementById('inp').value")
|
typed = page.evaluate("() => document.getElementById('inp').value")
|
||||||
|
|
||||||
# issue-#20 catcher: the iframe must be reachable and runnable.
|
|
||||||
frame_el = page.query_selector("#f")
|
|
||||||
frame = frame_el.content_frame() if frame_el else None
|
|
||||||
iframe_ok = bool(frame) and frame.evaluate(
|
|
||||||
"() => document.getElementById('ok') && document.getElementById('ok').textContent"
|
|
||||||
) == "ok"
|
|
||||||
|
|
||||||
# stealth-determinism catcher: identical draw → identical dataURL.
|
# stealth-determinism catcher: identical draw → identical dataURL.
|
||||||
canvas_a = page.evaluate(CANVAS_DRAW)
|
canvas_a = page.evaluate(CANVAS_DRAW)
|
||||||
canvas_b = page.evaluate(CANVAS_DRAW)
|
canvas_b = page.evaluate(CANVAS_DRAW)
|
||||||
@@ -98,14 +100,13 @@ def main(exe: str) -> int:
|
|||||||
assert clicked == 1, "page.click() did not fire onclick — mouse-event synthesis broken (firefox-2 class)"
|
assert clicked == 1, "page.click() did not fire onclick — mouse-event synthesis broken (firefox-2 class)"
|
||||||
assert moves >= 1, "page.mouse.move() produced no mousemove — jugglerSendMouseEvent regression"
|
assert moves >= 1, "page.mouse.move() produced no mousemove — jugglerSendMouseEvent regression"
|
||||||
assert typed == "ok", f"page.keyboard.type() failed: {typed!r}"
|
assert typed == "ok", f"page.keyboard.type() failed: {typed!r}"
|
||||||
assert iframe_ok, "iframe content_frame() unreachable — fission.webContentIsolationStrategy regression (issue #20)"
|
|
||||||
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)"
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"DRIVE GATE OK | UA={ua} | webdriver={webdriver} | "
|
f"DRIVE GATE OK | UA={ua} | webdriver={webdriver} | "
|
||||||
f"click+mousemove+keyboard+iframe+canvas-determinism+navsurface=ok"
|
f"click+mousemove+keyboard+canvas-determinism+navsurface=ok"
|
||||||
)
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user