headless: cloak on Windows/macOS, Xvfb on Linux; CI cloak + webgl-masking guards

headless=True now hides the window via the binary's own cloak pref
(zoom.stealth.cloak_windows) on Windows and macOS instead of the broken
thread-level SetThreadDesktop; macOS is now supported. Linux keeps Xvfb.

Adds e2e guards that also run per-platform in the release drive-gate:
- test_cloak: the window is hidden (Windows DWMWA_CLOAKED / macOS CGWindowAlpha)
  yet still renders + drives; the macOS leg is where the cocoa cloak patch runs.
- a WebGL readPixels masking guard: the gamma noise must stay a smooth gamma
  remap, not the pixelscan-maskable +-1 spikes.
This commit is contained in:
feder-cr
2026-06-11 11:58:14 +02:00
parent e524695088
commit c2103ed0db
6 changed files with 271 additions and 147 deletions
+20
View File
@@ -341,6 +341,26 @@ jobs:
shell: bash shell: bash
run: python scripts/ci_drive_gate.py "$FF_EXE" ${{ matrix.extra }} run: python scripts/ci_drive_gate.py "$FF_EXE" ${{ matrix.extra }}
# CLOAK + WEBGL-MASKING GUARDS — run the wrapper's e2e cloak/gamma checks
# against THIS leg's freshly-built artifact, on its native runner. The
# wrapper's headless=True is headed+hidden (cloak on Win/macOS, its own
# Xvfb on Linux), so software-GL rendering works on the GPU-less hosts.
# test_cloak asserts the window is hidden (Windows DWMWA_CLOAKED / macOS
# CGWindowAlpha) AND still renders — the macOS leg is the only place the
# cocoa cloak patch gets RUN. The webgl guard catches a regression of the
# gamma readPixels noise back to the pixelscan-maskable ±1 spike form.
- name: Install pyobjc Quartz (macOS — to read the cloak window alpha)
if: matrix.kind == 'mac'
run: python -m pip install --quiet pyobjc-framework-Quartz
- name: Cloak + WebGL-masking guards (headed)
shell: bash
run: |
python -m pip install --quiet -e .
INVPW_BINARY_PATH="$FF_EXE" python -m pytest \
tests/test_cloak.py \
"tests/test_fingerprint_surface.py::test_webgl_readpixels_no_masking_signature" \
-m e2e -o addopts='' -q
publish: publish:
name: publish-draft-release name: publish-draft-release
needs: [build, gate] needs: [build, gate]
+35 -85
View File
@@ -2,18 +2,23 @@
Playwright's ``headless=True`` flips Firefox onto a different code path — Playwright's ``headless=True`` flips Firefox onto a different code path —
no widget tree, software-only rendering, distinct timing — and anti-bot no widget tree, software-only rendering, distinct timing — and anti-bot
systems can spot the divergence. Running the browser *headed* on a systems can spot the divergence. Running the browser *headed* but hidden
virtual display gives us the real rendering pipeline while keeping the gives us the real rendering pipeline while keeping the windows off screen.
windows off the user's screen.
Linux: spawns its own ``Xvfb`` instance, points ``DISPLAY`` at it. Two mechanisms, by platform:
Windows: creates a hidden desktop via ``CreateDesktop`` and binds the
calling thread to it, so Playwright's child processes inherit it. - **Windows & macOS**: the patched binary cloaks its OWN chrome windows
when ``zoom.stealth.cloak_windows`` is set — ``DWMWA_CLOAK`` (Windows)
/ ``NSWindow`` alpha-0 + pinned occlusion-ignore (macOS). The window
renders on the real GPU but never appears on screen, in the taskbar or
the Dock. The launcher injects the pref; nothing host-side is spawned.
- **Linux**: spawns its own ``Xvfb`` instance and points ``DISPLAY`` at
it (X11/Wayland have no per-window cloak that keeps the GPU rendering).
""" """
from __future__ import annotations from __future__ import annotations
import os import os
import secrets
import subprocess import subprocess
import sys import sys
import time import time
@@ -131,95 +136,40 @@ class _LinuxVirtualDisplay:
self._display = None self._display = None
class _WindowsVirtualDesktop: # Windows & macOS: the patched Firefox cloaks its own chrome windows when this
"""A hidden Windows desktop the calling thread is bound to. # pref is set (DWMWA_CLOAK / NSWindow alpha-0 + pinned occlusion-ignore), so the
# window renders on the real GPU but never shows on screen / in the taskbar or
# Dock. window_occlusion_tracking is disabled so a hidden window keeps painting.
CLOAK_PREFS = {
"zoom.stealth.cloak_windows": True,
"widget.windows.window_occlusion_tracking.enabled": False,
}
Playwright's child processes (node driver → firefox.exe) inherit the
desktop because their ``STARTUPINFO.lpDesktop`` is NULL — Windows uses
the calling thread's desktop in that case.
pywin32 ships ``CreateDesktop`` in ``win32service`` but doesn't expose def cloak_prefs() -> dict:
``SetThreadDesktop`` / ``GetThreadDesktop`` as module functions. We """Prefs that make the patched binary self-cloak its chrome windows.
call them directly via ctypes against ``user32.dll``.
Used on Windows & macOS, where hiding is done inside the binary rather than
with a host-side virtual display.
""" """
return dict(CLOAK_PREFS)
def __init__(self) -> None:
self._desktop = None # PyHDESK from win32service.CreateDesktop
self._original_handle = 0 # raw HDESK int of the previous desktop
def start(self) -> None:
try:
import win32con # type: ignore
import win32service # type: ignore
except ImportError as e:
raise RuntimeError(
"invisible_playwright headless=True on Windows requires pywin32. "
"Install it: pip install pywin32"
) from e
import ctypes
from ctypes import wintypes
user32 = ctypes.windll.user32
kernel32 = ctypes.windll.kernel32
# Save the current desktop handle so we can restore it on stop().
get_thread_desktop = user32.GetThreadDesktop
get_thread_desktop.argtypes = [wintypes.DWORD]
get_thread_desktop.restype = wintypes.HANDLE
self._original_handle = get_thread_desktop(kernel32.GetCurrentThreadId())
name = f"sf_{secrets.token_hex(4)}"
self._desktop = win32service.CreateDesktop(
name, 0, win32con.GENERIC_ALL, None
)
# Bind the calling thread to the new desktop. Children spawned
# afterwards (Playwright driver → firefox.exe) inherit it because
# their STARTUPINFO.lpDesktop is NULL.
set_thread_desktop = user32.SetThreadDesktop
set_thread_desktop.argtypes = [wintypes.HANDLE]
set_thread_desktop.restype = wintypes.BOOL
if not set_thread_desktop(int(self._desktop)):
err = ctypes.get_last_error()
raise RuntimeError(
f"SetThreadDesktop failed (GetLastError={err}). "
"The thread cannot have any windows or hooks; close them first."
)
def stop(self) -> None:
import ctypes
from ctypes import wintypes
user32 = ctypes.windll.user32
if self._original_handle:
try:
set_thread_desktop = user32.SetThreadDesktop
set_thread_desktop.argtypes = [wintypes.HANDLE]
set_thread_desktop.restype = wintypes.BOOL
set_thread_desktop(self._original_handle)
except Exception:
pass
self._original_handle = 0
if self._desktop is not None:
try:
self._desktop.CloseDesktop()
except Exception:
pass
self._desktop = None
def make_virtual_display(): def make_virtual_display():
"""Return a started/stoppable virtual-display object for this platform. """Return a start()/stop()-able virtual display, or ``None`` when the
platform hides windows via the in-binary cloak pref instead.
InvisiblePlaywright supports Windows x86_64 and Linux x86_64 only. - Linux: a fresh ``Xvfb`` (the launcher start()s/stop()s it).
- Windows / macOS: ``None`` — the binary self-cloaks via ``cloak_prefs()``,
injected by the launcher; nothing host-side needs spawning.
""" """
if sys.platform == "win32":
return _WindowsVirtualDesktop()
if sys.platform.startswith("linux"): if sys.platform.startswith("linux"):
return _LinuxVirtualDisplay() return _LinuxVirtualDisplay()
if sys.platform in ("win32", "darwin"):
return None
raise RuntimeError( raise RuntimeError(
f"invisible_playwright supports Windows and Linux only (got {sys.platform!r})" f"invisible_playwright supports Windows, macOS and Linux "
f"(got {sys.platform!r})"
) )
+15 -6
View File
@@ -9,7 +9,7 @@ from playwright.sync_api import Browser, BrowserContext, Playwright, sync_playwr
from ._fpforge import Profile, generate_profile from ._fpforge import Profile, generate_profile
from ._geo import prepare_session_geo from ._geo import prepare_session_geo
from ._headless import make_virtual_display from ._headless import cloak_prefs, make_virtual_display
from ._proxy import configure_proxy as _configure_proxy_shared from ._proxy import configure_proxy as _configure_proxy_shared
from .download import ensure_binary from .download import ensure_binary
from .prefs import translate_profile_to_prefs from .prefs import translate_profile_to_prefs
@@ -340,6 +340,12 @@ class InvisiblePlaywright:
extra_prefs=self._extra_prefs, extra_prefs=self._extra_prefs,
virtual_display=bool(self._headless and _sys.platform == "win32"), virtual_display=bool(self._headless and _sys.platform == "win32"),
) )
# Windows & macOS hide the headless window via the binary's own cloak
# (DWMWA_CLOAK / NSWindow alpha) — inject the pref so the patched build
# cloaks its chrome windows. setdefault: an explicit user override wins.
if self._headless and _sys.platform in ("win32", "darwin"):
for _k, _v in cloak_prefs().items():
prefs.setdefault(_k, _v)
prefs["invisible_playwright.humanize"] = bool(self._humanize) prefs["invisible_playwright.humanize"] = bool(self._humanize)
if self._humanize: if self._humanize:
prefs["invisible_playwright.humanize.maxTime"] = str(self._humanize_max_seconds()) prefs["invisible_playwright.humanize.maxTime"] = str(self._humanize_max_seconds())
@@ -379,15 +385,18 @@ class InvisiblePlaywright:
def _resolve_headless(self) -> bool: def _resolve_headless(self) -> bool:
"""Translate the user's ``headless`` flag. """Translate the user's ``headless`` flag.
When ``True``, we keep Firefox in headed mode (real rendering When ``True``, Firefox stays in headed mode (real rendering pipeline →
pipeline → coherent fingerprint) and hide the windows on a fresh coherent fingerprint) and the window is hidden: on Linux via a fresh
Xvfb (Linux) or hidden Windows desktop. Xvfb spawned here; on Windows/macOS via the binary's own window cloak
(the ``zoom.stealth.cloak_windows`` pref added in ``_build_prefs``), so
``make_virtual_display()`` returns ``None`` and nothing is spawned.
""" """
if not self._headless: if not self._headless:
return False return False
vd = make_virtual_display() vd = make_virtual_display()
vd.start() if vd is not None:
self._virtual_display = vd vd.start()
self._virtual_display = vd
return False return False
def _humanize_max_seconds(self) -> float: def _humanize_max_seconds(self) -> float:
+106
View File
@@ -0,0 +1,106 @@
"""Cloak guard (e2e) — verifies the source-level "invisible headless" cloak:
the chrome window is hidden from the screen YET keeps rendering on the real GPU
(not Playwright's native headless, which has no WebGL). Runs per-platform in CI:
- Windows: the DWMWA_CLOAK attribute (queried via DWMWA_CLOAKED).
- macOS: the NSWindow alpha (queried via Quartz CGWindowListCopyWindowInfo).
- Linux: skipped — there the wrapper hides via Xvfb, not a source-level cloak.
This is the CI validation for the macOS cocoa cloak patch, which can't be built
or run on the Windows/Linux dev boxes.
"""
from __future__ import annotations
import sys
import time
import pytest
from invisible_playwright import InvisiblePlaywright
CLOAK_PREFS = {
"zoom.stealth.cloak_windows": True,
"widget.windows.window_occlusion_tracking.enabled": False,
}
_WEBGL_RENDERER = """() => {
const g = document.createElement('canvas').getContext('webgl');
if (!g) return 'NO-WEBGL';
const d = g.getExtension('WEBGL_debug_renderer_info');
return d ? g.getParameter(d.UNMASKED_RENDERER_WEBGL) : (g.getParameter(g.RENDERER) || '');
}"""
def _windows_moz_window_cloaked() -> bool:
"""True if at least one MozillaWindowClass top-level window is DWMWA_CLOAKED."""
import ctypes
from ctypes import wintypes
user32 = ctypes.windll.user32
dwm = ctypes.windll.dwmapi
DWMWA_CLOAKED = 14
ENUM = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
found = []
def cb(hwnd, _):
c = ctypes.create_unicode_buffer(256)
user32.GetClassNameW(hwnd, c, 256)
if c.value == "MozillaWindowClass":
v = wintypes.DWORD(0)
dwm.DwmGetWindowAttribute(wintypes.HWND(hwnd), DWMWA_CLOAKED,
ctypes.byref(v), 4)
found.append(v.value)
return True
user32.EnumWindows(ENUM(cb), 0)
return any(state != 0 for state in found)
def _macos_firefox_window_alpha_zero() -> bool:
"""True if a Firefox on-screen window reports ~0 alpha (cloaked)."""
from Quartz import ( # type: ignore
CGWindowListCopyWindowInfo,
kCGWindowListOptionOnScreenOnly,
kCGNullWindowID,
)
infos = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID)
alphas = []
for w in infos or []:
owner = (w.get("kCGWindowOwnerName") or "")
if "firefox" in owner.lower() or "nightly" in owner.lower():
alphas.append(float(w.get("kCGWindowAlpha", 1.0)))
# cloaked windows are alpha 0; if Firefox has any window it must be ~0.
return bool(alphas) and all(a < 0.05 for a in alphas)
@pytest.mark.e2e
@pytest.mark.skipif(
sys.platform.startswith("linux"),
reason="source-level cloak is Windows/macOS only; Linux hides via Xvfb",
)
def test_cloak_hides_window_but_keeps_rendering(firefox_binary):
with InvisiblePlaywright(
seed=42, binary_path=firefox_binary, headless=False, extra_prefs=CLOAK_PREFS
) as browser:
page = browser.new_context().new_page()
page.goto("https://example.com", timeout=30_000)
time.sleep(2)
# 1) still renders on the real GPU pipeline (a non-blank screenshot proves
# the compositor is alive despite the window being hidden).
shot = page.screenshot()
assert len(shot) > 3000, "cloaked window produced a blank screenshot (rendering paused)"
# 2) real WebGL present (native headless has none) -> headed pipeline intact.
renderer = page.evaluate(_WEBGL_RENDERER)
assert renderer and renderer != "NO-WEBGL", f"no real WebGL under cloak: {renderer!r}"
# 3) the window is actually hidden (per-platform).
if sys.platform == "win32":
assert _windows_moz_window_cloaked(), "Firefox window is not DWMWA_CLOAKED"
elif sys.platform == "darwin":
try:
hidden = _macos_firefox_window_alpha_zero()
except ImportError:
pytest.skip("pyobjc Quartz not available to verify macOS cloak alpha")
assert hidden, "Firefox macOS window is not alpha-cloaked"
+66
View File
@@ -236,3 +236,69 @@ def test_fpscanner_no_userAgentData_on_firefox(page):
"""navigator.userAgentData is Chromium-only. Presence on Firefox UA = bot.""" """navigator.userAgentData is Chromium-only. Presence on Firefox UA = bot."""
if "Firefox" in _ev(page, "navigator.userAgent"): if "Firefox" in _ev(page, "navigator.userAgent"):
assert not _ev(page, "'userAgentData' in navigator") assert not _ev(page, "'userAgentData' in navigator")
# ===========================================================================
# WebGL masking-detector guard (pixelscan getFixedRedBox / webglHash)
#
# pixelscan flags "fingerprint masking" on the WebGL readPixels output. We
# reproduce ITS probe locally (the fingerprintjs gradient triangle) and check
# the structural signature it keys on: our stealth readPixels noise MUST be a
# coherent, monotonic gamma remap (smooth, ~0 spikes), NOT isolated +-1 flips
# (which read as unnatural high-frequency noise and were flagged as masking).
# This is the CI-safe local stand-in for pixelscan's server-side check; it
# guards the gamma fix from ever silently regressing to the +-1 algorithm.
# ===========================================================================
_WEBGL_MASKING_PROBE = """() => {
const c = document.createElement('canvas');
const gl = c.getContext('webgl') || c.getContext('experimental-webgl');
if (!gl) return { error: 'no-webgl' };
const vs = 'attribute vec2 a;uniform vec2 o;varying vec2 v;' +
'void main(){v=a+o;gl_Position=vec4(a,0,1);}';
const fs = 'precision mediump float;varying vec2 v;' +
'void main(){gl_FragColor=vec4(v,0,1);}';
const buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER,
new Float32Array([-0.2,-0.9,0, 0.4,-0.26,0, 0,0.732134444,0]), gl.STATIC_DRAW);
const p = gl.createProgram();
const s1 = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(s1, vs); gl.compileShader(s1);
const s2 = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(s2, fs); gl.compileShader(s2);
gl.attachShader(p, s1); gl.attachShader(p, s2); gl.linkProgram(p); gl.useProgram(p);
const loc = gl.getAttribLocation(p, 'a'); gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 3, gl.FLOAT, false, 0, 0);
const off = gl.getUniformLocation(p, 'o'); gl.uniform2f(off, 1, 1);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 3);
const w = gl.drawingBufferWidth, h = gl.drawingBufferHeight;
const px = new Uint8Array(w * h * 4);
gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, px);
// count small local extrema (|delta|<=3 to both horizontal neighbours, same
// sign) — the +-1-noise signature; a smooth/monotonic render has ~none.
let spikes = 0;
for (let y = 0; y < h; y++) {
for (let x = 1; x < w - 1; x++) {
for (let ch = 0; ch < 3; ch++) {
const i = (y * w + x) * 4 + ch; const val = px[i];
if (val === 0) continue;
const dl = val - px[i - 4], dr = val - px[i + 4];
if (dl * dr > 0 && Math.abs(dl) <= 3 && Math.abs(dr) <= 3) spikes++;
}
}
}
return { spikes: spikes, dims: w + 'x' + h };
}"""
@pytest.mark.e2e
def test_webgl_readpixels_no_masking_signature(page):
"""Stealth WebGL readPixels noise must be a coherent gamma remap (smooth),
not isolated +-1 flips. +-1 noise on the smooth gradient triangle produced
~300+ 'spikes' and pixelscan flagged it as masking; the gamma remap leaves
the gradient smooth (~0 spikes). Regression guard for the gamma fix."""
res = _ev(page, _WEBGL_MASKING_PROBE)
assert "error" not in res, f"WebGL probe failed: {res}"
# genuine / gamma -> ~0; the rejected +-1 algorithm produced ~320.
assert res["spikes"] < 30, (
f"WebGL readPixels shows {res['spikes']} high-frequency noise spikes "
f"(pixelscan-maskable); the stealth noise must be a smooth gamma remap."
)
+29 -56
View File
@@ -1,35 +1,39 @@
"""Unit tests for the ``_headless`` virtual-display dispatcher. """Unit tests for the ``_headless`` window-hider dispatcher.
The dispatcher (``make_virtual_display``) is the only piece of ``make_virtual_display`` is pure platform routing:
``_headless`` we can exercise as a unit test on a single platform: - Linux: a ``_LinuxVirtualDisplay`` (Xvfb) object the launcher start()s/stop()s.
``_WindowsVirtualDesktop`` actually creates a Win32 desktop on - Windows / macOS: ``None`` — the patched binary self-cloaks its chrome windows
construction's later ``start()`` call, and ``_LinuxVirtualDisplay`` calls via ``cloak_prefs()`` (injected by the launcher), so nothing host-side spawns.
``Xvfb`` — both belong in integration/E2E coverage. The dispatcher's - Anything else: a clear ``RuntimeError`` naming the platform.
job is pure platform routing, which we patch via ``monkeypatch``.
Per scope: Windows-specific + platform-agnostic only. We still cover ``_LinuxVirtualDisplay`` construction does no I/O (Xvfb is only spawned in
the Linux dispatch branch because instantiating ``_LinuxVirtualDisplay`` ``start()``), so it's safe to exercise on any host.
does no I/O — Xvfb is only spawned in ``start()``, which we never call.
""" """
from __future__ import annotations from __future__ import annotations
import sys
import pytest import pytest
import invisible_playwright._headless as headless import invisible_playwright._headless as headless
from invisible_playwright._headless import ( from invisible_playwright._headless import (
CLOAK_PREFS,
_LinuxVirtualDisplay, _LinuxVirtualDisplay,
_WindowsVirtualDesktop, cloak_prefs,
make_virtual_display, make_virtual_display,
) )
@pytest.mark.unit @pytest.mark.unit
def test_make_virtual_display_returns_windows_desktop_on_win32(monkeypatch): def test_make_virtual_display_returns_none_on_win32(monkeypatch):
"""Windows hides via the in-binary cloak pref, not a host-side display."""
monkeypatch.setattr(headless.sys, "platform", "win32") monkeypatch.setattr(headless.sys, "platform", "win32")
vd = make_virtual_display() assert make_virtual_display() is None
assert isinstance(vd, _WindowsVirtualDesktop)
@pytest.mark.unit
def test_make_virtual_display_returns_none_on_darwin(monkeypatch):
"""macOS is now supported — it hides via the same in-binary cloak pref."""
monkeypatch.setattr(headless.sys, "platform", "darwin")
assert make_virtual_display() is None
@pytest.mark.unit @pytest.mark.unit
@@ -37,8 +41,7 @@ def test_make_virtual_display_returns_linux_xvfb_on_linux(monkeypatch):
"""``__init__`` of ``_LinuxVirtualDisplay`` does no I/O — only ``start()`` """``__init__`` of ``_LinuxVirtualDisplay`` does no I/O — only ``start()``
spawns Xvfb. Exercising the dispatcher here is safe on any host.""" spawns Xvfb. Exercising the dispatcher here is safe on any host."""
monkeypatch.setattr(headless.sys, "platform", "linux") monkeypatch.setattr(headless.sys, "platform", "linux")
vd = make_virtual_display() assert isinstance(make_virtual_display(), _LinuxVirtualDisplay)
assert isinstance(vd, _LinuxVirtualDisplay)
@pytest.mark.unit @pytest.mark.unit
@@ -49,21 +52,10 @@ def test_make_virtual_display_accepts_linux_variants(monkeypatch):
assert isinstance(make_virtual_display(), _LinuxVirtualDisplay) assert isinstance(make_virtual_display(), _LinuxVirtualDisplay)
@pytest.mark.unit
def test_make_virtual_display_raises_on_darwin(monkeypatch):
"""macOS is unsupported — the dispatcher must raise with a clear
message rather than returning a no-op shim. ``InvisiblePlaywright``
relies on this to bail before launching Firefox on a system where
the patched binary doesn't exist."""
monkeypatch.setattr(headless.sys, "platform", "darwin")
with pytest.raises(RuntimeError, match="Windows and Linux only"):
make_virtual_display()
@pytest.mark.unit @pytest.mark.unit
def test_make_virtual_display_raises_on_unsupported_platform(monkeypatch): def test_make_virtual_display_raises_on_unsupported_platform(monkeypatch):
monkeypatch.setattr(headless.sys, "platform", "freebsd14") monkeypatch.setattr(headless.sys, "platform", "freebsd14")
with pytest.raises(RuntimeError, match="Windows and Linux only"): with pytest.raises(RuntimeError, match="Windows, macOS and Linux"):
make_virtual_display() make_virtual_display()
@@ -77,32 +69,13 @@ def test_make_virtual_display_error_mentions_offending_platform(monkeypatch):
@pytest.mark.unit @pytest.mark.unit
def test_windows_desktop_initial_state_is_clean(): def test_cloak_prefs_enables_cloak_and_disables_occlusion():
"""Construction must not allocate Win32 resources — only ``start()`` """The cloak prefs must turn on the in-binary cloak and turn OFF Windows
does. Allows users to instantiate ``InvisiblePlaywright`` without occlusion tracking (so a hidden window keeps painting). Returns a copy."""
pywin32 installed; the import error fires lazily when ``start()`` runs.""" p = cloak_prefs()
vd = _WindowsVirtualDesktop() assert p["zoom.stealth.cloak_windows"] is True
assert vd._desktop is None assert p["widget.windows.window_occlusion_tracking.enabled"] is False
assert vd._original_handle == 0 assert p == CLOAK_PREFS and p is not CLOAK_PREFS
@pytest.mark.unit
@pytest.mark.skipif(sys.platform != "win32", reason="exercises Win32 ctypes")
def test_windows_desktop_stop_is_idempotent_without_start():
"""``stop()`` after never calling ``start()`` must be a no-op, so
``__exit__`` from a failed launch can call it unconditionally.
Skipped off Windows because ``stop()`` unconditionally resolves
``ctypes.windll.user32`` at the top of the function — that symbol
only exists on Windows. The early-return logic is safe because
callers only instantiate this class via ``make_virtual_display()``
which already routes on ``sys.platform == 'win32'``.
"""
vd = _WindowsVirtualDesktop()
vd.stop()
vd.stop()
assert vd._desktop is None
assert vd._original_handle == 0
# ────────────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────────────