Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
# swap-testing-e2e-cloakbrowser
## Why
`@playwright/test` ships a stock Chromium that is trivially fingerprinted as
automation (`navigator.webdriver === true`, `HeadlessChrome` UA, missing
`window.chrome`, GPU/canvas/audio leaks, CDP detection). E2E suites that
exercise production-like flows against bot-aware infrastructure
(Cloudflare Turnstile, reCAPTCHA v3, ShieldSquare, FingerprintJS,
BrowserScan) either fail outright or skew metrics on the first request.
[CloakBrowser](https://github.com/CloakHQ/cloakbrowser) (`cloakbrowser`,
v0.3.28) is a real Chromium binary with 49 source-level C++ patches
covering canvas, WebGL, audio, fonts, GPU, screen, WebRTC, network
timing, automation signals, and CDP input behaviour. It is a drop-in
replacement at the browser-launch layer: the returned object is a
`playwright-core` `Browser` and works with every existing Playwright
test, fixture, page-object, and helper without API changes.
This proposal swaps every browser-launch call inside
`@suaveplan/testing-e2e` to `cloakbrowser`, retains `@playwright/test`
as the test runner (CloakBrowser does not provide one), and exposes the
stealth-specific knobs (`humanize`, `proxy`, `geoip`,
`launchPersistentContext`, extension support) as first-class API surface.
## What Changes
- **ADDED:** `CLOAK_LAUNCH_FAILED`, `CLOAK_BINARY_MISSING`,
`CLOAK_PROXY_INVALID` error codes + `CloakLaunchError` class +
`classifyCloakError` helper that turns raw cloakbrowser errors into
classified `TestingE2eError` instances with remediation hints.
- **ADDED:** `@suaveplan/testing-e2e/cloakserve` subpath —
`connectCloakServe(options, chromium?)` + `buildCloakServeUrl(options)`
pair that wraps `chromium.connectOverCDP(...)` for the
[cloakserve](https://github.com/CloakHQ/CloakBrowser#cdp-server-mode)
CDP multiplexer. Strongly-typed query-parameter surface
(`fingerprint`, `timezone`, `locale`, `platform`, GPU vendor /
renderer, hardware concurrency, device memory, screen dimensions,
`proxy`, `geoip`). Errors wrapped in `CloakLaunchError`.
- **ADDED:** `@suaveplan/testing-e2e/presets` subpath — stealth
presets matching the upstream README's "Recommended config for
anti-bot sites" block. `STEALTH_DEFAULT_CLOAK`,
`STEALTH_HEADED_CLOAK`, `stealthMax(proxy)`,
`stealthMaxHeadless(proxy)`.
- **ADDED:** `tests/no-disallowed.test.ts` rule against
`waitForTimeout` / `wait_for_timeout`. Both forms emit CDP traffic
that reCAPTCHA v3 / Enterprise behavioural scorers detect; the
upstream README documents this explicitly. Test files excluded.
- **ADDED:** `docker/` bundle — `Dockerfile.cloakserve` (extends
`cloakhq/cloakbrowser`, adds the Kasada / Akamai font set, runs
`cloakserve` on 9222), `Dockerfile.manager` (extends
`cloakhq/cloakbrowser-manager`, exposes 8080 with a `cloakprofiles`
named volume), `docker-compose.yml` binding both to `127.0.0.1`
with the manager gated behind the `manager` compose profile,
`.env.example` for pinned tags + `AUTH_TOKEN`, operator `README.md`.
- **MODIFIED:** `extendedTest`'s `browser` fixture now launches via
`cloakbrowser.launch()` instead of the stock Playwright chromium
fixture. `freshPage` and any downstream fixture composition keep
the same signature. The launcher is unit-test-injectable via the
`launchBrowserWithCloak(opts, injectedLaunch?)` helper.
- **MODIFIED:** `launchWithExtension` accepts an injectable
`launchPersistentContext` function (default: cloakbrowser's). The
legacy `chromium: BrowserType` parameter is removed.
- **MODIFIED:** `packages/testing/testing-e2e/package.json` adds
`cloakbrowser: ^0.3.28` as a direct dependency. (The dep is pinned
in-package rather than added to `workspaces.catalogs.testing` to
keep this change isolated from concurrent in-flight work that also
touches the root catalog; promote to the testing catalog as a
follow-up if other packages start consuming `cloakbrowser`.)
- **MODIFIED:** Package description, README updated. **No public
re-exports of cloakbrowser symbols** — per CLAUDE.md rule 4,
consumers `import { launch } from "cloakbrowser"` directly when
bypassing the fixture layer.
## Impact
- **Affected specs:** `testing-e2e`.
- **Affected code:** all six consumer packages currently importing from
`@suaveplan/testing-e2e` continue to work unchanged — the browser
launcher swap is transparent to existing tests because the returned
`Browser` object is the same `playwright-core` `Browser` type.
- **Binary footprint:** CI runners gain a ~200MB stealth Chromium
download on first run (cached under `~/.cache/cloakbrowser/`).
Acceptable: the alternative is `playwright install chromium` (~170MB)
on every `@playwright/test` consumer.
- **Out of scope:** Python flavour (`cloakbrowser` PyPI), `cloakserve`
CDP multiplexer, `cloakbrowser-manager` Docker profile UI — none are
consumed by the genesis monorepo today.

View File

@@ -0,0 +1,212 @@
# testing-e2e — delta (swap-testing-e2e-cloakbrowser)
## ADDED Requirements
### Requirement: Cloakserve CDP connect helper
The package MUST expose `@suaveplan/testing-e2e/cloakserve` (also
re-exported from the package root) providing `connectCloakServe(options, chromium?)`
and `buildCloakServeUrl(options)`. The URL builder MUST map camelCase
option keys to cloakserve's kebab-case query-string keys
(`hardwareConcurrency` → `hardware-concurrency`, etc.), stringify
booleans, skip `undefined`/`null` values, and default the base URL to
`http://localhost:9222`. `connectCloakServe` MUST inject `@playwright/test`'s
`chromium` by default with a lazy dynamic-import cache, and wrap any
`connectOverCDP` rejection in a `CloakLaunchError` whose `context`
carries `{ cloakServeUrl }`.
#### Scenario: buildCloakServeUrl maps keys and base URL
- **WHEN** `buildCloakServeUrl({ fingerprint: 42, hardwareConcurrency: 8, geoip: true })` is called
- **THEN** the result equals `http://localhost:9222/?fingerprint=42&hardware-concurrency=8&geoip=true` (param order is preserved)
**Verified by:** `src/cloakserve/build-cloakserve-url.test.ts::"appends fingerprint seed"` AND `src/cloakserve/build-cloakserve-url.test.ts::"maps camelCase to kebab-case"` AND `src/cloakserve/build-cloakserve-url.test.ts::"stringifies booleans"`
#### Scenario: connectCloakServe forwards options and returns the Browser
- **WHEN** `connectCloakServe({ fingerprint: 11111, timezone: "America/New_York" }, injectedDriver)` is called
- **THEN** `injectedDriver.connectOverCDP` is called with a URL containing `fingerprint=11111` and `timezone=America%2FNew_York`, and the returned `Browser` is the injected fake
**Verified by:** `src/cloakserve/cloakserve-connect.test.ts::"uses injected chromium and builds the cloakserve URL"`
#### Scenario: connectCloakServe wraps errors
- **WHEN** the underlying `connectOverCDP` rejects with `ECONNREFUSED`
- **THEN** `connectCloakServe` rethrows a `CloakLaunchError` with
`code === TESTING_E2E.CLOAK.PROXY_INVALID` and
`context.cloakServeUrl` set to the URL that was attempted
**Verified by:** `src/cloakserve/cloakserve-connect.test.ts::"wraps connection errors in CloakLaunchError"` AND `src/cloakserve/cloakserve-connect.test.ts::"classifies generic connect errors as LAUNCH_FAILED"`
### Requirement: Stealth presets
The package MUST expose `@suaveplan/testing-e2e/presets` (also
re-exported from the package root) providing frozen `STEALTH_DEFAULT_CLOAK`
(`{ headless: true }`) and `STEALTH_HEADED_CLOAK`
(`{ headless: false, humanize: true }`) plus `stealthMax(proxy)` and
`stealthMaxHeadless(proxy)` factories that return fresh objects on
every call. `stealthMax(proxy)` MUST return
`{ headless: false, humanize: true, geoip: true, proxy }` — exactly
matching the CloakBrowser README's "Recommended config for anti-bot
sites" block.
#### Scenario: stealthMax matches the upstream recommendation
- **WHEN** `stealthMax("http://proxy:8080")` is called
- **THEN** the result is `{ headless: false, humanize: true, geoip: true, proxy: "http://proxy:8080" }`
**Verified by:** `src/presets/stealth-presets.test.ts::"matches the upstream recommended config"`
#### Scenario: static presets are frozen
- **WHEN** `Object.isFrozen(STEALTH_DEFAULT_CLOAK)` is checked
- **THEN** it is `true` (and same for `STEALTH_HEADED_CLOAK`)
**Verified by:** `src/presets/stealth-presets.test.ts::"is headless and frozen"` AND `src/presets/stealth-presets.test.ts::"is headed + humanized and frozen"`
### Requirement: `waitForTimeout` lint guard
The package's source-guard test MUST flag `waitForTimeout` and
`wait_for_timeout` calls anywhere under `src/` (excluding `.test.ts`
and `.bench.ts`). Both forms emit CDP protocol traffic that
reCAPTCHA v3 / Enterprise behavioural scorers detect — the upstream
CloakBrowser README documents this as a primary cause of low scores.
#### Scenario: guard fires on a synthetic violation
- **WHEN** the `no-disallowed source guard` test runs against the
current `src/` tree
- **THEN** the test passes (no violations) AND the `PATTERNS` array
contains entries named `waitForTimeout` and `wait_for_timeout`
**Verified by:** `tests/no-disallowed.test.ts::"src/ contains no disallowed constructs"`
### Requirement: Docker bundle
The package MUST ship a `docker/` directory containing
`Dockerfile.cloakserve` (extending `cloakhq/cloakbrowser`, installing
the Kasada / Akamai font set, running `cloakserve` on `9222`),
`Dockerfile.manager` (extending `cloakhq/cloakbrowser-manager`,
exposing 8080 with a `cloakprofiles` named volume),
`docker-compose.yml` (both services bound to `127.0.0.1`, manager
behind the `manager` compose profile, named volume defined),
`.env.example` (`CLOAKBROWSER_TAG`, `MANAGER_TAG`,
`MANAGER_AUTH_TOKEN`), and a `docker/README.md` operator playbook.
Both Dockerfiles MUST declare a healthcheck.
#### Scenario: cloakserve Dockerfile builds on the upstream image and exposes 9222
- **WHEN** `docker build -f docker/Dockerfile.cloakserve` is run
- **THEN** the resulting image is layered on `cloakhq/cloakbrowser`,
exposes port `9222`, defaults its CMD to `cloakserve`, and runs the
documented font-install step
**Verified by:** manual operator verification (`docker compose up cloakserve` + smoke connect via `connectCloakServe`); source-level invariants are enforced by reading the Dockerfile in CI is out of scope for this change
### Requirement: CLOAK_* error codes
The package MUST emit `CloakLaunchError extends TestingE2eError` with
codes `TESTING_E2E.CLOAK.LAUNCH_FAILED`,
`TESTING_E2E.CLOAK.BINARY_MISSING`, and
`TESTING_E2E.CLOAK.PROXY_INVALID`. All three codes MUST appear in the
exported `TESTING_E2E_ERROR_CODES` registry and conform to
`TESTING_E2E_ERROR_CODE_PATTERN`.
#### Scenario: codes registered
- **WHEN** `TESTING_E2E_ERROR_CODES` is iterated
- **THEN** the three CLOAK codes are present and each matches the
registry pattern
**Verified by:** `src/errors/testing-e2e-error.test.ts::"registers CLOAK_* codes"` AND `src/errors/testing-e2e-error.test.ts::"CloakLaunchError exposes the LAUNCH_FAILED code by default"`
#### Scenario: BINARY_MISSING raised when cloakbrowser binary not present
- **WHEN** the underlying cloakbrowser launcher throws an error whose
message includes `binary not found`, `ENOENT`, or `Failed to download`
- **THEN** the calling fixture rethrows a `CloakLaunchError` with code
`TESTING_E2E.CLOAK.BINARY_MISSING`, the original error as `cause`,
and a remediation hint pointing to `npx cloakbrowser install`
**Verified by:** `src/errors/testing-e2e-error.test.ts::"classifyCloakError maps ENOENT to BINARY_MISSING"`
## MODIFIED Requirements
### Requirement: Layer 0 dep-graph isolation
The `@suaveplan/testing-e2e` `dependencies` and `peerDependencies` MUST
NOT contain any `@suaveplan/*` workspace package. `dependencies` MUST
include `cloakbrowser` (any valid range — direct or `catalog:testing`)
alongside the existing `@playwright/test`, `@opentelemetry/api`,
`tslib`, and `zod` entries. `devDependencies` MAY include
`@suaveplan/testing` for the package's own tests.
#### Scenario: runtime closure clean and contains cloakbrowser
- **WHEN** `bunx turbo ls --filter=@suaveplan/testing-e2e...` is run
- **THEN** no `@suaveplan/*` package appears in the runtime closure
AND `cloakbrowser` is present as a direct dependency
**Verified by:** `tests/dep-graph.test.ts::"no @suaveplan/* in dependencies or peerDependencies"` AND `tests/dep-graph.test.ts::"cloakbrowser is a direct dependency"`
### Requirement: `extendedTest` default browser is stealth
`extendedTest.browser` MUST be overridden so the default browser used
by every fixture downstream (`freshPage`, consumer-extended fixtures)
launches via `cloakbrowser.launch()` (drop-in for stock chromium).
The override MUST honour `testInfo.project.use.cloak` if defined,
otherwise launch with cloakbrowser defaults (`headless: true`, stealth
args on). The launcher MUST be injectable for unit testing — the
fixture takes an internal default but exposes the launcher symbol on
the `cloak` worker fixture so tests can supply a spy.
#### Scenario: default browser is cloakbrowser
- **WHEN** a test runs under `extendedTest` with no per-project
override
- **THEN** the `browser` fixture is produced by the cloakbrowser
`launch` function (identity check via injected spy)
**Verified by:** `src/fixtures/extended-test.test.ts::"browser fixture is launched via cloakbrowser launch"` AND `src/fixtures/extended-test.test.ts::"cloak options from testInfo.project.use.cloak flow into launch"`
#### Scenario: errors map to CloakLaunchError
- **WHEN** the underlying cloakbrowser launcher throws
- **THEN** the fixture rethrows a `CloakLaunchError` with a classified
code (`LAUNCH_FAILED`, `BINARY_MISSING`, or `PROXY_INVALID`)
**Verified by:** `src/fixtures/extended-test.test.ts::"wraps launch errors in CloakLaunchError"`
### Requirement: Extension fixture uses cloakbrowser launchPersistentContext
`launchWithExtension` MUST accept a `launchPersistentContext` function
parameter (default: cloakbrowser's). The legacy
`chromium: BrowserType` parameter is removed. Extension args
(`--disable-extensions-except`, `--load-extension`) and userDataDir
behaviour are unchanged.
#### Scenario: default launcher is cloakbrowser
- **WHEN** `launchWithExtension({ extensionPath, userDataDir })` is
called with no explicit launcher
- **THEN** `cloakbrowser.launchPersistentContext` is invoked with the
expected `userDataDir` and Chromium args
**Verified by:** `src/extension-fixture/launch-with-extension.test.ts::"defaults to cloakbrowser launchPersistentContext"`
#### Scenario: launcher is injectable
- **WHEN** `launchWithExtension({ launchPersistentContext: spy, extensionPath, userDataDir })` is called
- **THEN** the provided `spy` is invoked exactly once with the
expected `userDataDir` and Chromium args
**Verified by:** `src/extension-fixture/launch-with-extension.test.ts::"uses injected launcher when provided"`
#### Scenario: launchPersistentContext errors map to CloakLaunchError
- **WHEN** the underlying launcher throws an error matching the
`ENOENT`/binary classifier
- **THEN** `launchWithExtension` rethrows a `CloakLaunchError` with
`code === TESTING_E2E.CLOAK.BINARY_MISSING`
**Verified by:** `src/extension-fixture/launch-with-extension.test.ts::"wraps launcher errors with CloakLaunchError"`

View File

@@ -0,0 +1,51 @@
# swap-testing-e2e-cloakbrowser — tasks
- [x] 1. Add `cloakbrowser: ^0.3.28` as a direct dep in `packages/testing/testing-e2e/package.json` (skip the root testing catalog for now to avoid entanglement with concurrent root-`package.json` work).
- [x] 2. Add `./cloakserve` and `./presets` to the package exports map; update description.
- [x] 3. Add `CLOAK_*` error codes:
- [x] Append `LAUNCH_FAILED`, `BINARY_MISSING`, `PROXY_INVALID` to `src/errors/codes.ts` registry under domain `CLOAK`.
- [x] Add `CloakLaunchError` class to `src/errors/testing-e2e-error.ts`.
- [x] Add `classifyCloakError(err)` + `wrapCloakError(err, context?)` helpers.
- [x] Co-located `.md` + tests covering all three codes + the classifier + the wrapper remediation hints.
- [x] 4. Refactor `src/extension-fixture/launch-with-extension.ts`:
- [x] Drop `chromium: BrowserType` parameter.
- [x] Accept optional `launchPersistentContext` injector (default: `cloakbrowser`'s, resolved via dynamic `import` with cache + `_resetDefaultLauncherCache` test hook).
- [x] Forward `launchOptions` (proxy / humanize / geoip / …) untouched.
- [x] Wrap launcher errors with `wrapCloakError`.
- [x] Update tests + `.md`; update `launch-with-extension.types.ts`.
- [x] 5. Override `browser` worker fixture inside `src/fixtures/extended-test.ts`:
- [x] Add `cloak: CloakOptions` worker option (Playwright `option: true`) with `{ headless: true }` default.
- [x] Extract `launchBrowserWithCloak(opts, injectedLaunch?)` as a unit-testable helper backed by `cloakbrowser.launch`.
- [x] Override `browser` fixture to call the helper; `apiContext` depends on Playwright's built-in `playwright` fixture (not `browser`) so API-only tests never start Chromium.
- [x] Wrap errors with `wrapCloakError`.
- [x] Update tests (source-level pins + runtime classifier tests) + `.md`.
- [x] 6. Add `src/cloakserve/`:
- [x] `cloakserve-options.types.ts` with `CloakServeFingerprint` + `CloakServeConnectOptions`.
- [x] `build-cloakserve-url.ts` — pure URL builder with camelCase → kebab-case mapping.
- [x] `cloakserve-connect.ts` — `connectCloakServe(options, chromium?)` wrapping `chromium.connectOverCDP`; lazy `@playwright/test` import + `_resetCloakServeChromiumCache` hook; errors wrapped in `CloakLaunchError`.
- [x] `index.ts` barrel with explicit named exports (rule 10).
- [x] `.md` per impl file + tests covering URL building, default base URL, kebab-case mapping, boolean stringification, error classification, lazy import caching.
- [x] 7. Add `src/presets/`:
- [x] `STEALTH_DEFAULT_CLOAK`, `STEALTH_HEADED_CLOAK` frozen presets.
- [x] `stealthMax(proxy)` + `stealthMaxHeadless(proxy)` factory presets matching the upstream "Recommended config for anti-bot sites" block.
- [x] `.md` + tests.
- [x] 8. Extend `tests/no-disallowed.test.ts` with patterns flagging `waitForTimeout` and `wait_for_timeout` calls in `src/`.
- [x] 9. Add `docker/` bundle:
- [x] `Dockerfile.cloakserve` extending `cloakhq/cloakbrowser`, installing the Kasada / Akamai font set, defaulting to `cloakserve`, healthcheck on `/json/version`.
- [x] `Dockerfile.manager` extending `cloakhq/cloakbrowser-manager`, `/data` volume, healthcheck on `/api/status`.
- [x] `docker-compose.yml` — both services bound to `127.0.0.1`; manager behind the `manager` compose profile; `cloakprofiles` named volume.
- [x] `.env.example` for `CLOAKBROWSER_TAG`, `MANAGER_TAG`, `MANAGER_AUTH_TOKEN`.
- [x] `docker/README.md` operator playbook.
- [x] 10. Update `src/index.ts` barrel: re-export new cloakserve symbols, presets, and CLOAK error helpers.
- [x] 11. Update vite externals + `vite.config.ts` entries for the new subpath modules.
- [x] 12. Rewrite `README.md` — stealth swap summary, binary download note, humanize/proxy/geoip features, worked examples (extendedTest, cloakserve, presets, Docker, extension fixture).
- [x] 13. Add `tests/dep-graph.test.ts` assertion that `cloakbrowser` is a direct dep.
- [x] 14. Pre-publish gate: `bunx turbo lint typecheck test --filter=@suaveplan/testing-e2e` green; `bunx turbo test:coverage --filter=@suaveplan/testing-e2e` at 100% on all metrics; `bun run docs:check` 100%.
- [ ] 15. On ship (post-merge): fold delta into `openspec/specs/testing-e2e/spec.md`; archive change folder under `openspec/archive/<date>-swap-testing-e2e-cloakbrowser/`.
## 0. Pre-flight (non-negotiable)
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.