fix: humanize pref namespace + async headless cloak

humanize: the wrapper wrote invisible_playwright.humanize[.maxTime], but the
binary's Juggler reads stealthfox.humanize (PageHandler.js gates the Bezier
mouse path on it). The old name was a dead no-op, so humanize never fired and
every mouse.move teleported the cursor — an automation tell. Renamed across
config.py, launcher.py and async_api.py; the mouse test now asserts the on/off
contrast instead of a false-green moves>=1.

headless (async): InvisiblePlaywright(headless=True) crashed on Windows/macOS.
_resolve_headless called make_virtual_display().start() unconditionally, but on
Win/macOS that returns None (the binary self-cloaks via DWMWA_CLOAK; only Linux
spawns Xvfb), so it died with AttributeError. It also never injected
cloak_prefs(), so the window wouldn't have hidden anyway. Mirror the sync
launcher: guard `if vd is not None` + inject cloak_prefs() when headless on
win32/darwin. Verified on FF150: headless=True loads, exits clean, window fully
hidden (no MainWindowHandle / no taskbar entry).
This commit is contained in:
feder-cr 2026-06-12 17:31:31 +02:00
parent 090baa6155
commit b34ecf2a21
5 changed files with 60 additions and 25 deletions

View file

@ -132,12 +132,9 @@ def test_mouse_move_outside_viewport_does_not_raise(firefox_binary):
# ────────────────────────────────────────────────────────────────────
@pytest.mark.e2e
def test_humanize_emits_intermediate_moves(firefox_binary):
"""A long mouse.move from one corner to another should fire several
mousemove events on the page when the humanize hook is enabled (which
is the StealthFox default)."""
with InvisiblePlaywright(seed=42, binary_path=firefox_binary) as browser:
def _humanize_move_count(firefox_binary, humanize):
"""Count page mousemove events fired by ONE long mouse.move."""
with InvisiblePlaywright(seed=42, binary_path=firefox_binary, humanize=humanize) as browser:
page = browser.new_page()
page.goto(_data_url(
"<div id=d style='width:600px;height:400px' "
@ -146,8 +143,27 @@ def test_humanize_emits_intermediate_moves(firefox_binary):
page.mouse.move(10, 10)
page.evaluate("window.__n = 0")
page.mouse.move(500, 300)
moves = page.evaluate("window.__n")
assert moves >= 1, f"expected at least 1 mousemove event, got {moves}"
return page.evaluate("window.__n")
@pytest.mark.e2e
def test_humanize_emits_intermediate_moves(firefox_binary):
"""A long mouse.move must expand into MANY intermediate mousemove events when
humanize is on (Bezier), and ~1 (a teleport) when off. We assert the on/off
CONTRAST: `moves >= 1` alone was a false-green a teleport already fires 1
and that false-green hid a pref-namespace bug (wrapper wrote
`invisible_playwright.humanize`, the binary's Juggler reads `stealthfox.humanize`)
that left humanize silently dead in production. This test now fails if the
pref ever stops reaching the binary."""
on = _humanize_move_count(firefox_binary, True)
off = _humanize_move_count(firefox_binary, False)
assert off <= 2, f"humanize OFF should ~teleport (<=2 moves), got {off}"
assert on >= 4, (
f"humanize ON must expand into many intermediate moves (Bezier); got {on} "
f"(off={off}). moves==1 means the cursor teleports — the exact automation "
f"tell humanize exists to remove, and a sign the stealthfox.* pref isn't "
f"reaching the binary's Juggler."
)
# ────────────────────────────────────────────────────────────────────

View file

@ -29,8 +29,8 @@ def test_get_default_stealth_prefs_random_seed_returns_dict():
assert isinstance(prefs, dict)
assert len(prefs) > 0
# humanize toggle is always set explicitly
assert "invisible_playwright.humanize" in prefs
assert prefs["invisible_playwright.humanize"] is True
assert "stealthfox.humanize" in prefs
assert prefs["stealthfox.humanize"] is True
def test_get_default_stealth_prefs_seed_is_deterministic():
@ -50,22 +50,22 @@ def test_get_default_stealth_prefs_different_seeds_differ():
def test_humanize_false_disables_prefs():
"""humanize=False removes the maxTime knob and flips the toggle to False."""
prefs = get_default_stealth_prefs(seed=42, humanize=False)
assert prefs["invisible_playwright.humanize"] is False
assert "invisible_playwright.humanize.maxTime" not in prefs
assert prefs["stealthfox.humanize"] is False
assert "stealthfox.humanize.maxTime" not in prefs
def test_humanize_default_sets_max_time_1_5():
"""humanize=True -> default maxTime is 1.5s, stored as string."""
prefs = get_default_stealth_prefs(seed=42, humanize=True)
assert prefs["invisible_playwright.humanize"] is True
assert prefs["invisible_playwright.humanize.maxTime"] == "1.5"
assert prefs["stealthfox.humanize"] is True
assert prefs["stealthfox.humanize.maxTime"] == "1.5"
def test_humanize_float_overrides_max_time():
"""Float for humanize is the explicit cap in seconds."""
prefs = get_default_stealth_prefs(seed=42, humanize=3.0)
assert prefs["invisible_playwright.humanize"] is True
assert prefs["invisible_playwright.humanize.maxTime"] == "3.0"
assert prefs["stealthfox.humanize"] is True
assert prefs["stealthfox.humanize.maxTime"] == "3.0"
def test_extra_prefs_overlay_takes_precedence():