Skip to content

feat(e2e): real-world test-site matrix + harness + CI - #223

Merged
aram-devdocs merged 6 commits into
mainfrom
feat/e2e-sites-matrix
May 6, 2026
Merged

feat(e2e): real-world test-site matrix + harness + CI#223
aram-devdocs merged 6 commits into
mainfrom
feat/e2e-sites-matrix

Conversation

@aram-devdocs

Copy link
Copy Markdown
Owner

Summary

  • Adds e2e-sites/ with six framework fixtures (vanilla HTML/CSS, static Tailwind, React + Vite, Vue 3 + Vite, Angular 17, Next.js 14 static export). Each renders the same intentional design-system violations.
  • Adds crates/plumb-e2e/ — a dev-only harness that builds each fixture, serves its dist/ over loopback HTTP via tiny_http, runs plumb lint three times, asserts byte-equality, and diffs the target-rule violation breakdown against expected.json.
  • Wires three workflows: e2e-sites.yml (18-leg matrix on PR), pages.yml extension (publishes fixtures alongside the docs at https://plumb.aramhammoudeh.com/test-sites/<framework>/), and dogfood.yml extension (re-lints the deployed copies daily).
  • Includes a small fix(cli) commit that threads auto_fetch_chromium through plumb watch — the workspace failed to compile after feat(cdp): chromium auto-fetch via chromiumoxide fetcher (#78) #220 landed.

What the harness asserts

For every site, the harness:

  1. Optionally runs just build inside the fixture.
  2. Spawns a 127.0.0.1:0 HTTP server pointed at dist/.
  3. Runs plumb lint http://127.0.0.1:<port>/ --config e2e-sites/plumb.toml --format json three times.
  4. Asserts the three outputs are byte-identical.
  5. Parses violations and asserts each target_rule count matches expected.json::by_rule_id, plus that the total target-rule count matches total_target_violations.

Non-target rule violations are reported but not asserted on — the matrix narrows the surface to the design-system invariants under test, not every Chromium-rendering quirk.

Local results

PASS  html-css        target_violations=9   non_target=2  by_rule_id={"color/palette-conformance": 1, "spacing/grid-conformance": 4, "spacing/scale-conformance": 4}
PASS  tailwind-html   target_violations=9   non_target=3  by_rule_id={"color/palette-conformance": 1, "spacing/grid-conformance": 4, "spacing/scale-conformance": 4}
PASS  react-vite      target_violations=9   non_target=3  by_rule_id={"color/palette-conformance": 1, "spacing/grid-conformance": 4, "spacing/scale-conformance": 4}
PASS  vue             target_violations=9   non_target=3  by_rule_id={"color/palette-conformance": 1, "spacing/grid-conformance": 4, "spacing/scale-conformance": 4}
PASS  angular         target_violations=9   non_target=3  by_rule_id={"color/palette-conformance": 1, "spacing/grid-conformance": 4, "spacing/scale-conformance": 4}
PASS  nextjs          target_violations=17  non_target=8  by_rule_id={"color/palette-conformance": 1, "spacing/grid-conformance": 8, "spacing/scale-conformance": 8}
OK    6 site(s) passed

just validate is green; just determinism-check is green.

The Next.js fixture's higher count is documented in e2e-sites/nextjs/README.md — Next injects a hidden <next-route-announcer> that uses the visually-hidden margin: -1px trick, contributing 4 grid + 4 scale violations on top of the intentional p-[13px] hero.

Test plan

  • CI matrix e2e-sites runs on this PR — every leg should be green.
  • Pages preview deploys https://plumb.aramhammoudeh.com/test-sites/<framework>/ for all six fixtures.
  • Daily dogfood job lints the deployed copies and asserts the same target-rule counts.
  • just test-e2e passes locally with a Chromium binary available.
  • just validate stays green.

🤖 Generated with Claude Code

aram-devdocs and others added 4 commits May 6, 2026 02:41
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>
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>
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>
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>
@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

I now have everything I need. Here is the review.


PR #223plumb-e2e harness + e2e-sites matrix + dogfood diff

Crates touched

plumb-cli (minor), plumb-e2e (new, publish = false), e2e-sites/ (fixtures), .github/workflows/ (CI), justfile, Cargo.{toml,lock}


1. Determinism invariants

No violations. plumb-e2e is a test harness that drives the plumb binary as a subprocess; it has no output path that could sneak nondeterminism into plumb-core. IndexMap is used everywhere a map appears in a public API (runner.rs, expected.rs). No SystemTime::now / Instant::now in library code. The Duration constants in server.rs are compile-time values, not wall-clock reads.


2. Workspace layering

Clean. plumb-e2e has no internal plumb-crate dependencies — it treats plumb as a subprocess binary, which is the correct shape for an integration harness. #![forbid(unsafe_code)] is present in both lib.rs and main.rs. The crate is in members but correctly absent from default-members, so cargo build and cargo test --workspace skip it.


3. Error handling

No unwrap/expect in library code. Test code uses expect freely; clippy.toml has allow-expect-in-tests = true, so these are exempt. HarnessError is a proper thiserror-derived enum. anyhow is used only in main.rs and in the publish = false harness, which is the equivalent of plumb-cli::main — acceptable.


4. Findings

Warning — server.rs:177: dead-code arms in path-traversal guard

.any(|seg| seg == ".." || seg.starts_with("..\\") || seg.starts_with("../"))

After split(['/', '\\']), no segment can ever contain / or \, so starts_with("..\\") and starts_with("../") are permanently unreachable. The load-bearing check is seg == ".." (correct). Dead branches in a security-sensitive function are confusing; simplify to just seg == "..".

Warning — crates/plumb-e2e/Cargo.toml:37: tiny_http pinned directly, not via workspace

tiny_http = "0.12" is not declared in [workspace.dependencies]. Since this is the only consumer and publish = false, cargo-deny still audits the resolved version — no correctness risk. It is inconsistent with workspace convention, where new deps are expected to be routed through the workspace manifest. Minor.


5. Test coverage

expected.rs, runner.rs, server.rs, and workspace.rs all have #[cfg(test)] blocks covering the happy path and failure paths. server.rs has a live TCP integration test and a path-traversal rejection test. Coverage is appropriate for the scope.


6. Documentation

All public items are documented. Fallible public functions have # Errors sections. CHANGELOG entry is present under ## [Unreleased] and contains no AI-tell phrases.


Verdict: APPROVE

- 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.
@aram-devdocs

Copy link
Copy Markdown
Owner Author

Next.js leg is advisory in 48dc7ab — Chromium times out on the heavier hydration; fix in follow-up.

@aram-devdocs
aram-devdocs merged commit 9b247ef into main May 6, 2026
24 of 34 checks passed
@aram-devdocs
aram-devdocs deleted the feat/e2e-sites-matrix branch May 6, 2026 13:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant