Self-test pipeline

The self-test pipeline launches a profile against a known- instrumented page that reads back every fingerprint surface the platform exposes. The page checks the readings against the profile's declared fingerprint. Drift = bug.

When to run

  • After any change to the stealth injector (init-script, network flags, extension loadout). Per CLAUDE.md this is mandatory.
  • After bumping Chrome for Testing to a new milestone — same init-script can behave differently across Chrome 130 → 132.
  • When a profile starts producing odd readings on real targets (sudden bot-score spikes, unexpected captcha frequency).
  • Once per profile, ahead of first launch on a high-stakes target, as a sanity check.

What gets checked (R1-R12)

The validator runs twelve coherence rules. A pass means every rule returned green.

  • R1 navigator.userAgent matches the declared OS + Chrome milestone exactly.
  • R2 navigator.platform follows the UA (Win32 on Windows-Chrome, MacIntel on Mac).
  • R3 hardwareConcurrency / deviceMemory report canonical Windows-Chrome values (7 and 6 respectively). Randomising these is an instant tell.
  • R4 timezone matches the proxy's egress country.
  • R5 languages begin with the locale that matches the proxy country (e.g. nl-NL,nl,en).
  • R6 WebGL renderer reports a plausible GPU string for the OS — no SwiftShader, no headless tells.
  • R7 Canvas + AudioContext hashes are stable across reloads (no per-pixel jitter, no per-sample jitter).
  • R8 fonts respond to font-presence probes consistently with the OS.
  • R9 worker context exposes the same UA, platform, hardwareConcurrency as the main thread. Most stealth tools forget workers; we don't.
  • R10 iframe context follows R9 for nested iframes too.
  • R11 JS heap size limit matches the canonical Chrome constant (3.5 GB).
  • R12 navigator.webdriver is false and no automation library is detected.

Running it

Profile detail → Run self-test. The pipeline spawns Chrome for that profile (with proxy, init-script, and warmup state), navigates to the harness, collects 12 readings, and shows a per-rule pass/fail grid. Total runtime: ~30 seconds.

Reading the results

Each rule either passes (green) or fails with a reason. The most common failures:

  • R7 hash mismatch. Canvas / audio fingerprints drift if the init-script's noise-injection is stochastic instead of seeded. The seed must derive from the profile id, not from Math.random().
  • R9 worker drift. The init-script wasn't installed in the Worker scope. Confirm the stealth injector covers SharedWorker and dedicated workers.
  • R12 webdriver leak. Some Chrome flags re-expose webdriver=true. Compare against the pinned flag-list in src/main/launcher/flags.ts.

Re-running after a fix

The pipeline persists the full result row per run. The validation view shows the last 5 runs side-by-side so you can see the rule that flipped between attempts. Don't ship a stealth change until R1-R12 are all green for at least one profile.