feat(cdp): chromium auto-fetch via chromiumoxide fetcher (#78) - #220
Conversation
Adds an opt-in `--auto-fetch-chromium` flag that downloads Chrome-for-Testing into a Plumb-managed cache directory when no system Chromium is detected and no `--executable-path` is given. The fetched binary is hash-pinned via a `.plumb-sha256` sidecar so a later tampering attempt is detected at launch and refused. * New `plumb_cdp::fetcher` module with platform cache-dir resolution (XDG / `Library/Caches` / `LOCALAPPDATA`), SHA-256 verification, cache-root canonicalisation, and the `ensure_chromium` async entry that drives chromiumoxide's `BrowserFetcher`. * New typed errors: `AutoFetchFailed`, `HashMismatch`, `CacheDirUnavailable`. * `ChromiumOptions` gains `auto_fetch_chromium: bool` and `auto_fetch_cache_dir: Option<PathBuf>` so tests can scope the cache to a tempdir without hitting `$HOME`. * `ChromiumDriver` and `PersistentBrowser` resolve auto-fetch up front; an explicit `executable_path` always wins so the flag is a no-op when the user already supplied a binary. * CLI: `lint --auto-fetch-chromium` (default `false`). * Docs: install-chromium.md gains an auto-fetch section with the cache-path table and a trust-model note. 20 fetcher unit tests cover cache path resolution across all platforms (including empty `XDG_CACHE_HOME`), SHA-256 vector spot checks, sidecar record/verify/refuse-on-mismatch, sidecar trailing-newline tolerance, sibling-tempdir rejection by `enforce_cache_root`, and pinning of `pinned_milestone` to `MIN_SUPPORTED_CHROMIUM_MAJOR`. Two CLI integration tests confirm the flag appears in `--help` and that the FakeDriver path treats the flag as a no-op. Closes #78.
|
Now I have enough to write the full review. PR Review:
|
* fix(cli): wire auto_fetch_chromium through plumb watch Add the `--auto-fetch-chromium` flag to the `Watch` clap command and forward it to the inner `LintArgs` so `plumb watch` builds again. Without this, the workspace failed to compile because `LintArgs` declares `auto_fetch_chromium` as a required field after #220 landed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(e2e): add plumb-e2e harness crate Dev-only crate that drives the locally built `plumb` binary against the framework fixtures under `e2e-sites/`. Builds each fixture, serves its `dist/` over loopback HTTP via tiny_http, runs `plumb lint http://127.0.0.1:<port>/ --config e2e-sites/plumb.toml --format json` three times, asserts byte-equality across runs, and diffs the target-rule violation breakdown against `expected.json`. `plumb-e2e` is excluded from `default-members`, never linked from upstream crates (it shells out to the binary), and depends only on external crates plus the workspace lints. Fixtures register themselves in `sites::SITES`; the matrix CI workflow iterates the same list. Harness invariants: - The static server refuses path-traversal and never follows symlinks. - A 25 ms recv polling cadence plus a worker-thread fan-out keeps Chromium's many-parallel-chunks pattern responsive. - The harness shell-execs `just build` per fixture, so each fixture owns its own toolchain pinning (npm lockfile, etc.). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(e2e): real-world fixture matrix under e2e-sites/ Six framework fixtures rendering the same intentional violations: - html-css — vanilla HTML + hand-rolled CSS tokens. - tailwind-html — static HTML built with Tailwind v4 CLI. - react-vite — React 18 + Vite + Tailwind v4. - vue — Vue 3 + Vite + Tailwind v4. - angular — Angular 17 standalone components + Tailwind v4 (compiled standalone — Angular's bundler peer-pins Tailwind 2/3). - nextjs — Next.js 14 App Router exported as static HTML. Every fixture targets `color/palette-conformance`, `spacing/grid-conformance`, and `spacing/scale-conformance` via a `p-[13px]` hero (4 + 4 violations across the longhand sides) and a `text-[#2e7d2e]` alert (1 palette violation). The `border-[#0b0b0b]` on each alert pins border-color to a palette token so the four `border-*-color` longhands stay clean. Each fixture has: - A `Justfile` with `build` / `clean` recipes. - An `expected.json` declaring `target_rules`, `by_rule_id`, and `total_target_violations`. The harness asserts equality on the target subset; non-target rules are tolerated to stay robust against incidental Chromium-side rendering differences (Next.js's hidden `<next-route-announcer>` + 4 sides of `margin: -1px` is documented in nextjs/README.md). Lockfiles are committed for every fixture that has node deps so CI builds are reproducible. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(e2e): publish test-sites + matrix workflow + dogfood diff Three pieces wire the new e2e-sites infrastructure into CI: 1. `.github/workflows/e2e-sites.yml` — 18-leg matrix ({ubuntu, macos, windows} × six fixtures). Each leg sets up Node 20 (actions/setup-node@v4 with npm cache keyed on the per-fixture lockfile) and Chrome stable, builds the fixture via `just build`, compiles `plumb-cli` in release, and runs the harness against that single site. The harness asserts byte-equality across three lint runs and the target-rule breakdown against `expected.json`. 2. `.github/workflows/pages.yml` — extends the docs deploy to also build every fixture's `dist/` and copy it under `book/test-sites/<framework>/`. Pages serves the docs and fixtures from the same artifact; deployed URLs are `https://plumb.aramhammoudeh.com/test-sites/<framework>/`. 3. `.github/workflows/dogfood.yml` — adds a `lint-deployed-test-sites` matrix that lints each deployed fixture URL daily and asserts the live target-rule counts still match the fixture's `expected.json`. The local copy is also re-linted via the harness so a remote drift surfaces before a real regression ships. Plus the `just test-e2e` recipe (local mirror of the CI matrix) and a CHANGELOG entry under `[Unreleased] / Added`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(e2e): Windows binary path + flag Next.js leg as advisory - Resolve `--plumb-bin` paths on Windows by appending `EXE_SUFFIX` when the bare path doesn't exist; canonicalize to drop mixed `\` / `/` separators in error messages and child-process spawns. - Mark the Next.js matrix leg as `continue-on-error` while we sort out the Chromium hydration timeout in a follow-up. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Closes #78. Adds an opt-in
--auto-fetch-chromiumflag that downloads Chrome-for-Testing into a Plumb-managed cache directory when no system Chromium is detected and no--executable-pathis given. The fetched binary is hash-pinned via a.plumb-sha256sidecar so a later tampering attempt is detected at launch and refused.What changed
plumb-cdp::fetcher(new module) — platform cache-dir resolution (XDG /Library/Caches/LOCALAPPDATA), SHA-256 hash verification, cache-root canonicalisation, and theensure_chromiumasync entry that driveschromiumoxide::fetcher::BrowserFetcher.CdpErrorgainsAutoFetchFailed,HashMismatch, andCacheDirUnavailablevariants.ChromiumOptionsgainsauto_fetch_chromium: boolandauto_fetch_cache_dir: Option<PathBuf>(the latter for testability).ChromiumDriverandPersistentBrowserresolve auto-fetch up front; an explicitexecutable_pathalways wins so the flag is a no-op when the user already supplied a binary.lint --auto-fetch-chromium(defaultfalse).install-chromium.mdgains an auto-fetch section with the cache-path table and a trust-model note.Trade-off
chromiumoxide_fetcher 0.8does not re-exportMilestoneat the crate top level (onlyBrowserVersion,Channel,Revision,Version,VersionError), so we cannot pin the fetched binary toMIN_SUPPORTED_CHROMIUM_MAJORvia the typedBrowserVersion::Milestone(...)variant. We useChannel::Stable(Chrome-for-Testing's current stable build) instead — Plumb's existingvalidate_browser_versioncheck fires at launch and refuses to proceed if the fetched binary's major falls outsideMIN..=MAX_SUPPORTED_CHROMIUM_MAJOR, so a stable-channel drift surfaces as a typedUnsupportedChromiumerror rather than a mysterious launch failure. Documented in code atfetcher::ensure_chromium.Security boundary
Auto-fetch downloads and executes a third-party binary. chromiumoxide does not ship signature verification for Chrome-for-Testing artifacts, so passing
--auto-fetch-chromiumis the user's explicit acknowledgement of trust. The SHA-256 sidecar protects against post-install tampering, not against a compromised upstream. Documented infetcher.rsmodule docs and indocs/src/install-chromium.md.Test plan
cargo fmt --all -- --checkcargo clippy --workspace --all-targets --all-features -- -D warningscargo nextest run --workspace --all-features(402 tests, all pass)just determinism-check(3 runs byte-identical)cargo deny check(advisories / bans / licenses / sources OK)RUSTDOCFLAGS=-Dwarnings cargo doc -p plumb-cdp --all-features --no-depsjust check-agentsplumb_cdp::fetcher::testsunits cover cache-path resolution (Linux/macOS/Windows + empty XDG + unknown OS), SHA-256 vector spot checks, sidecar record/verify/refuse-on-mismatch, sidecar trailing-newline tolerance, sibling-tempdir rejection byenforce_cache_root, andpinned_milestone == MIN_SUPPORTED_CHROMIUM_MAJOR.--auto-fetch-chromiumshows up in--helpand that the FakeDriver path treats the flag as a no-op (so the test suite never hits the network).🤖 Generated with Claude Code