diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 03b855f..26ce75f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -344,11 +344,16 @@ jobs: # 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. + # Xvfb on Linux). Linux (Xvfb + llvmpipe) and Windows (WARP) give a + # software WebGL context on the GPU-less hosts, so the WebGL-dependent + # assertions run there. macOS GitHub runners expose NO WebGL in the CI + # session at all (even vanilla Firefox; macOS has no software-GL fallback), + # so on the mac legs the WebGL checks self-skip and the cloak is validated + # via its non-blank screenshot + CGWindowAlpha == 0. 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 (covered on Linux + Windows). - name: Install pyobjc Quartz (macOS — to read the cloak window alpha) if: matrix.kind == 'mac' run: python -m pip install --quiet pyobjc-framework-Quartz diff --git a/.github/workflows/verify-cloak.yml b/.github/workflows/verify-cloak.yml new file mode 100644 index 0000000..4d119e8 --- /dev/null +++ b/.github/workflows/verify-cloak.yml @@ -0,0 +1,103 @@ +# ───────────────────────────────────────────────────────────────────────────── +# verify-cloak.yml — re-runnable CLOAK + WEBGL-MASKING GUARDS for an EXISTING +# build run's artifacts, WITHOUT rebuilding Firefox (~3h on the mac legs). +# +# release.yml runs these same guards in its `gate` job against each freshly-built +# artifact. This re-runs them against the artifacts of a PRIOR build run (input +# `run_id`) using the CURRENT wrapper code on the default branch — so a test-only +# fix (e.g. making the macOS leg tolerant of the runner's missing WebGL) can be +# validated against the real binaries in ~10 min instead of paying a full rebuild. +# +# Same guard command as release.yml's gate. Headed-but-cloaked; zero proxy / zero +# secrets. The macOS legs are the only place the cocoa cloak patch actually RUNS. +# ───────────────────────────────────────────────────────────────────────────── +name: verify-cloak + +on: + workflow_dispatch: + inputs: + run_id: + description: 'build run id whose asset-* artifacts to re-gate (e.g. 27346856197)' + required: true + +permissions: + contents: read + actions: read # download-artifact needs this to read another run's artifacts + +jobs: + guard: + name: guard-${{ matrix.leg }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 25 + strategy: + fail-fast: false + matrix: + # Same legs/runners/assets as release.yml's gate matrix. + include: + - leg: linux-x86_64 + runner: ubuntu-24.04 + kind: linux + asset: firefox-150.0.1-stealth-linux-x86_64.tar.gz + - leg: linux-arm64 + runner: ubuntu-24.04-arm + kind: linux + asset: firefox-150.0.1-stealth-linux-arm64.tar.gz + - leg: win-x86_64 + runner: windows-latest + kind: win + asset: firefox-150.0.1-stealth-win-x86_64.zip + - leg: macos-arm64 + runner: macos-15 + kind: mac + asset: firefox-150.0.1-stealth-macos-arm64.tar.gz + - leg: macos-x86_64 + runner: macos-15-intel + kind: mac + asset: firefox-150.0.1-stealth-macos-x86_64.tar.gz + steps: + - name: Checkout wrapper (current default branch — the FIXED tests) + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: { fetch-depth: 1 } + - name: Download build asset from the prior run (no rebuild) + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: asset-${{ matrix.leg }} + path: art + run-id: ${{ github.event.inputs.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: { python-version: '3.11' } + - name: Install Playwright driver (no bundled browser — we override executable_path) + # Single-source pin (see release.yml); the wrapper enforces juggler compat. + shell: bash + run: python -m pip install --quiet "playwright==$(cat scripts/playwright_pin.txt)" + - name: Linux system deps for headless firefox + if: matrix.kind == 'linux' + run: sudo "$(which python)" -m playwright install-deps firefox + - name: Extract + locate firefox binary + shell: bash + run: | + set -e + mkdir -p ff + A="art/${{ matrix.asset }}" + case "${{ matrix.kind }}" in + win) python -c "import zipfile; zipfile.ZipFile('$A').extractall('ff')"; EXE="ff/firefox.exe";; + linux) tar xzf "$A" -C ff; EXE="ff/firefox";; + mac) tar xzf "$A" -C ff; EXE="ff/Firefox.app/Contents/MacOS/firefox";; + esac + [ -e "$EXE" ] || { echo "ERROR: firefox binary not found at $EXE"; exit 1; } + chmod +x "$EXE" 2>/dev/null || true + echo "FF_EXE=$EXE" >> "$GITHUB_ENV" + echo "located: $EXE" + - 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 ".[dev]" + 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 diff --git a/tests/test_cloak.py b/tests/test_cloak.py index bb8b5e4..71ad50a 100644 --- a/tests/test_cloak.py +++ b/tests/test_cloak.py @@ -91,9 +91,18 @@ def test_cloak_hides_window_but_keeps_rendering(firefox_binary): 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. + # 2) headed pipeline intact: a real WebGL context (Playwright's native + # headless has none). Linux (Xvfb + llvmpipe) and Windows (WARP) give a + # software context on the GPU-less runners, so a missing context there + # is a real regression -> hard fail. macOS GitHub runners expose NO + # WebGL in the CI session at all (even vanilla Firefox), and macOS has + # no software-GL fallback; the cloak's "still rendering" property is + # already proven by the non-blank screenshot above, so we don't also + # require a live WebGL context there. renderer = page.evaluate(_WEBGL_RENDERER) - assert renderer and renderer != "NO-WEBGL", f"no real WebGL under cloak: {renderer!r}" + webgl_ok = bool(renderer) and renderer != "NO-WEBGL" + if not (sys.platform == "darwin" and not webgl_ok): + assert webgl_ok, f"no real WebGL under cloak: {renderer!r}" # 3) the window is actually hidden (per-platform). if sys.platform == "win32": diff --git a/tests/test_fingerprint_surface.py b/tests/test_fingerprint_surface.py index 56789f9..8a9ff68 100644 --- a/tests/test_fingerprint_surface.py +++ b/tests/test_fingerprint_surface.py @@ -27,6 +27,7 @@ Run only this file: from __future__ import annotations import re +import sys import pytest @@ -296,6 +297,12 @@ def test_webgl_readpixels_no_masking_signature(page): ~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) + if res.get("error") == "no-webgl" and sys.platform == "darwin": + pytest.skip( + "macOS CI runners expose no WebGL (no software-GL fallback); the gamma " + "readPixels remap is platform-agnostic C++ and is exercised by the Linux " + "(Xvfb/llvmpipe) and Windows (WARP) gates." + ) assert "error" not in res, f"WebGL probe failed: {res}" # genuine / gamma -> ~0; the rejected +-1 algorithm produced ~320. assert res["spikes"] < 30, (