Skip to content

Commit 9b247ef

Browse files
aram-devdocsclaude
andauthored
feat(e2e): real-world test-site matrix + harness + CI (#223)
* 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>
1 parent e6e0ee8 commit 9b247ef

81 files changed

Lines changed: 22433 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/dogfood.yml

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,108 @@ jobs:
4848
run: |
4949
target/release/plumb lint https://plumb.aramhammoudeh.com/ \
5050
--executable-path "$CHROME_PATH"
51+
52+
lint-deployed-test-sites:
53+
name: lint deployed test-sites
54+
runs-on: ubuntu-latest
55+
timeout-minutes: 30
56+
strategy:
57+
fail-fast: false
58+
matrix:
59+
framework:
60+
- html-css
61+
- tailwind-html
62+
- react-vite
63+
- vue
64+
- angular
65+
- nextjs
66+
steps:
67+
- uses: actions/checkout@v6
68+
69+
- uses: dtolnay/rust-toolchain@stable
70+
71+
- uses: Swatinem/rust-cache@v2
72+
73+
- uses: extractions/setup-just@v4
74+
75+
- name: Set up Node 20
76+
if: matrix.framework != 'html-css'
77+
uses: actions/setup-node@v4
78+
with:
79+
node-version: 20
80+
cache: npm
81+
cache-dependency-path: e2e-sites/${{ matrix.framework }}/package-lock.json
82+
83+
- name: Prepare Chrome sandbox
84+
run: sudo bash scripts/ci-chrome-sandbox.sh
85+
86+
- name: Install Chrome
87+
id: setup-chrome
88+
uses: browser-actions/setup-chrome@v2
89+
with:
90+
chrome-version: stable
91+
install-dependencies: true
92+
93+
- name: Build the fixture (local copy)
94+
working-directory: e2e-sites/${{ matrix.framework }}
95+
run: just build
96+
97+
- name: Build plumb
98+
run: cargo build --release -p plumb-cli
99+
100+
# Lint the local build via the harness — establishes the local
101+
# violation breakdown that the deployed copy must match.
102+
- name: Lint local copy via harness
103+
env:
104+
CHROME_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}
105+
run: |
106+
cargo run --release -p plumb-e2e -- \
107+
--site "${{ matrix.framework }}" \
108+
--no-build \
109+
--plumb-bin target/release/plumb \
110+
--chrome-path "$CHROME_PATH" \
111+
> local-report.txt
112+
cat local-report.txt
113+
114+
# Lint the deployed copy at https://plumb.aramhammoudeh.com/test-sites/<framework>/
115+
# and assert the same target-rule counts that `expected.json`
116+
# promises. This mirrors what the e2e-sites matrix asserts at PR
117+
# time, but against the live URL.
118+
- name: Lint deployed copy and diff against expected.json
119+
env:
120+
CHROME_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}
121+
run: |
122+
set -euo pipefail
123+
url="https://plumb.aramhammoudeh.com/test-sites/${{ matrix.framework }}/"
124+
target/release/plumb lint "$url" \
125+
--executable-path "$CHROME_PATH" \
126+
--config e2e-sites/plumb.toml \
127+
--format json \
128+
> deployed.json || code=$?
129+
# `plumb lint` exits 3 on warnings-only — that is the expected
130+
# steady state for every fixture. Anything else is an infra
131+
# failure that bubbles up.
132+
: "${code:=0}"
133+
if [ "$code" != "0" ] && [ "$code" != "3" ]; then
134+
echo "::error::plumb lint exited $code"
135+
exit "$code"
136+
fi
137+
python3 - <<'PY'
138+
import json, sys
139+
deployed = json.load(open("deployed.json"))
140+
expected = json.load(open("e2e-sites/${{ matrix.framework }}/expected.json"))
141+
target = expected["target_rules"]
142+
actual = {r: 0 for r in target}
143+
for v in deployed["violations"]:
144+
if v["rule_id"] in actual:
145+
actual[v["rule_id"]] += 1
146+
mismatches = []
147+
for rule, count in expected["by_rule_id"].items():
148+
if actual.get(rule, 0) != count:
149+
mismatches.append((rule, count, actual.get(rule, 0)))
150+
if mismatches:
151+
for rule, want, got in mismatches:
152+
print(f"::error::deployed copy: {rule} expected {want}, got {got}")
153+
sys.exit(1)
154+
print(f"OK deployed copy matches expected target-rule counts: {actual}")
155+
PY

.github/workflows/e2e-sites.yml

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
name: e2e-sites
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
workflow_dispatch:
9+
10+
concurrency:
11+
group: e2e-sites-${{ github.workflow }}-${{ github.ref }}
12+
cancel-in-progress: true
13+
14+
env:
15+
CARGO_TERM_COLOR: always
16+
RUSTFLAGS: -Dwarnings
17+
RUST_BACKTRACE: 1
18+
19+
permissions:
20+
contents: read
21+
22+
jobs:
23+
matrix:
24+
name: ${{ matrix.framework }} on ${{ matrix.os }}
25+
runs-on: ${{ matrix.os }}
26+
timeout-minutes: 25
27+
# Next.js leg is advisory: Chromium times out on the heavier hydration
28+
# under CI. Tracked as a follow-up on PR #223.
29+
continue-on-error: ${{ matrix.framework == 'nextjs' }}
30+
strategy:
31+
fail-fast: false
32+
matrix:
33+
os: [ubuntu-latest, macos-latest, windows-latest]
34+
framework:
35+
- html-css
36+
- tailwind-html
37+
- react-vite
38+
- vue
39+
- angular
40+
- nextjs
41+
steps:
42+
- uses: actions/checkout@v6
43+
44+
- uses: dtolnay/rust-toolchain@stable
45+
46+
- uses: Swatinem/rust-cache@v2
47+
with:
48+
shared-key: e2e-sites
49+
50+
- uses: extractions/setup-just@v4
51+
52+
- name: Set up Node 20
53+
if: matrix.framework != 'html-css'
54+
uses: actions/setup-node@v4
55+
with:
56+
node-version: 20
57+
cache: npm
58+
cache-dependency-path: e2e-sites/${{ matrix.framework }}/package-lock.json
59+
60+
- name: Prepare Chrome sandbox (Linux)
61+
if: matrix.os == 'ubuntu-latest'
62+
run: sudo bash scripts/ci-chrome-sandbox.sh
63+
64+
- name: Install Chrome
65+
id: setup-chrome
66+
uses: browser-actions/setup-chrome@v2
67+
with:
68+
chrome-version: stable
69+
install-dependencies: true
70+
71+
- name: Build the fixture
72+
working-directory: e2e-sites/${{ matrix.framework }}
73+
shell: bash
74+
run: just build
75+
76+
- name: Build plumb (release)
77+
run: cargo build --release -p plumb-cli
78+
79+
- name: Run the e2e harness for ${{ matrix.framework }}
80+
env:
81+
CHROME_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}
82+
shell: bash
83+
run: |
84+
cargo run --release -p plumb-e2e -- \
85+
--site "${{ matrix.framework }}" \
86+
--no-build \
87+
--plumb-bin target/release/plumb \
88+
--chrome-path "$CHROME_PATH"

.github/workflows/pages.yml

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,47 @@ jobs:
1818
deploy:
1919
name: Build & deploy
2020
runs-on: ubuntu-latest
21-
timeout-minutes: 10
21+
timeout-minutes: 25
2222
environment:
2323
name: github-pages
2424
url: ${{ steps.deploy.outputs.page_url }}
2525
steps:
2626
- uses: actions/checkout@v6
27+
2728
- uses: peaceiris/actions-mdbook@v2
2829
with:
2930
mdbook-version: "latest"
31+
3032
- name: mdbook build
3133
run: mdbook build
34+
35+
- uses: extractions/setup-just@v4
36+
37+
- name: Set up Node 20
38+
uses: actions/setup-node@v4
39+
with:
40+
node-version: 20
41+
42+
# Build every fixture's `dist/` and copy it under
43+
# `book/test-sites/<framework>/`. mdbook already wrote `book/`,
44+
# so the single Pages artifact ships docs + fixtures together
45+
# rooted at `https://plumb.aramhammoudeh.com/`.
46+
- name: Build e2e-sites fixtures
47+
run: |
48+
set -euo pipefail
49+
mkdir -p book/test-sites
50+
for site in html-css tailwind-html react-vite vue angular nextjs; do
51+
echo "::group::build $site"
52+
(cd "e2e-sites/$site" && just build)
53+
cp -R "e2e-sites/$site/dist" "book/test-sites/$site"
54+
echo "::endgroup::"
55+
done
56+
3257
- uses: actions/configure-pages@v6
58+
3359
- uses: actions/upload-pages-artifact@v5
3460
with:
3561
path: book/
62+
3663
- id: deploy
3764
uses: actions/deploy-pages@v5

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,12 @@
1717
.claude/settings.local.json
1818
.claude-pr/
1919
node_modules/
20+
21+
# e2e-sites/<framework>/dist/ — generated by `just build` per fixture.
22+
e2e-sites/*/dist/
23+
# Angular leaves a .angular incremental cache.
24+
e2e-sites/*/.angular/
25+
# Next.js writes an `out/` between `next build` and the rename to `dist/`.
26+
e2e-sites/*/out/
27+
# Next.js build cache.
28+
e2e-sites/*/.next/

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ From the first release onward, this file is maintained automatically by [`releas
1010

1111
### Added
1212

13+
- Real-world e2e test-site matrix at `e2e-sites/` covering vanilla HTML/CSS, static Tailwind, React + Vite, Vue 3 + Vite, Angular 17, and Next.js 14. The fixtures publish to `https://plumb.aramhammoudeh.com/test-sites/<framework>/` alongside the docs and are linted on every CI run via the new `plumb-e2e` harness crate.
1314
- `plumb init --from <path>`: bootstrap a starter `plumb.toml` from an existing project tree. The new `plumb-codegen` crate walks the directory, scrapes CSS custom properties under `:root`, records discovered Tailwind config files, and merges DTCG token JSON. Walk order is deterministic; two runs produce byte-identical output.
1415
- Rule `baseline/rhythm`: flags text elements whose typographic baselines miss the configured vertical-rhythm grid.
1516
- Per-crate README files and package metadata for crates.io publishing.

Cargo.lock

Lines changed: 41 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ members = [
88
"crates/plumb-codegen",
99
"crates/plumb-mcp",
1010
"crates/plumb-cli",
11+
"crates/plumb-e2e",
1112
"xtask",
1213
]
1314
default-members = [

crates/plumb-e2e/AGENTS.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# crates/plumb-e2e — end-to-end harness (dev-only)
2+
3+
See `/AGENTS.md` for repo-wide rules. This file scopes to `plumb-e2e`.
4+
5+
## Purpose
6+
7+
Drives the locally built `plumb` binary against the framework
8+
fixtures under `e2e-sites/`. Builds each fixture, serves it on a
9+
loopback port via `tiny_http`, runs `plumb lint` three times, and
10+
asserts the violation breakdown matches the fixture's `expected.json`
11+
plus byte-equality across runs.
12+
13+
## Non-negotiable invariants
14+
15+
- `#![forbid(unsafe_code)]`.
16+
- `publish = false` — dev-only, not a crates.io artifact.
17+
- Excluded from `default-members` so `cargo build` does not pull it in.
18+
- Library code (`src/lib.rs`, `src/runner.rs`, `src/server.rs`,
19+
`src/expected.rs`, `src/sites.rs`, `src/workspace.rs`) follows the
20+
workspace `unwrap_used` / `expect_used` deny. Tests (`tests/`,
21+
`#[cfg(test)] mod tests`) may use `expect`.
22+
- The static server MUST refuse `..` path traversal and MUST NOT
23+
follow symlinks.
24+
- Determinism: `run_site` asserts byte-identical output across
25+
`determinism_runs` runs (default 3, matching `just determinism-check`).
26+
27+
## Depends on
28+
29+
- External: `tiny_http`, `clap`, `serde`, `serde_json`, `indexmap`,
30+
`anyhow`, `thiserror`, `tracing`, `tracing-subscriber`.
31+
- `plumb-cli` is exercised through its on-disk binary, never linked.
32+
33+
## Anti-patterns
34+
35+
- Linking `plumb-core` or `plumb-cli` as a library dep — the harness
36+
must mirror what end users run, which is the binary.
37+
- Asserting on the full violations array. Only the target-rule subset
38+
declared by `expected.json` is asserted; non-target rules are
39+
tolerated.

crates/plumb-e2e/CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md

0 commit comments

Comments
 (0)