ci(tailwind): build the Tailwind plugin against real Tailwind, and assert the bad build fails (#17) - #28
Conversation
Nothing in CI ran the plugin through an actual Tailwind build. The suite in
tests/js calls the plugin's exports directly, so every claim it makes about
Tailwind is a proxy, and a proxy cannot notice the real contract changing.
The sharpest case is the marker that lets the CSS-first path pass options:
assert.equal(cfUiAxes.__isOptionsFunction, true);
That asserts cf-ui still sets a flag. It does not assert Tailwind still reads
it. Verified by mutation — renaming the marker in both the plugin and that
assertion leaves the 65-test suite fully green while the real build fails with
"does not accept options", which is the #7 defect restored: a consumer who
cannot pass a composition never gets theirs validated, so only the default
composition is checked, and that one always passes.
The new job covers all of it: both good paths compile (bare `@plugin`, and
`@plugin { composition: console; }`); the compiled CSS actually carries the
axis rules in both modes, the theme aliases, and the wide-gamut layer, since a
build that succeeds and emits nothing is the other silent failure; and an
unknown composition exits non-zero, read from the process itself because piping
through anything else reports the pipeline's status instead of Tailwind's. That
last check also asserts *why* the build failed, so a failure for an unrelated
reason cannot satisfy it.
Its own job rather than three matrix steps — nothing here depends on the Python
version. Tailwind is pinned via package.json plus a committed lockfile, so a
bump breaking this is the signal the job exists to produce. `just test-tailwind`
runs it locally, and it throws rather than skipping when the toolchain is
missing, because a skip reads as a pass.
Follow-up, not fixed here: `just test-js` runs zero tests and exits 0 on
Windows — under cmd.exe the quoted glob reaches node literally. CI is
unaffected, and it is off this issue's checklist.
Closes #17
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NhqNRBg83czKfr8L6FF5xf
Adversarial reviewOverviewAdds a The design is right for what the issue asks. Assertions are driven off Verified by execution, not by readingThe content assertions are not vacuous. This was the biggest risk — if stock Tailwind emitted any of these tokens on its own, the assertions would pass against a plugin that did nothing. Control build,
Every claim is attributable to the plugin. The error-message regex survives CI's output shape. The job is not a silent no-op. Checked specifically because that is this ticket's own failure mode, and because a 7-second green job invites the assumption. The run log shows all 8 tests executing individually, not a zero-test pass. The wheel does not ship any of this. Findings1. [low] 2. [low] 3. [low] 4. [low] Node is pinned to 22 in the new job and unpinned in Nothing found onCorrectness of the fixture paths and The two follow-ups named in the PR description — the Windows |
Closes #17.
What this does
Adds a CI job that runs the vendored plugin through the actual Tailwind CLI, because nothing did before. The suite in
tests/jscalls the plugin's exports directly — fast, and it is where the logic belongs — but every claim it makes about Tailwind is a proxy, and a proxy cannot notice the real contract changing underneath it.The check has teeth — verified by mutation, not by assertion
The issue names the weak spot exactly:
That asserts cf-ui still sets a flag. It does not assert Tailwind still reads it. So I reproduced the scenario rather than arguing about it — renamed the marker to
__acceptsOptionsin both the plugin and that assertion, then ran both suites:tests/js(65 tests)tests/tailwind(new)Error: The plugin "..." does not accept optionsThat is the #7 defect restored, with a fully green suite sitting on top of it. A CSS-first consumer who cannot pass a composition never gets theirs validated, so the plugin only ever checks the default composition — which always passes. The feature is inert while looking fine.
Both files were restored via
git checkoutand both suites are green again (65 + 8).Worth noting the second failure in that run: the bad-path test also asserts why the build failed (
unknown composition 'brutalist'), so it went red when the build started failing for a different reason. A failure for any old reason cannot satisfy it.Scope checklist
tailwindcss+@tailwindcss/cliand builds against the vendored plugin — newtailwind-buildjob in.github/workflows/ci.yml; fixtures reference the plugin by relative path, exactly as a consumer would.@plugin, and@plugin { composition: console; }—fixtures/bare.cssandfixtures/composition.css.--color-primaryaliases, and the@media (color-gamut: p3)layer — driven offcf_ui_axes.jsonrather than a hand-written list, so it covers every axis value and both modes and cannot drift from the definition. A build that succeeds and emits nothing fails here.fixtures/bad-composition.css. The CLI is spawned withspawnSyncandstatusis read straight off the process; nothing is piped, so nothing can mask it.statusis kept raw rather than coerced, so a crash-by-signal (null) cannot satisfy anequal(status, 0)either.cf_ui_axes.jsonis committed, so the job needs no Python at all.4.3.3for both packages intests/tailwind/package.json, plus a committed lockfile so CI runsnpm ci.Notes
source(none)on the@importkeeps Tailwind from crawling the repo for utility classes. This check is about the plugin's base layer, and auto-detection would make the build both slower and sensitive to unrelated files.The harness throws rather than skipping when the toolchain is absent. That mirrors the reasoning already in the workflow for
tests/js— a skip reads as a pass, and this file exists precisely because a green suite hid an inert feature once already.just test-tailwindinstalls and runs it locally.Follow-ups, deliberately not in this diff
just test-jsruns zero tests and exits 0 on Windows. Undercmd.exethe quoted glob"tests/js/**/*.test.mjs"reaches node literally, node matches nothing, and node exits 0 — a silent pass, which is the same class of problem this ticket is about. CI is unaffected (bash strips the quotes, and the workflow runs the command directly), so this is local-only. Off CI: build the Tailwind plugin against real Tailwind, and assert the bad build fails #17's checklist, so it wants its own ticket.valueSetscannot be expressed through@plugin's flat key/value options and needs atailwind.config.js. The literal checklist asks for the two CSS-first paths, so that is what this delivers.🤖 Generated with Claude Code