Skip to content

feat(vibe-tests): setup test — installing the system into an app that already has one - #5268

Draft
nynexman4464 wants to merge 2 commits into
mainfrom
nynexman4464/vibe-tests/setup
Draft

feat(vibe-tests): setup test — installing the system into an app that already has one#5268
nynexman4464 wants to merge 2 commits into
mainfrom
nynexman4464/vibe-tests/setup

Conversation

@nynexman4464

Copy link
Copy Markdown
Contributor

The gap

Every vibe test we run hands the agent a project where the design system is already correctly wired. The nightly's project-astryx-tailwind environment ships a globals.css with the layer order declared and every import in place. The adoption test (#4902) installs the system in every condition on purpose, because there the variable is whether the agent reaches for it.

So nothing we run measures the install — the twenty minutes between "we're going to use Astryx" and "the app renders correctly with Astryx in it", in an app that already has a styling system, a token vocabulary, house components and a year of CSS.

Why that gap is expensive

A bad install looks exactly like a good one. Applying the recipe packages/core/README.md prints, to the fixture app, by script:

tsc --noEmit clean
vite build clean
console / page errors 0
failed requests 0
contrast on the page title 15.79 → 1.10

The toolchain says nothing. The only signal is that the page is wrong — the one signal an agent doesn't have, CI doesn't have, and a diff review doesn't have. A greenfield arm cannot produce this finding at all: there is no "before" to measure against.

What this PR is

The design (PLAN.md) and a working harness. No agent has run it yet — no agent findings, no scores, no recommendation. Mechanism verified without agents first, same order as cli-discovery-test.

Nightly evaluation Adoption test (#4902) This test
Environment empty scaffold per target one fixed app, system installed one fixed app, system absent
Variable the design system the guidance surface the guidance surface
The task build a thing build a feature install the system
Output one .tsx file a working-tree diff a built, rendered page
"Correct" valid component usage did it adopt, does it fit is the app still the app

Pieces

  • fixture-app/ — the same ops console the adoption test uses, made runnable. A setup run has to be compiled and rendered to be scored, so this is a real Vite + React + Tailwind v4 build with a middleware data stub. It builds and renders before every run, and that pristine build is the reference every number is a delta against. (When feat(vibe-tests): adoption test — introducing Astryx into an app that already has a styling system #4902 lands the two fixtures should converge to one copy — this one is a superset.)
  • conditions.json — four arms: public docs only (floor) → AGENTS.md points at the CLI → an existing-app recipe exists → handed the exact edits (ceiling). The system is installed in no condition; installing it is the task.
  • prompts.json — three feature-shaped requests. None of them says "don't break the app": a recipe that only works when the user thinks to ask for that is not a recipe.
  • setup-measure.mjs — build, serve, render, probe — in both color schemes. Astryx paints through light-dark() and the fixture has no color-mode system for it to read, so an arm can be correct on a dark-mode laptop and unreadable in headless CI. A single-scheme measurement would inherit whichever machine ran it.
  • setup-eval.ts — the deterministic scorer: app regressions by category, legibility against the AA floor, mode dependence, variable capture. No LLM in this path, and the analyzer never sees the condition id (Checker Protocol §1).
  • apply-recipe.mjs — our own documented recipes, transcribed, so the mechanism can be verified with no agent in the loop and so the ceiling arm has something to hand an agent verbatim.
  • setup-eval.test.ts — 20 tests, each check asserted on a sample that should trip it and one that shouldn't.

Mechanism-verification run

Three recipes applied by script, no agent. --scheme light (a light-mode machine, and every headless CI):

arm builds console app regressions unreadable mode-dependent vars captured verdict
docs-verbatim yes 0 30 (typo 22, color 4, geom 1, spacing 3) 3 3 5 silent-damage
docs-no-bridge yes 0 11 (typo 7, color 4) 3 3 2 silent-damage
layered-in-place yes 0 11 (typo 7, color 4) 0 0 2 cosmetic-drift

Same three sandboxes under --scheme dark: zero unreadable probes in every arm. That flip is the "works on my machine" failure, and it is why both schemes are measured.

Three causes separate cleanly, and each is separately fixable:

  1. The recipe is written for an empty file. Every documented Tailwind recipe replaces globals.css wholesale, with Tailwind imported in three pieces so Astryx can sit between them. An existing app's file is not empty. Transcribed literally, --spacing, --text-xs and --font-sans stop resolving at :root, and the type scale moves under every existing utility — text-xs in this console goes 12px → 10px, across every table, badge and button.
  2. The Tailwind bridge renames tokens the app already uses. tailwind-theme.css declares --color-card, --color-muted, --color-border, --color-accent, --color-primary, --color-secondary, the whole --radius-* and --shadow-* ladders, the --text-* scale and the base --spacing unit — 13 exact collisions with the default shadcn/Tailwind vocabulary, re-pointed app-wide by one import line. It is printed unconditionally in the README, in astryx docs styling-libraries, and in our own environments/project-astryx-tailwind.
  3. <Theme> is a color boundary that follows the OS. It paints color: light-dark(#171717, #fafafa) on its wrapper; an app whose dark look is its own CSS variables has no data-theme for it to read, so every inherited piece of text under the provider turns near-black on a near-black surface. mode fixes it in one word and no setup path mentions it.

The candidate recipe (4 CSS lines and a prop, against the documented 7-line replacement) removes the legibility failure and the mode dependence and cuts regressions from 30 to 11. What it cannot remove is the residue: 11 properties still move, all typography and inherited color. That is a product question — there is currently no way to adopt the components without adopting the type ramp for everything under the provider — and it is what the ceiling arm exists to expose.

Pre-registered decision rule

Set before any agent runs. A guidance change ships if, against the floor, it eliminates legibility failures and halves app regressions with non-overlapping CIs, without increasing console errors or breaking the build. Regression count alone never justifies shipping — it is a drift measure; legibility and mode dependence are the severity ones. Damage the ceiling arm still produces is a product finding filed against the packages, not evidence that guidance needs more words.

Verification

  • pnpm vitest run internal/vibe-tests/setup-test — 20 tests pass.
  • Each check proven to fail without its detector. Disabled the contrast check, dropped the missing-probe record, made mode dependence always empty, made variable capture blind to emptied variables, and made the verdict never say silent-damage — every mutation turned the suite red, and green again on restore.
  • Harness exercised end to end: run-setup.mjs builds both arms' sandboxes, each builds before the agent arrives, the guidance lands in AGENTS.md, the git baseline commits, and setup-measure.mjs + setup-aggregate.ts produce the tables above.
  • eslint clean (0 errors, 0 warnings on the new files); pre-commit check:* suite clean.

Notes for review

  • The s2 guidance text is a strawman, not a proposal — it is the candidate the test exists to judge.
  • One fixture: findings generalize to "dense console with an existing utility-CSS vocabulary and a dark surface", not to all apps. A light-surface fixture would not produce cause 3 at all, which is why that is stated rather than assumed.
  • eslint.config.js gains one ignore, and internal/vibe-tests/.gitignore one negation so the fixture's own AGENTS.md — the independent variable — is not swallowed by the generated-docs rule.
  • Sandboxes share one prepared node_modules for the app's own dependencies, so registry friction on the system's packages is measured but install-time flakiness is not.

Next step is a pilot — floor vs ceiling on s1, K=3, one agent — enough to answer "do the arms separate at all" before anyone pays for the full grid. Not run yet.

… already has one

Every arm we run today hands the agent a project where the design system is
already correctly wired: the nightly's `project-astryx-tailwind` environment
ships a globals.css with the layer order declared and every import in place, and
the adoption test installs the system in every condition on purpose. So nothing
measures the install itself — the twenty minutes between "we're going to use
Astryx" and "the app renders correctly with Astryx in it", in an app that already
has a styling system, a token vocabulary and a year of CSS.

That gap matters because of what a bad install looks like: it looks like a good
one. Applying the recipe our own README prints to the fixture leaves tsc clean,
the bundler happy, zero console errors, zero failed requests — and the page
title at 1.10:1 contrast. The only signal is that the page is wrong, which is
the one signal an agent, CI and a diff review all lack.

- `fixture-app/` — the same ops console the adoption test uses, made runnable:
  a setup run has to be COMPILED AND RENDERED to be scored, so the fixture is a
  real Vite + React + Tailwind v4 build with a middleware data stub. It builds
  before every run, and that pristine build is the reference every number is a
  delta against.
- `conditions.json` — four arms, floor (public docs only) to ceiling (handed the
  exact edits). The system is installed in NO condition; installing it is the task.
- `prompts.json` — three feature-shaped requests. None says "don't break the app":
  a recipe that only works when the user thinks to ask for that is not a recipe.
- `setup-measure.mjs` — build, serve, render, probe, in BOTH color schemes. The
  system paints through light-dark() and the fixture has no color-mode system for
  it to read, so an arm can be correct on a dark-mode laptop and unreadable in
  headless CI; a single-scheme measurement would inherit whoever ran it.
- `setup-eval.ts` — the deterministic scorer: app regressions by category,
  legibility against the AA floor, mode dependence, variable capture. No LLM in
  this path, and the analyzer never sees the condition id.
- `apply-recipe.mjs` — our documented recipes, transcribed, so the mechanism can
  be verified with no agent in the loop and the ceiling arm has something to hand
  an agent verbatim.

No agent has run this yet: no agent findings, no scores, no recommendation. What
the mechanism-verification run does show is that the three causes separate
cleanly — a recipe written for an empty file, a Tailwind bridge that renames 13
tokens the app already uses, and a theme provider that follows the OS — and that
the candidate recipe removes the legibility failure and the mode dependence while
leaving a typography residue the ceiling arm cannot remove either. PLAN.md has
the tables and the pre-registered decision rule.
@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview Aug 21, 2026 12:03am

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Aug 20, 2026
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

🧪 Sandbox Preview

View Sandbox for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

No new or modified components detected.

Bundle Size Summary

No component packages changed.

Accessibility Audit

Status: No accessibility violations detected.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

github-actions Bot added a commit that referenced this pull request Aug 20, 2026
… position is the fourth cause

A claim we had heard and had no mechanism for: you "can't just put the imports
in globals.css". You can — but the layer statement has to go FIRST, and putting
it anywhere else is worse than not following the recipe at all.

`@layer a, b, c;` only orders names that are not already registered, and the
app's own `@import 'tailwindcss'` registers theme/base/utilities on its way past.
A statement below that import can only append the system's layers AFTER Tailwind's
utilities — the exact inverse of what the recipe exists to arrange — so the
system's reset outranks every utility the app already uses and quietly strips
borders, padding and the type scale off every control. Measured on the fixture:
38 changed properties against the documented recipe's 30, with the colour mode
correct and nothing in the console.

Source review cannot catch this, and neither can a class-string lint, because the
`@layer` statement does not survive compilation: Tailwind rewrites it and the
bundle carries only `@layer components;`, so the effective order is the order the
layer BLOCKS first appear in the output. So the measurement reads the emitted CSS.
That is the same guard agentcloud's own e2e spec makes after D114534048, where a
duplicate astryx.css import demoted astryx-base to the bottom of the cascade and
a button rendered square — generalized here into a measure with a verdict
attached.

- `apply-recipe.mjs` gains `appended-at-end`, the naive edit, as a fourth arm.
- `setup-measure.mjs` records the emitted layer order.
- `setup-eval.ts` gains `cascadeInverted`, and an inverted cascade now reads as
  silent-damage rather than drift — it is one edit from correct and damages more
  of the app than the recipe it was trying to follow.
- PLAN.md carries the four causes, both colour schemes, and a new section on
  where each was already found and written down: the production incident behind
  cause 4, the comment refusing the bridge behind cause 2, the `dark:`-follows-
  the-OS sibling of cause 3, and the abandoned installer fix behind cause 1.
  Plus the two open upstream items the test depends on (#5205, #5165).
- The residue every arm leaves now has a name: `--color-accent` and
  `--color-border` collide because `@scope` bounds selector matching, not
  inheritance, and the provider syncs its attribute onto <html>. The guidance
  says to re-assert those on the boundary element; the harness reports the
  collision but does not yet assert the fix, and PLAN says so.

Verification: 24 tests pass (4 new). Four arms measured end to end in both colour
schemes; the tables in PLAN.md are that run.
@nynexman4464

Copy link
Copy Markdown
Contributor Author

Update: a fourth cause, and where all four had already been found

Since the first push I read the full 1:1 transcript this came out of, and mined the diffs and sessions where this problem has already been hit. Two things changed.

The layer statement's POSITION is a cause on its own

The claim I went looking for was "you can't just inject them into globals.css, for whatever reason, it doesn't work there — you need a separate file." You can. It has to go first, and putting it anywhere else is worse than not following the recipe at all.

New arm appended-at-end: same imports, same layer statement, added to the END of the app's existing globals.css — which is what "add these imports to your globals.css" produces when the file already has content. Colour mode correct, bridge not imported, and it is still the most damaging arm measured:

arm builds console app regressions unreadable cascade mode-dependent vars captured verdict
docs-verbatim yes 0 30 (t22 c4 g1 s3) 3 ok 3 5 silent-damage
docs-no-bridge yes 0 11 (t7 c4) 3 ok 3 2 silent-damage
appended-at-end yes 0 38 (t13 c12 g6 s7) 0 INVERTED 0 2 silent-damage
layered-in-place yes 0 11 (t7 c4) 0 ok 0 2 cosmetic-drift

@layer a, b, c; only orders names that are not already registered, and the app's own @import 'tailwindcss' registers theme, base and utilities on its way past. Below that import the statement can only append the system's layers after Tailwind's utilities:

properties > theme > base > utilities > reset > astryx-base > astryx-theme     ← appended
properties > reset > theme > base > astryx-base > astryx-theme > utilities     ← prepended

The system's reset then outranks every utility the app already uses, and border-width: 0 on :where(*) removes every border in the app. Nothing errors.

Source review cannot catch this and neither can a class-string lint, because the @layer statement does not survive compilation — Tailwind rewrites it and the bundle carries only @layer components;, so the effective order is the order the layer blocks first appear in the output. So setup-measure.mjs now records the emitted layer order and setup-eval.ts scores cascadeInverted, which reads as silent-damage rather than drift: it is one edit from correct and it damages more of the app than the recipe it was trying to follow.

Every cause here has already been found once, by someone, somewhere unfindable

That is the argument for the harness, so PLAN.md now says it plainly:

  • Cause 4 was a production incident — D114534048, internal. A duplicate import '@astryxdesign/core/astryx.css' in layout.tsx emitted astryx-base at the top of the bundle and therefore at the bottom of the cascade; a Settings button rendered square because --_button-radius resolved against a token that no longer applied. Its own words: "That statement does not survive compilation … the effective priority order is whatever order the layer blocks first appear in the output, which is decided purely by import order." Its guard — an e2e spec that reads the shipped CSS — is the measure this PR generalizes. "This class of bug is silent: nothing errors, styles just quietly lose."
  • Cause 2 is refused by name in agentcloud's globals.css, in a comment written the same week the bridge was being recommended everywhere else: it "maps the relationship the wrong way round … meant for Astryx-only apps; here navi owns the utility layer."
  • Cause 3 has a sibling in the same app: every dark: utility followed the reader's operating system rather than the theme they had picked, because Tailwind v4 resolves dark: from prefers-color-scheme unless @custom-variant dark is registered. Same shape, same silence.
  • Cause 1 has an abandoned fix: nest add xds first removed Tailwind as a "superseded package", then learned to detect the app's styling system and inject the layered block — but only for an app it is setting up, and only inside its own markers.

The residue now has a name

Every arm, the candidate included, captures --color-accent and --color-border. That is not measurement noise: the system's tokens live in @scope … to ([data-astryx-theme]), which looks contained but is not — @scope bounds selector matching, not inheritance — and the provider deliberately syncs that attribute onto <html> so portals can find it. Tailwind's bg-accent reads var(--color-accent), so an app that named its own accent the obvious thing silently starts painting the system's. The fix that worked elsewhere is to re-assert the app's values on the boundary element rather than in :root; it is in the candidate guidance, and PLAN is explicit that the harness reports the collision without yet asserting the fix.

Two open upstream items this test depends on

24 tests (4 new), still all four detectors proven to go red when disabled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant