diff --git a/e2e/react-router-app/scripts/config.test.ts b/e2e/react-router-app/scripts/config.test.ts index 4d04697f..9519c9b3 100644 --- a/e2e/react-router-app/scripts/config.test.ts +++ b/e2e/react-router-app/scripts/config.test.ts @@ -10,29 +10,16 @@ function source(path: string): string { return readFileSync(absolute, 'utf8'); } +// Fixture self-containment (no cross-fixture imports) is enforced for all +// e2e/* members by the fixture-sibling vector in scripts/verify/topology.ts +// (runs in verify:lint). describe('React Router Worker canary structure', () => { it('delegates the Worker to the generated server build', () => { + // The two anchors carry the invariant: the Worker still exercises React + // Router SSR rather than degenerating into a stub that would build and + // dry-run green. const worker = source('workers/app.ts'); expect(worker).toContain('createRequestHandler'); expect(worker).toContain('virtual:react-router/server-build'); - expect(worker).not.toContain('/api/health'); - expect(worker).not.toContain('new URL'); - expect(worker).toMatch( - /async fetch\(request: Request\): Promise \{\s*return requestHandler\(request\);\s*\}/ - ); - }); - - it('contains no cross-fixture imports', () => { - for (const path of [ - 'app/root.tsx', - 'app/routes.ts', - 'app/routes/home.tsx', - 'app/routes/client.tsx', - 'src/ds.ts', - 'src/components.tsx', - 'workers/app.ts', - ]) { - expect(source(path)).not.toMatch(/e2e\/(next|vite|vinext)-app/); - } }); }); diff --git a/e2e/vinext-app/scripts/config.test.ts b/e2e/vinext-app/scripts/config.test.ts deleted file mode 100644 index 3364f0c2..00000000 --- a/e2e/vinext-app/scripts/config.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { existsSync, readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; -import { describe, expect, it } from 'vitest'; - -const ROOT = resolve(import.meta.dirname, '..'); - -function source(path: string): string { - const absolute = resolve(ROOT, path); - expect(existsSync(absolute), `${path} must exist`).toBe(true); - return readFileSync(absolute, 'utf8'); -} - -describe('Vinext canary structure', () => { - it('contains no cross-fixture imports', () => { - for (const path of [ - 'app/layout.tsx', - 'app/page.tsx', - 'app/client/page.tsx', - 'pages/_app.tsx', - 'pages/legacy.tsx', - 'src/ds.ts', - 'src/components.tsx', - ]) { - expect(source(path)).not.toMatch(/e2e\/(next|vite|react-router)-app/); - } - }); -}); diff --git a/packages/_assertions/__tests__/conditions-inside-layers.test.ts b/packages/_assertions/__tests__/conditions-inside-layers.test.ts index 08c2a089..3823bc51 100644 --- a/packages/_assertions/__tests__/conditions-inside-layers.test.ts +++ b/packages/_assertions/__tests__/conditions-inside-layers.test.ts @@ -61,16 +61,6 @@ describe('assertConditionsInsideLayers (Guardrail G2)', () => { ).not.toThrow(); }); - it('passes even for nested sublayers (@layer composed inside @layer anm-variants)', () => { - // The `@container (min-width: 600px)` lives inside `@layer composed`, itself - // nested in `@layer anm-variants` — still "inside a named @layer block". - expect(() => - assertConditionsInsideLayers(CONDITIONS_IN_LAYERS, { - atRules: ['@container'], - }) - ).not.toThrow(); - }); - it('fails when a @container rule appears outside any @layer block', () => { const hoisted = ` @layer anm-base { .animus-Card { display: flex; } } diff --git a/packages/_assertions/__tests__/property-registration-split.test.ts b/packages/_assertions/__tests__/property-registration-split.test.ts deleted file mode 100644 index 77986e80..00000000 --- a/packages/_assertions/__tests__/property-registration-split.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { - AssertionError, - assertPropertyRegistrationSplit, - type SplitStylesheetParts, -} from '../src/assert-css'; - -// Shapes mirror `assembleStylesheet({ split: true })`: declaration carries the -// @layer ordering line, variables carries the pre-@layer custom-property CSS -// (now including @property registration rules), body carries global+component. -const DECLARATION = '@layer anm-global, anm-base, anm-variants;\n'; -const VARIABLES = [ - '@property --current-bg { syntax: ""; inherits: true; initial-value: transparent; }', - '', - ':root {\n --color-primary: #abc;\n}', -].join('\n'); -const BODY = '@layer anm-base { .animus-card { padding: 8px; } }'; - -function makeParts( - overrides: Partial = {} -): SplitStylesheetParts { - return { - declaration: DECLARATION, - variables: VARIABLES, - body: BODY, - ...overrides, - }; -} - -function rejoin(parts: SplitStylesheetParts): string { - return [parts.declaration, parts.variables, parts.body] - .filter(Boolean) - .join('\n'); -} - -describe('assertPropertyRegistrationSplit', () => { - it('passes when @property is in variables, absent from body, and the concat holds', () => { - const parts = makeParts(); - expect(() => - assertPropertyRegistrationSplit(parts, rejoin(parts)) - ).not.toThrow(); - }); - - it('throws when the variables part has no @property rule', () => { - const parts = makeParts({ - variables: ':root {\n --color-primary: #abc;\n}', - }); - expect(() => assertPropertyRegistrationSplit(parts, rejoin(parts))).toThrow( - AssertionError - ); - }); - - it('throws when @property leaks into the body part', () => { - const parts = makeParts({ - body: `${BODY}\n@property --leak { syntax: "*"; inherits: false; }`, - }); - expect(() => assertPropertyRegistrationSplit(parts, rejoin(parts))).toThrow( - /body part must contain no @property/ - ); - }); - - it('throws when @property leaks into the declaration part', () => { - const parts = makeParts({ - declaration: `${DECLARATION}@property --leak { syntax: "*"; inherits: false; }`, - }); - expect(() => assertPropertyRegistrationSplit(parts, rejoin(parts))).toThrow( - /declaration part must contain no @property/ - ); - }); - - it('throws when the declaration lacks the @layer ordering statement', () => { - const parts = makeParts({ declaration: '/* no layer decl */\n' }); - // A declaration without @layer also contains no @property, so the failure - // is specifically the missing @layer ordering statement. - expect(() => assertPropertyRegistrationSplit(parts, rejoin(parts))).toThrow( - /@layer ordering statement/ - ); - }); - - it('throws when the rejoined parts do not equal the non-split output', () => { - const parts = makeParts(); - expect(() => - assertPropertyRegistrationSplit(parts, `${rejoin(parts)}/* drift */`) - ).toThrow(/do not equal the non-split output/); - }); -}); diff --git a/packages/_assertions/__tests__/receipt.test.ts b/packages/_assertions/__tests__/receipt.test.ts index 9f9820c0..64de35bb 100644 --- a/packages/_assertions/__tests__/receipt.test.ts +++ b/packages/_assertions/__tests__/receipt.test.ts @@ -69,19 +69,6 @@ describe('writeLaneReceipt', () => { engineOverride: false, packageForm: 'workspace', }); - // Explicitly prove every one of the eight fields survived. - for (const key of [ - 'lane', - 'host', - 'hostVersion', - 'mode', - 'engineLoaded', - 'engineDefault', - 'engineOverride', - 'packageForm', - ] as const) { - expect(parsed).toHaveProperty(key); - } }); it('creates missing parent directories and appends a trailing newline', () => { diff --git a/packages/_assertions/src/assert-css.ts b/packages/_assertions/src/assert-css.ts index 33d634b2..f480585d 100644 --- a/packages/_assertions/src/assert-css.ts +++ b/packages/_assertions/src/assert-css.ts @@ -155,83 +155,6 @@ export function assertNoUnresolvedTokens( } } -/** The three structured parts returned by `assembleStylesheet({ split: true })`. */ -export interface SplitStylesheetParts { - declaration: string; - variables: string; - body: string; -} - -/** - * Assert the property-registration split contract (stylesheet-assembly delta, - * "Property registration rules contained in the variables part"): - * - * - `@property` rules live in the `variables` part (at least one present), - * - the `body` part contains none, - * - the `declaration` part is only the `@layer` ordering statement (no - * `@property`), and - * - rejoining the parts reproduces the non-split output — the same - * `[declaration, variables, body].filter(Boolean).join('\n')` that - * `assembleStylesheet` returns without `split`. - * - * Pure over the split parts + the non-split string; no I/O. - */ -export function assertPropertyRegistrationSplit( - parts: SplitStylesheetParts, - nonSplit: string -): void { - const countProperties = (css: string): number => - (css.match(/@property\b/g) ?? []).length; - - const inVariables = countProperties(parts.variables); - const inBody = countProperties(parts.body); - const inDeclaration = countProperties(parts.declaration); - - if (inVariables < 1) { - throw new AssertionError( - 'assertPropertyRegistrationSplit: expected @property rule(s) in the variables part, found none', - { inVariables, inBody, inDeclaration } - ); - } - if (inBody !== 0) { - throw new AssertionError( - `assertPropertyRegistrationSplit: body part must contain no @property rules, found ${inBody}`, - { inBody } - ); - } - if (inDeclaration !== 0) { - throw new AssertionError( - `assertPropertyRegistrationSplit: declaration part must contain no @property rules, found ${inDeclaration}`, - { inDeclaration } - ); - } - if (!LAYER_DECLARATION_RE.test(parts.declaration)) { - throw new AssertionError( - 'assertPropertyRegistrationSplit: declaration part must contain the @layer ordering statement', - { declaration: parts.declaration } - ); - } - // "Only the @layer ordering statement": nothing may remain once the - // ordering statement is removed (spec: declaration part SHALL remain - // only the @layer ordering statement). - if (parts.declaration.replace(LAYER_DECLARATION_RE, '').trim() !== '') { - throw new AssertionError( - 'assertPropertyRegistrationSplit: declaration part must contain ONLY the @layer ordering statement', - { declaration: parts.declaration } - ); - } - - const rejoined = [parts.declaration, parts.variables, parts.body] - .filter(Boolean) - .join('\n'); - if (rejoined !== nonSplit) { - throw new AssertionError( - 'assertPropertyRegistrationSplit: rejoined split parts do not equal the non-split output', - { rejoined, nonSplit } - ); - } -} - /** All `@layer { … }` block spans (single-name block opens, brace- * matched). The layer DECLARATION statement (`@layer a, b, c;`) is not a block * and is excluded. Nested sublayers (`@layer composed { … }`) are included. */ diff --git a/packages/_integration/CLAUDE.md b/packages/_integration/CLAUDE.md index cdef9acb..ca3b31b2 100644 --- a/packages/_integration/CLAUDE.md +++ b/packages/_integration/CLAUDE.md @@ -68,7 +68,7 @@ Covers color tokens only. Scale tokens (font, space) resolve to literals not var | File | What it tests | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `extraction.test.ts` | Variant resolution, compound resolution, transforms, system props, responsive, multi-file | +| `extraction.test.ts` | Variant resolution, compound resolution, transforms, responsive, multi-file (system_prop_map content is pinned by `manifest-shape.test.ts`) | | `serialization.test.ts` | Round-trip: serialize() → analyzeProject() → valid manifest | | `composition.test.ts` | compose() through full pipeline, slot CSS, shared variants | | `post-processing.test.ts` | applyUnitFallback, applyPrefix, assembleStylesheet coverage (v1-era resolveGlobalStyles/resolveTokenAliases/resolveTransformPlaceholders helpers were deleted with retire-extract-v1 cleanup — the Rust v2 engine owns that resolution) | diff --git a/packages/_integration/__tests__/extraction.test.ts b/packages/_integration/__tests__/extraction.test.ts index 7ccf76c8..fdac7fa8 100644 --- a/packages/_integration/__tests__/extraction.test.ts +++ b/packages/_integration/__tests__/extraction.test.ts @@ -135,18 +135,6 @@ describe('transform resolution', () => { }); }); -// ─── System Props ──────────────────────────────────────────── - -describe('system props extraction', () => { - test('produces system_prop_map in manifest', () => { - const entry = readFixtureFile(COMPONENTS, 'system-props.tsx'); - const { manifest } = runPipeline([entry]); - - expect(manifest.system_prop_map).toBeDefined(); - expect(manifest.system_prop_map).toEqual(expect.any(Object)); - }); -}); - // ─── Responsive Extraction ─────────────────────────────────── describe('responsive extraction', () => { diff --git a/packages/_integration/__tests__/manifest-shape.test.ts b/packages/_integration/__tests__/manifest-shape.test.ts index 2fe849f7..8de0d893 100644 --- a/packages/_integration/__tests__/manifest-shape.test.ts +++ b/packages/_integration/__tests__/manifest-shape.test.ts @@ -223,28 +223,20 @@ describe('component descriptor completeness', () => { ); test('manifest.components is a non-empty object', () => { - expect(manifest.components).toEqual(expect.any(Object)); + // Non-empty: the vacuity anchor for the descriptor loop below. expect(Object.keys(manifest.components).length).toBeGreaterThan(0); }); test('every component descriptor has required non-empty fields', () => { + // The manifest decoder already guarantees string types; non-emptiness + // (and the class_name prefix) is the claim here. for (const [id, descriptor] of Object.entries(manifest.components)) { - expect(descriptor.file).toEqual(expect.any(String)); - expect(descriptor.file.length).toBeGreaterThan(0); - expect(descriptor.binding).toEqual(expect.any(String)); - expect(descriptor.binding.length).toBeGreaterThan(0); - expect(descriptor.class_name).toEqual(expect.any(String)); - expect(descriptor.class_name).toMatch(/^animus-/); - expect(descriptor.replacement).toEqual(expect.any(String)); - expect(descriptor.replacement.length).toBeGreaterThan(0); - expect(descriptor.tag).toEqual(expect.any(String)); - expect(descriptor.tag.length).toBeGreaterThan(0); - expect(Object.prototype.toString.call(descriptor.terminal)).toBe( - '[object String]' - ); - expect(descriptor.terminal.length).toBeGreaterThan(0); - // id should be a non-empty string and match the key expect(id.length).toBeGreaterThan(0); + expect(descriptor.class_name).toMatch(/^animus-/); + const emptyFields = ( + ['file', 'binding', 'replacement', 'tag', 'terminal'] as const + ).filter((field) => descriptor[field].length === 0); + expect(emptyFields).toEqual([]); } }); }); @@ -255,9 +247,9 @@ describe('files-to-components consistency', () => { ); test('every component_id in manifest.files exists in manifest.components', () => { - expect(manifest.files).toEqual(expect.any(Object)); + // Non-empty: the vacuity anchor for the loop below. + expect(Object.keys(manifest.files).length).toBeGreaterThan(0); for (const [filePath, componentIds] of Object.entries(manifest.files)) { - expect(Array.isArray(componentIds)).toBe(true); for (const id of componentIds) { expect(manifest.components[id]).toBeDefined(); expect(manifest.components[id].file).toBe(filePath); @@ -273,7 +265,7 @@ describe('provenance reciprocity', () => { test('reverse_provenance is reciprocal with extends_from', () => { const reverse = manifest.reverse_provenance; - expect(reverse).toEqual(expect.any(Object)); + expect(Object.keys(reverse).length).toBeGreaterThan(0); for (const [parentId, childIds] of Object.entries(reverse)) { expect(manifest.components[parentId]).toBeDefined(); for (const childId of childIds) { @@ -359,10 +351,9 @@ describe('system_prop_map validation', () => { ); test('system_prop_map is populated for used props', () => { - expect(manifest.system_prop_map).toEqual(expect.any(Object)); - // system-props.tsx uses p, mt, display, color — at minimum p and mt should appear. + // system-props.tsx uses p, mt, display, color — at minimum p should + // appear. This is the vacuity anchor for the class-name loop below. expect(manifest.system_prop_map.p).toBeDefined(); - expect(manifest.system_prop_map.p).toEqual(expect.any(Object)); }); test('all system_prop_map class name values are animus-u- prefixed', () => { diff --git a/packages/_integration/__tests__/mdx-preprocessing.test.ts b/packages/_integration/__tests__/mdx-preprocessing.test.ts index 1ff8c6ea..a1e3e781 100644 --- a/packages/_integration/__tests__/mdx-preprocessing.test.ts +++ b/packages/_integration/__tests__/mdx-preprocessing.test.ts @@ -1,8 +1,4 @@ -import { - DEFAULT_EXTENSIONS, - preprocessMdx, - type PreprocessMdxResult, -} from '@animus-ui/extract/pipeline'; +import { DEFAULT_EXTENSIONS, preprocessMdx } from '@animus-ui/extract/pipeline'; import { describe, expect, test } from 'vitest'; /** @@ -90,11 +86,6 @@ describe('preprocessMdx — compile failure (kind: "error")', () => { }); describe('preprocessMdx — result shape contract (PreprocessMdxResult)', () => { - test('kind is always one of the documented union members', async () => { - const result: PreprocessMdxResult = await preprocessMdx('# ok\n', 'x.mdx'); - expect(['ok', 'missing-dep', 'error']).toContain(result.kind); - }); - /** * Honestly-unreachable branch — DOCUMENTED GAP. * diff --git a/packages/_integration/__tests__/post-processing.test.ts b/packages/_integration/__tests__/post-processing.test.ts index a1eb2609..ab357ab1 100644 --- a/packages/_integration/__tests__/post-processing.test.ts +++ b/packages/_integration/__tests__/post-processing.test.ts @@ -218,26 +218,8 @@ describe('assembleStylesheet', () => { expect(declarations).toHaveLength(1); }); - test('uses custom layers when provided', () => { - const result = assembleStylesheet({ - layers: [ - 'reset', - 'anm-global', - 'anm-base', - 'anm-variants', - 'anm-compounds', - 'anm-states', - 'anm-system', - 'anm-custom', - 'overrides', - ], - variableCss: ':root { --x: 1; }', - }); - - expect(result).toContain('@layer reset, anm-global, anm-base,'); - expect(result).toContain('overrides;'); - }); - + // Custom-layer declaration content is pinned byte-for-byte by + // packages/extract/tests/canary.test.ts 'custom layers with bookends'. test('throws on invalid layer order', () => { expect(() => assembleStylesheet({ @@ -306,12 +288,7 @@ describe('assembleStylesheet split + post-processing', () => { ); }); - test('split form preserves :root position (not in body)', () => { - const { variables, body } = assembleStylesheet({ - ...opts, - split: true, - }); - expect(variables).toContain(':root'); - expect(body).not.toContain(':root'); - }); + // Split-mode :root placement is pinned (with variable content) by + // packages/extract/tests/canary.test.ts 'variables contains :root block, + // not in body'. }); diff --git a/packages/_integration/__tests__/serialization.test.ts b/packages/_integration/__tests__/serialization.test.ts index 3c86f542..31ecd471 100644 --- a/packages/_integration/__tests__/serialization.test.ts +++ b/packages/_integration/__tests__/serialization.test.ts @@ -17,65 +17,24 @@ beforeAll(() => { clearAnalysisCache(); }); -describe('serialization shape', () => { - test('ds.toConfig() returns propConfig, groupRegistry, transforms', () => { - expect(config.propConfig).toEqual(expect.any(String)); - expect(config.groupRegistry).toEqual(expect.any(String)); - expect(config.transforms).toEqual(expect.any(Object)); - - // propConfig and groupRegistry must be valid JSON +describe('serialize → NAPI round-trip', () => { + test('serialized system + theme output feeds analyzeProject and yields layered CSS', () => { + // The JSON-bearing fields must parse (spec: "valid JSON strings accepted + // by analyzeProject()"); variableCss/contextualVarsJson are plain strings + // with no JSON.parse counterpart. expect(() => JSON.parse(config.propConfig)).not.toThrow(); expect(() => JSON.parse(config.groupRegistry)).not.toThrow(); - }); - - test('ds.toConfig() omits the retired selector order output', () => { - expect(config.selectorAliases).toEqual(expect.any(String)); - expect(config).not.toHaveProperty('selectorOrder'); - }); - - test('tokens.serialize() returns scalesJson, variableMapJson, variableCss, contextualVarsJson', () => { - expect(theme.scalesJson).toEqual(expect.any(String)); - expect(theme.variableMapJson).toEqual(expect.any(String)); - expect(theme.variableCss).toEqual(expect.any(String)); - expect(theme.contextualVarsJson).toEqual(expect.any(String)); - - // JSON fields must be valid JSON expect(() => JSON.parse(theme.scalesJson)).not.toThrow(); expect(() => JSON.parse(theme.variableMapJson)).not.toThrow(); - }); -}); - -describe('serialize → NAPI round-trip', () => { - test('serialized output feeds analyzeProject successfully', () => { - const entry = readFixtureFile(COMPONENTS, 'button.tsx'); - const fileEntries = JSON.stringify([entry]); - - const manifestJson = analyzeProject(fileEntries); - - expect(manifestJson).toEqual(expect.any(String)); - const manifest = JSON.parse(manifestJson); - expect(manifest).toBeDefined(); - expect(manifest.css).toBeDefined(); - }); + expect(theme.variableCss).toEqual(expect.any(String)); + expect(theme.contextualVarsJson).toEqual(expect.any(String)); + expect(config).not.toHaveProperty('selectorOrder'); - test('manifest contains @layer declarations', () => { + // One boundary crossing proves acceptance: non-empty layered CSS plus a + // populated report from the single-file entry. const entry = readFixtureFile(COMPONENTS, 'button.tsx'); - const fileEntries = JSON.stringify([entry]); - - const manifestJson = analyzeProject(fileEntries); - - const manifest = JSON.parse(manifestJson); + const manifest = JSON.parse(analyzeProject(JSON.stringify([entry]))); expect(manifest.css).toContain('@layer'); - }); - - test('manifest contains component extraction data', () => { - const entry = readFixtureFile(COMPONENTS, 'button.tsx'); - const fileEntries = JSON.stringify([entry]); - - const manifestJson = analyzeProject(fileEntries); - - const manifest = JSON.parse(manifestJson); - expect(manifest.report).toBeDefined(); expect(manifest.report.components_extracted).toBeGreaterThan(0); }); }); diff --git a/packages/_integration/__tests__/svelte-usage-extraction.test.ts b/packages/_integration/__tests__/svelte-usage-extraction.test.ts index 768e4f83..1b47b229 100644 --- a/packages/_integration/__tests__/svelte-usage-extraction.test.ts +++ b/packages/_integration/__tests__/svelte-usage-extraction.test.ts @@ -135,10 +135,6 @@ describe('isolated native Svelte usage projection', () => { { extractFacts } ); expect(renamedIngested.diagnostics).toEqual([]); - expect( - renamedIngested.ownership['components/svelte-usage/Renamed.svelte'] - .analysisPaths - ).toEqual(['components/svelte-usage/Renamed.svelte.instance.tsx']); const renamed = runPipeline(renamedIngested.analysisEntries); expect(renamed.css).toContain('--tone-quiet'); expect(renamed.css).not.toContain('--tone-loud'); diff --git a/packages/_integration/fixtures/components/extended.tsx b/packages/_integration/fixtures/components/extended.tsx new file mode 100644 index 00000000..132ed7d7 --- /dev/null +++ b/packages/_integration/fixtures/components/extended.tsx @@ -0,0 +1,14 @@ +import { Button } from './button'; + +// Cross-file extension chain: the one fixture that populates +// `extends_from` / `reverse_provenance`, so the provenance-reciprocity +// tests in manifest-shape.test.ts iterate a non-empty collection. +export const OutlineButton = Button.extend() + .styles({ + border: '1px solid', + borderColor: 'primary', + bg: 'transparent', + }) + .asElement('button'); + +export const App = () => ; diff --git a/packages/_parity/baseline-intents.md b/packages/_parity/baseline-intents.md index 41f2b669..51a4f746 100644 --- a/packages/_parity/baseline-intents.md +++ b/packages/_parity/baseline-intents.md @@ -202,3 +202,13 @@ committed production/development pair. Ordinary parity runs never write it. adaptation belongs to the TypeScript ingestion pipeline and is proven by the dedicated real-engine integration tests. Every pre-existing parity unit stays byte-identical in the same run. +- [x] `test-value-audit-extension-fixture-20260818` — refresh after the + test-value audit added `fixtures/components/extended.tsx` (a cross-file + `Button.extend()` chain) so `manifest-shape.test.ts`'s provenance- + reciprocity tests iterate a non-empty `reverse_provenance` (they were + vacuous: no prior integration fixture used `.extend()`). The fixture + enters the automatically discovered parity inventory as a NEW unit only + (`integration/extended.tsx` · css/code/observables/diagnostics, both + modes); every pre-existing unit stays byte-identical in the same run + (65/66 with only the new unit's four unregistered surfaces, corpus + digest moves accordingly). diff --git a/packages/_parity/baselines/v2/development.json b/packages/_parity/baselines/v2/development.json index 2bf52093..d63d6539 100644 --- a/packages/_parity/baselines/v2/development.json +++ b/packages/_parity/baselines/v2/development.json @@ -1,8 +1,8 @@ { - "corpusSha256": "d0c4f19d386944d3d3a6776c64e8ae08aee8ac11a8362ff02c47660952325ce4", + "corpusSha256": "0bdc9be48cb7a7db59cc1fcf5dbca46232d3d5f472b6de47778d99fe3eb35503", "engine": "v2", "mode": "development", - "refreshIntent": "svelte-parity-corpus-enumeration-20260810", + "refreshIntent": "test-value-audit-extension-fixture-20260818", "surfaceSchemaSha256": "43eb1265e97aab8497c11ca05e8a4159a6d4f4b69a6837a704e1d96fa75eb108", "units": { "extract-all": { @@ -550,6 +550,27 @@ }, "parseCount": 1 }, + "integration/extended.tsx": { + "code": { + "extended.tsx": "import { Button } from './button';\n\n// Cross-file extension chain: the one fixture that populates\n// `extends_from` / `reverse_provenance`, so the provenance-reciprocity\n// tests in manifest-shape.test.ts iterate a non-empty collection.\nexport const OutlineButton = Button.extend()\n .styles({\n border: '1px solid',\n borderColor: 'primary',\n bg: 'transparent',\n })\n .asElement('button');\n\nexport const App = () => ;\n" + }, + "css": "@layer anm-global, anm-base, anm-variants, anm-compounds, anm-states, anm-system, anm-custom;\n\n@layer anm-variants {\n @layer standalone, composed;\n @layer composed {\n }\n}\n\n", + "diagnostics": [ + "extended.tsx|bail|OutlineButton|chain dropped: could not resolve parent component 'Button'" + ], + "hasComponents": { + "extended.tsx": false + }, + "observables": { + "componentFragmentKeys": [], + "componentFragmentsJson": "{}", + "dynamicPropsJson": "{}", + "reverseProvenanceEdges": [], + "sheetsJson": "{\"base\":\"\",\"compounds\":\"\",\"custom\":\"\",\"declaration\":\"@layer anm-global, anm-base, anm-variants, anm-compounds, anm-states, anm-system, anm-custom;\\n\",\"global\":\"@layer anm-global {\\nbody {\\n font-family: {fonts.base};\\n margin: 0;\\n}\\n@keyframes anm-ember {\\n 0% {\\n opacity: 0;\\n }\\n 100% {\\n opacity: 1;\\n }\\n}\\n}\\n\",\"states\":\"\",\"system\":\"\",\"variants\":\"@layer anm-variants {\\n @layer standalone, composed;\\n @layer composed {\\n }\\n}\\n\"}", + "systemPropMapJson": "{}" + }, + "parseCount": 1 + }, "integration/layout.tsx": { "code": { "layout.tsx": "import { createComponent } from '@animus-ui/system';\nimport 'virtual:animus/styles.css';\nimport { ds } from '../setup';\n\nexport const Container = createComponent('div', 'animus-Container-4c059a41', {});\n\nexport const Stack = createComponent('div', 'animus-Stack-ae863a92', {});\n\nexport const App = () => (\n \n \n \n);\n\n" diff --git a/packages/_parity/baselines/v2/production.json b/packages/_parity/baselines/v2/production.json index 47ab3678..17ec7fc8 100644 --- a/packages/_parity/baselines/v2/production.json +++ b/packages/_parity/baselines/v2/production.json @@ -1,8 +1,8 @@ { - "corpusSha256": "d0c4f19d386944d3d3a6776c64e8ae08aee8ac11a8362ff02c47660952325ce4", + "corpusSha256": "0bdc9be48cb7a7db59cc1fcf5dbca46232d3d5f472b6de47778d99fe3eb35503", "engine": "v2", "mode": "production", - "refreshIntent": "svelte-parity-corpus-enumeration-20260810", + "refreshIntent": "test-value-audit-extension-fixture-20260818", "surfaceSchemaSha256": "43eb1265e97aab8497c11ca05e8a4159a6d4f4b69a6837a704e1d96fa75eb108", "units": { "extract-all": { @@ -513,6 +513,27 @@ }, "parseCount": 1 }, + "integration/extended.tsx": { + "code": { + "extended.tsx": "import { Button } from './button';\n\n// Cross-file extension chain: the one fixture that populates\n// `extends_from` / `reverse_provenance`, so the provenance-reciprocity\n// tests in manifest-shape.test.ts iterate a non-empty collection.\nexport const OutlineButton = Button.extend()\n .styles({\n border: '1px solid',\n borderColor: 'primary',\n bg: 'transparent',\n })\n .asElement('button');\n\nexport const App = () => ;\n" + }, + "css": "@layer anm-global, anm-base, anm-variants, anm-compounds, anm-states, anm-system, anm-custom;\n\n@layer anm-variants {\n @layer standalone, composed;\n @layer composed {\n }\n}\n\n", + "diagnostics": [ + "extended.tsx|bail|OutlineButton|chain dropped: could not resolve parent component 'Button'" + ], + "hasComponents": { + "extended.tsx": false + }, + "observables": { + "componentFragmentKeys": [], + "componentFragmentsJson": "{}", + "dynamicPropsJson": "{}", + "reverseProvenanceEdges": [], + "sheetsJson": "{\"base\":\"\",\"compounds\":\"\",\"custom\":\"\",\"declaration\":\"@layer anm-global, anm-base, anm-variants, anm-compounds, anm-states, anm-system, anm-custom;\\n\",\"global\":\"@layer anm-global {\\nbody {\\n font-family: {fonts.base};\\n margin: 0;\\n}\\n@keyframes anm-ember {\\n 0% {\\n opacity: 0;\\n }\\n 100% {\\n opacity: 1;\\n }\\n}\\n}\\n\",\"states\":\"\",\"system\":\"\",\"variants\":\"@layer anm-variants {\\n @layer standalone, composed;\\n @layer composed {\\n }\\n}\\n\"}", + "systemPropMapJson": "{}" + }, + "parseCount": 1 + }, "integration/layout.tsx": { "code": { "layout.tsx": "import { createComponent } from '@animus-ui/system';\nimport 'virtual:animus/styles.css';\nimport { ds } from '../setup';\n\nexport const Container = createComponent('div', 'animus-Container-4c059a41', {});\n\nexport const Stack = createComponent('div', 'animus-Stack-ae863a92', {});\n\nexport const App = () => (\n \n \n \n);\n\n" diff --git a/packages/_parity/last-failure.txt b/packages/_parity/last-failure.txt index 90582b62..3adfe288 100644 --- a/packages/_parity/last-failure.txt +++ b/packages/_parity/last-failure.txt @@ -1,20 +1,13 @@ parity baseline — engines: baseline:v2 vs v2 — devMode: false -Units passed: 64/66 (96.97%) -Divergences: 11 (11 unregistered) +Units passed: 65/66 (98.48%) +Divergences: 4 (4 unregistered) Failing units (sorted): - integration/svelte-lifecycle · css (UNREGISTERED) [dcf25e1b438599b78769ba1e8ccad9cf2d17171f2b7897d34836046533edb4d6 -> 7dc386e22fccb03994ed06cb561164205272a488be8d121895fb86de2c654f39] — unit missing from candidate - integration/svelte-lifecycle · code (UNREGISTERED) [2e10e0eb3656faec8e2dd16c0337a6c27e8df677e1ffe65a8d4011604cc95321 -> 7dc386e22fccb03994ed06cb561164205272a488be8d121895fb86de2c654f39] — unit missing from candidate - integration/svelte-lifecycle · observables (UNREGISTERED) [9f8a577ab84e38ad065dfd6e859cdb25ec47cc86123e57eef637f6e49aedd80b -> 7dc386e22fccb03994ed06cb561164205272a488be8d121895fb86de2c654f39] — unit missing from candidate - integration/svelte-lifecycle · diagnostics (UNREGISTERED) [4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945 -> 7dc386e22fccb03994ed06cb561164205272a488be8d121895fb86de2c654f39] — unit missing from candidate - integration/svelte-usage · css [selector] (UNREGISTERED) [dcf25e1b438599b78769ba1e8ccad9cf2d17171f2b7897d34836046533edb4d6 -> 8ff12cf3d90e07e107e35e7a1a6bea98fe94b8ebda78b32afa6e84801caedd19] — CSS bytes differ (175 vs 585) - integration/svelte-usage · code (UNREGISTERED) [2e10e0eb3656faec8e2dd16c0337a6c27e8df677e1ffe65a8d4011604cc95321 -> addc9660f3053f5dcbfe8d7c12a1581d4b61be23a5a3b0233419ef6ef905e435] — definition.ts: present in one engine only - integration/svelte-usage · code (UNREGISTERED) [2e10e0eb3656faec8e2dd16c0337a6c27e8df677e1ffe65a8d4011604cc95321 -> addc9660f3053f5dcbfe8d7c12a1581d4b61be23a5a3b0233419ef6ef905e435] — definition.ts: hasComponents differs - integration/svelte-usage · observables (UNREGISTERED) [9f8a577ab84e38ad065dfd6e859cdb25ec47cc86123e57eef637f6e49aedd80b -> db46345b24e32f3b1513228e72b7baf91c30e22f57ee1c1c488f3ac18b48f201] — parseCount differs - integration/svelte-usage · observables (UNREGISTERED) [9f8a577ab84e38ad065dfd6e859cdb25ec47cc86123e57eef637f6e49aedd80b -> db46345b24e32f3b1513228e72b7baf91c30e22f57ee1c1c488f3ac18b48f201] — componentFragmentKeys differs - integration/svelte-usage · observables (UNREGISTERED) [9f8a577ab84e38ad065dfd6e859cdb25ec47cc86123e57eef637f6e49aedd80b -> db46345b24e32f3b1513228e72b7baf91c30e22f57ee1c1c488f3ac18b48f201] — sheetsJson differs - integration/svelte-usage · observables (UNREGISTERED) [9f8a577ab84e38ad065dfd6e859cdb25ec47cc86123e57eef637f6e49aedd80b -> db46345b24e32f3b1513228e72b7baf91c30e22f57ee1c1c488f3ac18b48f201] — componentFragmentsJson differs + integration/extended.tsx · css (UNREGISTERED) [7dc386e22fccb03994ed06cb561164205272a488be8d121895fb86de2c654f39 -> dcf25e1b438599b78769ba1e8ccad9cf2d17171f2b7897d34836046533edb4d6] — unit missing from baseline + integration/extended.tsx · code (UNREGISTERED) [7dc386e22fccb03994ed06cb561164205272a488be8d121895fb86de2c654f39 -> ced3d69b657daf5ceafe7246dff836c080aa32d491f5afd3711f3f057753ee42] — unit missing from baseline + integration/extended.tsx · observables (UNREGISTERED) [7dc386e22fccb03994ed06cb561164205272a488be8d121895fb86de2c654f39 -> 42d5fb8fab40ec14100d4eb5a596e43bc6d0f4df6cec4d7c2d4ac2fb129119a1] — unit missing from baseline + integration/extended.tsx · diagnostics (UNREGISTERED) [7dc386e22fccb03994ed06cb561164205272a488be8d121895fb86de2c654f39 -> 97ca2717c37e577206a357c652d5f190ec4d14afb5d403678650fcf330a4b6ae] — unit missing from baseline Usage-case families: ok mdx-provider-scope — expected identical, observed identical @@ -38,21 +31,14 @@ Baseline metadata errors: parity baseline — engines: baseline:v2 vs v2 — devMode: true -Units passed: 64/66 (96.97%) -Divergences: 11 (11 unregistered) +Units passed: 65/66 (98.48%) +Divergences: 4 (4 unregistered) Failing units (sorted): - integration/svelte-lifecycle · css (UNREGISTERED) [dcf25e1b438599b78769ba1e8ccad9cf2d17171f2b7897d34836046533edb4d6 -> 7dc386e22fccb03994ed06cb561164205272a488be8d121895fb86de2c654f39] — unit missing from candidate - integration/svelte-lifecycle · code (UNREGISTERED) [2e10e0eb3656faec8e2dd16c0337a6c27e8df677e1ffe65a8d4011604cc95321 -> 7dc386e22fccb03994ed06cb561164205272a488be8d121895fb86de2c654f39] — unit missing from candidate - integration/svelte-lifecycle · observables (UNREGISTERED) [9f8a577ab84e38ad065dfd6e859cdb25ec47cc86123e57eef637f6e49aedd80b -> 7dc386e22fccb03994ed06cb561164205272a488be8d121895fb86de2c654f39] — unit missing from candidate - integration/svelte-lifecycle · diagnostics (UNREGISTERED) [4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945 -> 7dc386e22fccb03994ed06cb561164205272a488be8d121895fb86de2c654f39] — unit missing from candidate - integration/svelte-usage · css [selector] (UNREGISTERED) [dcf25e1b438599b78769ba1e8ccad9cf2d17171f2b7897d34836046533edb4d6 -> 8ff12cf3d90e07e107e35e7a1a6bea98fe94b8ebda78b32afa6e84801caedd19] — CSS bytes differ (175 vs 585) - integration/svelte-usage · code (UNREGISTERED) [2e10e0eb3656faec8e2dd16c0337a6c27e8df677e1ffe65a8d4011604cc95321 -> addc9660f3053f5dcbfe8d7c12a1581d4b61be23a5a3b0233419ef6ef905e435] — definition.ts: present in one engine only - integration/svelte-usage · code (UNREGISTERED) [2e10e0eb3656faec8e2dd16c0337a6c27e8df677e1ffe65a8d4011604cc95321 -> addc9660f3053f5dcbfe8d7c12a1581d4b61be23a5a3b0233419ef6ef905e435] — definition.ts: hasComponents differs - integration/svelte-usage · observables (UNREGISTERED) [9f8a577ab84e38ad065dfd6e859cdb25ec47cc86123e57eef637f6e49aedd80b -> db46345b24e32f3b1513228e72b7baf91c30e22f57ee1c1c488f3ac18b48f201] — parseCount differs - integration/svelte-usage · observables (UNREGISTERED) [9f8a577ab84e38ad065dfd6e859cdb25ec47cc86123e57eef637f6e49aedd80b -> db46345b24e32f3b1513228e72b7baf91c30e22f57ee1c1c488f3ac18b48f201] — componentFragmentKeys differs - integration/svelte-usage · observables (UNREGISTERED) [9f8a577ab84e38ad065dfd6e859cdb25ec47cc86123e57eef637f6e49aedd80b -> db46345b24e32f3b1513228e72b7baf91c30e22f57ee1c1c488f3ac18b48f201] — sheetsJson differs - integration/svelte-usage · observables (UNREGISTERED) [9f8a577ab84e38ad065dfd6e859cdb25ec47cc86123e57eef637f6e49aedd80b -> db46345b24e32f3b1513228e72b7baf91c30e22f57ee1c1c488f3ac18b48f201] — componentFragmentsJson differs + integration/extended.tsx · css (UNREGISTERED) [7dc386e22fccb03994ed06cb561164205272a488be8d121895fb86de2c654f39 -> dcf25e1b438599b78769ba1e8ccad9cf2d17171f2b7897d34836046533edb4d6] — unit missing from baseline + integration/extended.tsx · code (UNREGISTERED) [7dc386e22fccb03994ed06cb561164205272a488be8d121895fb86de2c654f39 -> ced3d69b657daf5ceafe7246dff836c080aa32d491f5afd3711f3f057753ee42] — unit missing from baseline + integration/extended.tsx · observables (UNREGISTERED) [7dc386e22fccb03994ed06cb561164205272a488be8d121895fb86de2c654f39 -> 42d5fb8fab40ec14100d4eb5a596e43bc6d0f4df6cec4d7c2d4ac2fb129119a1] — unit missing from baseline + integration/extended.tsx · diagnostics (UNREGISTERED) [7dc386e22fccb03994ed06cb561164205272a488be8d121895fb86de2c654f39 -> 97ca2717c37e577206a357c652d5f190ec4d14afb5d403678650fcf330a4b6ae] — unit missing from baseline Usage-case families: ok mdx-provider-scope — expected identical, observed identical diff --git a/packages/_parity/scoreboard.snap b/packages/_parity/scoreboard.snap index 651adc44..43ec3d2a 100644 --- a/packages/_parity/scoreboard.snap +++ b/packages/_parity/scoreboard.snap @@ -1,6 +1,6 @@ parity baseline — engines: baseline:v2 vs v2 — devMode: false -Units passed: 65/65 (100.00%) +Units passed: 66/66 (100.00%) Divergences: 0 (0 unregistered) Usage-case families: @@ -23,7 +23,7 @@ Usage-case families: parity baseline — engines: baseline:v2 vs v2 — devMode: true -Units passed: 65/65 (100.00%) +Units passed: 66/66 (100.00%) Divergences: 0 (0 unregistered) Usage-case families: diff --git a/packages/_parity/self-check.snap b/packages/_parity/self-check.snap index 60ed9bd1..11f02c70 100644 --- a/packages/_parity/self-check.snap +++ b/packages/_parity/self-check.snap @@ -1,6 +1,6 @@ parity self-check — engines: v2 vs v2 — devMode: false -Units passed: 65/65 (100.00%) +Units passed: 66/66 (100.00%) Divergences: 0 (0 unregistered) Usage-case families: @@ -23,7 +23,7 @@ Usage-case families: parity self-check — engines: v2 vs v2 — devMode: true -Units passed: 65/65 (100.00%) +Units passed: 66/66 (100.00%) Divergences: 0 (0 unregistered) Usage-case families: diff --git a/packages/extract/tests/canary.test.ts b/packages/extract/tests/canary.test.ts index d0f1affb..d3de35a2 100644 --- a/packages/extract/tests/canary.test.ts +++ b/packages/extract/tests/canary.test.ts @@ -22,16 +22,23 @@ describe('v2 system loader NAPI boundary', () => { const config = v2.loadSystemModule(systemPath, root); // Required string fields (NAPI snake_case → camelCase auto-conversion). - for (const [field, value] of Object.entries({ + // Explicit picks: NAPI class instances expose fields as getters, not own + // enumerable properties, so toMatchObject(config) cannot see them. + expect({ propConfig: config.propConfig, groupRegistry: config.groupRegistry, scalesJson: config.scalesJson, variableMapJson: config.variableMapJson, variableCss: config.variableCss, contextualVarsJson: config.contextualVarsJson, - })) { - expect(value, field).toEqual(expect.any(String)); - } + }).toEqual({ + propConfig: expect.any(String), + groupRegistry: expect.any(String), + scalesJson: expect.any(String), + variableMapJson: expect.any(String), + variableCss: expect.any(String), + contextualVarsJson: expect.any(String), + }); // The JSON-bearing fields must parse. expect(() => JSON.parse(config.propConfig)).not.toThrow(); @@ -118,13 +125,6 @@ describe('assembleStylesheet: split mode', () => { '@layer anm-global, anm-base;\n@layer anm-base { .btn { padding: 8px; } }', }; - test('split: true returns object with declaration, variables, body', () => { - const result = assemble({ ...opts, split: true }); - expect(result).toHaveProperty('declaration'); - expect(result).toHaveProperty('variables'); - expect(result).toHaveProperty('body'); - }); - test('declaration contains @layer statement, not in body', () => { const { declaration, body } = assemble({ ...opts, split: true }); expect(declaration).toContain('@layer anm-global, anm-base'); @@ -157,3 +157,71 @@ describe('assembleStylesheet: split mode', () => { expect(joined).toEqual(stringResult); }); }); + +// assembleStylesheet: @property registration split contract (rehomed from +// packages/vite-plugin/tests/property-registration-split.test.ts — the suite +// exercises the shared pipeline export, not any Vite seam, so the extract +// owner is its home). typed-property-registration: "@property rules SHALL +// appear in the variables part of the assembled stylesheet, before any +// @layer block." +describe('assembleStylesheet: @property registration split', () => { + const { assembleStylesheet: assemble } = require('../dist/index.mjs'); + + // Exactly the shape createTheme's serialize().variableCss produces for a + // registered contextual var (see packages/system/__tests__/theme.test.ts). + const VARIABLE_CSS = [ + '@property --current-bg { syntax: ""; inherits: true; initial-value: transparent; }', + '', + ':root {\n --color-primary: #abc;\n}', + ].join('\n'); + + const COMPONENT_CSS = '@layer anm-base { .animus-card { padding: 8px; } }'; + + test('places @property in the variables part, absent from body/declaration', () => { + const { declaration, variables, body } = assemble({ + variableCss: VARIABLE_CSS, + componentCss: COMPONENT_CSS, + split: true, + }); + + expect(variables).toContain('@property --current-bg'); + expect(body).not.toContain('@property'); + expect(declaration).not.toContain('@property'); + // declaration remains only the @layer ordering statement. + expect(declaration).toMatch(/@layer\s+[\w-]+(\s*,\s*[\w-]+)*\s*;/); + }); + + test('concatenation invariant: rejoined split equals the non-split output', () => { + const split = assemble({ + variableCss: VARIABLE_CSS, + componentCss: COMPONENT_CSS, + split: true, + }); + const nonSplit = assemble({ + variableCss: VARIABLE_CSS, + componentCss: COMPONENT_CSS, + }); + + const rejoined = [split.declaration, split.variables, split.body] + .filter(Boolean) + .join('\n'); + expect(rejoined).toBe(nonSplit); + }); + + test('@property appears before the @layer declaration in assembled output', () => { + const nonSplit = assemble({ + variableCss: VARIABLE_CSS, + componentCss: COMPONENT_CSS, + }); + + const propIdx = nonSplit.indexOf('@property --current-bg'); + const layerBaseIdx = nonSplit.indexOf('@layer anm-base {'); + const declIdx = nonSplit.search(/@layer\s+[\w-]+(\s*,\s*[\w-]+)*\s*;/); + + expect(propIdx).toBeGreaterThanOrEqual(0); + // @property sits in the variables part, after the ordering declaration + // line but before any component @layer block. + expect(propIdx).toBeGreaterThan(declIdx); + expect(layerBaseIdx).toBeGreaterThan(propIdx); + }); +}); diff --git a/packages/extract/tests/collect-external-packages.test.ts b/packages/extract/tests/collect-external-packages.test.ts index d148309b..0d3e16ba 100644 --- a/packages/extract/tests/collect-external-packages.test.ts +++ b/packages/extract/tests/collect-external-packages.test.ts @@ -98,28 +98,6 @@ describe('collectExternalPackageSources', () => { expect(result.sourceEntries.size).toBe(1); }); - test('redirects a package export subpath to its matching source module', async () => { - const root = makeRoot(); - const pkg = makePackage(join(root, 'packages', 'ds'), { - 'src/index.ts': 'export const root = 1;', - 'src/definition.ts': 'export const system = 1;', - 'dist/definition.mjs': 'export const system = 1;', - }); - - const result = await collect(root, { - '@x/ds/definition': join(pkg, 'dist', 'definition.mjs'), - }); - - expect(result.packageMap).toEqual({ - '@x/ds/definition': 'packages/ds/src/definition.ts', - // Derived root alias — see the dedicated subpath/root-alias tests. - '@x/ds': 'packages/ds/src/index.ts', - }); - expect(result.sourceEntries.get('@x/ds/definition')).toBe( - join(pkg, 'src', 'definition.ts') - ); - }); - test('a subpath specifier also registers its package root for app-side imports', async () => { const root = makeRoot(); const pkg = makePackage(join(root, 'packages', 'ds'), { @@ -142,7 +120,14 @@ describe('collectExternalPackageSources', () => { expect(result.sourceEntries.get('@x/ds')).toBe( join(pkg, 'src', 'index.ts') ); - // The alias is derived, not declared: exactly one outcome record. + // The declared subpath key redirects to its own source module, never to + // the package root's src/index.ts. + expect(result.sourceEntries.get('@x/ds/definition')).toBe( + join(pkg, 'src', 'definition.ts') + ); + // The alias is derived, not declared: exactly one outcome record — and it + // carries the discovered file count (includes-driven-discovery + // §Resolved specifier records its file count). expect(result.outcomes).toEqual([ { specifier: '@x/ds/definition', outcome: 'resolved', fileCount: 2 }, ]); @@ -366,22 +351,6 @@ describe('collectExternalPackageSources', () => { ]); }); - test('records a resolved outcome carrying the discovered file count', async () => { - const root = makeRoot(); - const pkg = makePackage(join(root, 'packages', 'ds'), { - 'src/index.ts': 'export * from "./Button";', - 'src/Button.tsx': 'export const Button = 1;', - }); - - const result = await collect(root, { - '@x/ds': join(pkg, 'dist', 'index.mjs'), - }); - - expect(result.outcomes).toEqual([ - { specifier: '@x/ds', outcome: 'resolved', fileCount: 2 }, - ]); - }); - test('records an unresolvable outcome per specifier, in declaration order', async () => { const root = makeRoot(); const pkg = makePackage(join(root, 'packages', 'ds'), { diff --git a/packages/extract/tests/discover-packages.test.ts b/packages/extract/tests/discover-packages.test.ts index b057b407..048d0ab1 100644 --- a/packages/extract/tests/discover-packages.test.ts +++ b/packages/extract/tests/discover-packages.test.ts @@ -254,28 +254,6 @@ describe('extractSystemFilePackages', () => { } }); - test('from() and legacy includes forms contribute to one discovered set', () => { - const path = writeFixture(` - import { createSystem } from '@animus-ui/system'; - import { ds as legacyDs } from '@animus-ui/test-ds'; - import { ds as kitDs } from '@acme/ui-kit'; - - export const { system: ds } = createSystem({ includes: [legacyDs] }) - .from(kitDs) - .addGroup('space', {}) - .build(); - `); - - try { - const pkgs = extractSystemFilePackages(path); - expect(pkgs).toContain('@animus-ui/test-ds'); - expect(pkgs).toContain('@acme/ui-kit'); - } finally { - rmSync(path, { force: true }); - rmSync(join(path, '..'), { recursive: true, force: true }); - } - }); - test('createTheme().from() never contributes discovery membership', () => { const path = writeFixture(` import { createSystem, createTheme } from '@animus-ui/system'; @@ -303,28 +281,6 @@ describe('extractSystemFilePackages', () => { } }); - test('from() sources survive a reformatted chain', () => { - const path = writeFixture(` - import { createSystem } from '@animus-ui/system'; - import { ds as kitDs } from '@acme/ui-kit'; - - export const { system: ds } = createSystem() - .from( - kitDs - ) - .addGroup('space', {}) - .build(); - `); - - try { - const pkgs = extractSystemFilePackages(path); - expect(pkgs).toContain('@acme/ui-kit'); - } finally { - rmSync(path, { force: true }); - rmSync(join(path, '..'), { recursive: true, force: true }); - } - }); - test('discovers package from an extend() chain call', () => { const path = writeFixture(` import { createSystem } from '@animus-ui/system'; @@ -346,29 +302,6 @@ describe('extractSystemFilePackages', () => { } }); - test('discovers every source of repeated extend() calls', () => { - const path = writeFixture(` - import { createSystem } from '@animus-ui/system'; - import { ds as a } from '@ds-a/core'; - import { ds as b } from '@ds-b/core'; - - export const { system: ds } = createSystem() - .extend(a) - .extend(b) - .addGroup('space', {}) - .build(); - `); - - try { - const pkgs = extractSystemFilePackages(path); - expect(pkgs).toContain('@ds-a/core'); - expect(pkgs).toContain('@ds-b/core'); - } finally { - rmSync(path, { force: true }); - rmSync(join(path, '..'), { recursive: true, force: true }); - } - }); - test('discovers every source of a mixed extend()/from() chain', () => { const path = writeFixture(` import { createSystem } from '@animus-ui/system'; @@ -452,6 +385,10 @@ describe('extractSystemFilePackages', () => { }); test('extend() sources survive a reformatted chain', () => { + // Trivia sits between the argument and the closing paren — the one + // position the tolerance block's trailing-comma fixture never reaches + // (there the comma follows the identifier directly). Both spellings run + // the same link scan, so extend() carries this shape for from() too. const path = writeFixture(` import { createSystem } from '@animus-ui/system'; import { ds as kitDs } from '@acme/ui-kit'; diff --git a/packages/extract/tests/dynamic-prop-config.test.ts b/packages/extract/tests/dynamic-prop-config.test.ts index ebc740b8..5b015026 100644 --- a/packages/extract/tests/dynamic-prop-config.test.ts +++ b/packages/extract/tests/dynamic-prop-config.test.ts @@ -11,62 +11,6 @@ import type { DynamicPropMeta } from '../pipeline/dynamic-prop-config'; * the builder reads. */ describe('buildDynamicPropConfig', () => { - test('carries the CSS property from a camelCase manifest meta', () => { - expect( - buildDynamicPropConfig({ - lineHeight: { - varName: '--animus-line-height', - slotClass: 'animus-dyn-line-height', - property: 'lineHeight', - }, - }) - ).toEqual({ - lineHeight: { - varName: '--animus-line-height', - slotClass: 'animus-dyn-line-height', - property: 'lineHeight', - }, - }); - }); - - test('carries member properties for a multi-property prop', () => { - expect( - buildDynamicPropConfig({ - mx: { - varName: '--animus-mx', - slotClass: 'animus-dyn-mx', - property: 'margin', - properties: ['marginLeft', 'marginRight'], - }, - }).mx - ).toEqual({ - varName: '--animus-mx', - slotClass: 'animus-dyn-mx', - property: 'margin', - properties: ['marginLeft', 'marginRight'], - }); - }); - - test('emits transform name and scale values when present', () => { - expect( - buildDynamicPropConfig({ - color: { - varName: '--animus-color', - slotClass: 'animus-dyn-color', - property: 'color', - transformName: 'toColor', - scaleValues: { primary: '#00f' }, - }, - }).color - ).toEqual({ - varName: '--animus-color', - slotClass: 'animus-dyn-color', - property: 'color', - transformName: 'toColor', - scaleValues: { primary: '#00f' }, - }); - }); - test('omits absent property, empty properties, null transform, empty scales', () => { expect( JSON.stringify( diff --git a/packages/extract/tests/error-diagnostics.test.ts b/packages/extract/tests/error-diagnostics.test.ts index 9f8e2fa0..cd39e06b 100644 --- a/packages/extract/tests/error-diagnostics.test.ts +++ b/packages/extract/tests/error-diagnostics.test.ts @@ -82,17 +82,6 @@ describe('assertNoErrorDiagnostics', () => { ).not.toThrow(); }); - it('throws on one error naming component, file, and message', () => { - const thrown = thrownFrom(() => - assertNoErrorDiagnostics([objectResultError]) - ); - expect(thrown).not.toBeNull(); - expect(thrown!.message).toContain('[animus]'); - expect(thrown!.message).toContain('Broken'); - expect(thrown!.message).toContain('src/invalid.tsx'); - expect(thrown!.message).toContain(objectResultError.message); - }); - it('lists every error entry, one [animus]-prefixed line each', () => { const second: CssDiagnosticLike = { file: 'src/other.tsx', diff --git a/packages/extract/tests/post-process-css.test.ts b/packages/extract/tests/post-process-css.test.ts index 31a8dc31..d01c85d8 100644 --- a/packages/extract/tests/post-process-css.test.ts +++ b/packages/extract/tests/post-process-css.test.ts @@ -22,7 +22,9 @@ describe('postProcessCss', () => { const out = postProcessCss(css, { minify: true, targets: SAFARI15 }); expect(out).toContain('-webkit-backdrop-filter:blur(8px)'); - expect(out).not.toContain('\n '); + // Minification removes unnecessary newlines and indentation entirely. + expect(out).not.toContain('\n'); + expect(out).not.toContain(' '); // Layer wrapper preserved expect(out).toContain('@layer anm-base'); }); @@ -69,3 +71,67 @@ describe('postProcessCss', () => { expect(postProcessCss('', { minify: true, targets: CHROME120 })).toBe(''); }); }); + +// Ported from packages/vite-plugin/tests/post-process.test.ts (deleted): that +// suite ran a test-local Lightning CSS mirror and never executed this +// production helper. These are the css-post-processing spec scenarios the +// mirror alone witnessed, now against the real export with fixed targets. +describe('postProcessCss — layer topology (css-post-processing spec)', () => { + test('preserves all six cascade layer blocks in declared order', () => { + const input = [ + '@layer anm-global, anm-base, anm-variants, anm-states, anm-system, anm-custom;', + '@layer anm-global { body { margin: 0; } }', + '@layer anm-base { .animus-Box-abc12345 { display: flex; } }', + '@layer anm-variants { .animus-Box-abc12345--size-sm { padding: 0.5rem; } }', + '@layer anm-states { .animus-Box-abc12345--disabled { opacity: 0.4; } }', + '@layer anm-system { .animus-u-def67890 { margin-top: 1rem; } }', + '@layer anm-custom { .animus-dyn-aabb1122-density { line-height: var(--animus-density); } }', + ].join('\n'); + + const out = postProcessCss(input, { minify: true, targets: CHROME120 }); + + const blockIdx = (name: string) => out.indexOf(`@layer ${name}{`); + const order = [ + 'anm-global', + 'anm-base', + 'anm-variants', + 'anm-states', + 'anm-system', + 'anm-custom', + ].map(blockIdx); + // Every block survives... + expect(order.filter((i) => i < 0)).toEqual([]); + // ...in declared order. + expect([...order].sort((a, b) => a - b)).toEqual(order); + }); + + test('does not merge layer blocks with different names', () => { + const input = + '@layer anm-base { .a { color: red; } }\n@layer anm-system { .b { color: blue; } }'; + const out = postProcessCss(input, { minify: true, targets: CHROME120 }); + expect(out).toContain('@layer anm-base{'); + expect(out).toContain('@layer anm-system{'); + }); + + test('preserves a slot variable nested under @media + @layer', () => { + const input = [ + '@layer anm-system { .animus-dyn-p { padding: var(--animus-p); } }', + '@media (min-width: 768px) {', + ' @layer anm-system { .animus-dyn-p-sm { padding: var(--animus-p-sm); } }', + '}', + ].join('\n'); + const out = postProcessCss(input, { minify: true, targets: CHROME120 }); + expect(out).toContain('var(--animus-p)'); + expect(out).toContain('var(--animus-p-sm)'); + expect(out).toContain('@media'); + }); + + test('prefixes user-select for Safari targets, preserving the original', () => { + const out = postProcessCss('.foo { user-select: none; }', { + minify: false, + targets: SAFARI15, + }); + expect(out).toContain('-webkit-user-select'); + expect(out).toContain('user-select: none'); + }); +}); diff --git a/packages/extract/tests/svelte-source-adapter.test.ts b/packages/extract/tests/svelte-source-adapter.test.ts index 2f2a8702..c68b918d 100644 --- a/packages/extract/tests/svelte-source-adapter.test.ts +++ b/packages/extract/tests/svelte-source-adapter.test.ts @@ -69,6 +69,13 @@ describe('adaptSvelteSource', () => { const moduleEntry = result.entries[0]; const instanceEntry = result.entries[1]; + // Byte-exact projections: the entry list above fixes the entries, so + // these two literals pin the whole output — both resolver binding forms + // (direct named `badge`/`moduleBadge`, aliased `badge as badgeAlias`), + // all four prop forms (absent ``, literal `tone={'strong'}`, + // shorthand `active={active}`, dynamic `size={width + 1}`), and every + // absence: no manufactured export, no `` template tag scanned, + // no `dynamicAttrs` local carried through. expect(moduleEntry.source).toBe( "import { moduleBadge } from './module-badge';\n;\n" ); @@ -81,34 +88,6 @@ describe('adaptSvelteSource', () => { expect(instanceEntry.source).not.toContain('export let'); }); - test('supports direct named and aliased imported resolver bindings', async () => { - const result = await okResult(); - const source = result.entries.map((entry) => entry.source).join('\n'); - - expect(source).toContain(' { - const result = await okResult(); - const instanceSource = result.entries[1].source; - - expect(instanceSource).toContain(''); - expect(instanceSource).toContain("tone={'strong'}"); - expect(instanceSource).toContain('active={active}'); - expect(instanceSource).toContain('size={width + 1}'); - }); - - test('does not manufacture exports or scan wrapper component tags', async () => { - const result = await okResult(); - const source = result.entries.map((entry) => entry.source).join('\n'); - - expect(source).not.toMatch(/\bexport\b/); - expect(source).not.toContain('Wrapper'); - expect(source).not.toContain('dynamicAttrs'); - }); - test('returns no entries when neither script scope witnesses a resolver', async () => { const result = await adaptSvelteSource( ``, diff --git a/packages/next-plugin/tests/analyze-project-args.test.ts b/packages/next-plugin/tests/analyze-project-args.test.ts deleted file mode 100644 index c0aeafea..00000000 --- a/packages/next-plugin/tests/analyze-project-args.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { buildAnalyzeProjectArgs } from '@animus-ui/extract/pipeline'; -import { describe, expect, test } from 'vitest'; - -const inputs = { - filesJson: 'next-files', - scalesJson: 'next-scales', - variableMapJson: 'next-variable-map', - contextualVarsJson: 'next-contextual-vars', - propConfigJson: 'next-prop-config', - groupRegistryJson: 'next-group-registry', - packageResolutionJson: 'next-package-resolution', - emitterConfigJson: 'next-emitter-config', - selectorAliasesJson: 'next-selector-aliases', - globalStyleBlocksJson: 'next-global-styles', - pathAliasesJson: 'next-path-aliases', - keyframesJson: 'next-keyframes', - staticCssJson: 'next-static-css', - conditionAliasesJson: 'next-condition-aliases', - externalDirsJson: 'next-external-dirs', - transformSourcesJson: 'next-transform-sources', -}; - -describe('Next analyzeProject argument construction', () => { - test('pins all 18 production NAPI slots', () => { - expect(buildAnalyzeProjectArgs({ ...inputs, devMode: false })).toEqual([ - 'next-files', - 'next-scales', - 'next-variable-map', - 'next-contextual-vars', - 'next-prop-config', - 'next-group-registry', - 'next-package-resolution', - false, - 'next-emitter-config', - 'next-selector-aliases', - null, - 'next-global-styles', - 'next-path-aliases', - 'next-keyframes', - 'next-static-css', - 'next-condition-aliases', - 'next-external-dirs', - 'next-transform-sources', - ]); - }); - - test('pins all 18 HMR NAPI slots', () => { - expect(buildAnalyzeProjectArgs({ ...inputs, devMode: true })).toEqual([ - 'next-files', - 'next-scales', - 'next-variable-map', - 'next-contextual-vars', - 'next-prop-config', - 'next-group-registry', - 'next-package-resolution', - true, - 'next-emitter-config', - 'next-selector-aliases', - null, - 'next-global-styles', - 'next-path-aliases', - 'next-keyframes', - 'next-static-css', - 'next-condition-aliases', - 'next-external-dirs', - 'next-transform-sources', - ]); - }); -}); diff --git a/packages/next-plugin/tests/manifest-diagnostics.test.ts b/packages/next-plugin/tests/manifest-diagnostics.test.ts index 981d43fa..08245f95 100644 --- a/packages/next-plugin/tests/manifest-diagnostics.test.ts +++ b/packages/next-plugin/tests/manifest-diagnostics.test.ts @@ -1,69 +1,12 @@ -import { surfaceManifestDiagnostics } from '@animus-ui/extract/pipeline'; import { readFileSync } from 'fs'; import { resolve } from 'path'; import { describe, expect, test } from 'vitest'; -const aliasWarn = { - file: 'src/broken.tsx', - component: 'Broken', - kind: 'warn', - message: - "unresolvable token alias {colors.missing} in 'border' — declaration dropped", -}; - +// Formatter behavior (warn/bail/skip wording, unknown-kind tolerance) for the +// shared `surfaceManifestDiagnostics` is pinned once, in the spec-named host +// copy: packages/vite-plugin/tests/manifest-diagnostics.test.ts. This file +// keeps only the Next-host architecture pins. describe('Next manifest diagnostic surfacing', () => { - test('surfaces one warn with file, component, property, and alias context', () => { - const warnings: string[] = []; - - surfaceManifestDiagnostics({ diagnostics: [aliasWarn] }, (message) => - warnings.push(message) - ); - - expect(warnings).toEqual([ - "⚠ src/broken.tsx: Broken: unresolvable token alias {colors.missing} in 'border' — declaration dropped", - ]); - }); - - test('surfaces bail and skip diagnostics (shared pipeline semantics)', () => { - const warnings: string[] = []; - - surfaceManifestDiagnostics( - { - diagnostics: [ - { - file: 'src/bail.tsx', - component: 'Bailed', - kind: 'bail', - message: 'stage evaluation failed', - }, - { - file: 'src/skip.tsx', - component: 'Skipped', - kind: 'skip', - message: 'dynamic borderColor', - }, - ], - }, - (message) => warnings.push(message) - ); - - expect(warnings).toEqual([ - '⚠ Bailed not extracted: stage evaluation failed', - '⚠ Skipped: skipped dynamic borderColor', - ]); - }); - - test('ignores unknown diagnostic kinds', () => { - const warnings: string[] = []; - - surfaceManifestDiagnostics( - { diagnostics: [{ ...aliasWarn, kind: 'future-kind' }] }, - (message) => warnings.push(message) - ); - - expect(warnings).toEqual([]); - }); - test('both pipelines route through a single shared diagnostic-surfacing core', () => { // The single surfacing call now lives in the shared pipeline's // runProjectAnalysis (used by BOTH bundler plugins), immediately after diff --git a/packages/next-plugin/tests/session-artifacts.test.ts b/packages/next-plugin/tests/session-artifacts.test.ts index fe3dbbf7..2e569fca 100644 --- a/packages/next-plugin/tests/session-artifacts.test.ts +++ b/packages/next-plugin/tests/session-artifacts.test.ts @@ -394,8 +394,6 @@ describe('session directory + transaction write order (design D1/D2)', () => { readSessionArtifact(session, ANALYSIS_COMMIT_ARTIFACT) ); expect('inputsHash' in commit).toBe(false); - expect(commit.manifestHash).toEqual(expect.any(String)); - expect(commit.stylesHash).toEqual(expect.any(String)); }); test('write order (Turbopack): manifest → inputs → styles → system-props → commit → epoch', async () => { @@ -543,7 +541,6 @@ describe('payload envelopes (spec: Manifest disk artifact)', () => { readSessionArtifact(session, 'analysis-inputs.json') ); expect(inputs.__animusSession.sessionId).toBe(session.sessionId); - expect(inputs.filesJson).toEqual(expect.any(String)); const styles = readSessionArtifact(session, 'styles.css'); const match = styles.match(/\/\* __animusSession (\{.*\}) \*\//); diff --git a/packages/oracle/__tests__/core-identity.test.ts b/packages/oracle/__tests__/core-identity.test.ts index cac07229..290afb56 100644 --- a/packages/oracle/__tests__/core-identity.test.ts +++ b/packages/oracle/__tests__/core-identity.test.ts @@ -65,10 +65,6 @@ describe('stableHash', () => { expect(stableHash('oracle')).toBe('f131a0db68862d39'); }); - it('is 16 lowercase hex characters', () => { - expect(stableHash({ any: 'value' })).toMatch(/^[0-9a-f]{16}$/); - }); - it('agrees with canonicalJson on content equality', () => { expect(stableHash({ a: 1, b: 2 })).toBe(stableHash({ b: 2, a: 1 })); expect(stableHash({ a: 1 })).not.toBe(stableHash({ a: 2 })); diff --git a/packages/oracle/__tests__/engine-prove-refine.test.ts b/packages/oracle/__tests__/engine-prove-refine.test.ts index 5d98d646..59c225bf 100644 --- a/packages/oracle/__tests__/engine-prove-refine.test.ts +++ b/packages/oracle/__tests__/engine-prove-refine.test.ts @@ -6,11 +6,8 @@ import { applyDeltas } from '../src/core/world'; import { createOracle, DEFAULT_MAX_CELLS } from '../src/engines'; import { createInMemoryHost } from '../src/providers/in-memory'; import { - BASE_DIMENSIONS, - card, - classesFor, - obligations, - panel, + config as fixtureConfig, + host as fixtureHost, smallNarrow, tokens, } from './fixture-world'; @@ -20,213 +17,42 @@ import type { OracleHost } from '../src/providers/host'; import type { InMemoryHostConfig } from '../src/providers/in-memory'; import type { FixtureOptions } from './fixture-world'; -/** The shared world plus two rules this suite's harvest must DISCOVER: - * `wide-1024` sits on a cut the domain does not declare, and - * `state-disabled` gives the fixpoint a state-guarded rule to reach. */ -const config = (options: FixtureOptions = {}): InMemoryHostConfig => ({ - rules: [ - { - id: 'global-body', - selector: { raw: 'body', classNames: [] }, - declarations: [ - { property: 'color', value: 'var(--color-text)' }, - { property: 'font-size', value: '16px' }, - ], - condition: TRUE, - layer: 'anm-global', - order: 0, - source: { file: 'src/theme.ts', span: [10, 40] }, - }, - { - id: 'base-card', - selector: { raw: '.anm-Card', classNames: ['anm-Card'] }, - declarations: [ - { - property: 'padding', - value: '4px', - authoredProperty: 'p', - authoredValue: '1', - }, - { property: 'gap', value: '2px' }, - ], - condition: TRUE, - layer: 'anm-base', - order: 0, - source: { file: 'src/Card.tsx', span: [67, 200] }, - origin: { component: 'Card', method: 'styles' }, - }, - { - id: 'surface', - selector: { raw: '.anm-surface', classNames: ['anm-surface'] }, - declarations: [ - { property: 'background', value: 'var(--surface-bg)' }, - { property: 'gap', value: '5px' }, - ], - condition: TRUE, - layer: 'anm-base', - order: 1, - source: { file: 'src/theme.ts' }, - }, - { - id: 'variant-large', - selector: { - raw: '.anm-Card--size-large', - classNames: ['anm-Card--size-large'], - }, - declarations: [{ property: 'padding', value: '12px' }], - condition: eq('variant:Card:size', 'large'), - layer: 'anm-variants', - order: 2, - source: { file: 'src/Card.tsx' }, - origin: { - component: 'Card', - method: 'variant', - variantProp: 'size', - variantOption: 'large', - }, - }, - { - id: 'variant-large-strong', - selector: { - raw: '.anm-Card.anm-Card--size-large', - classNames: ['anm-Card', 'anm-Card--size-large'], - }, - declarations: [{ property: 'padding', value: '20px' }], - condition: eq('variant:Card:size', 'large'), - layer: 'anm-variants', - order: 0, - source: { file: 'src/Card.tsx' }, - origin: { component: 'Card', method: 'compound', compoundIndex: 0 }, - }, - { - id: 'wide', - selector: { raw: '.anm-Card', classNames: ['anm-Card'] }, - declarations: [{ property: 'padding', value: '16px' }], - condition: range('viewport.inline', { min: 768 }), - layer: 'anm-variants', - order: 3, - source: { file: 'src/Card.tsx' }, - }, - { - id: 'wide-1024', - selector: { raw: '.anm-Card', classNames: ['anm-Card'] }, - declarations: [{ property: 'padding', value: '24px' }], - // 1024 is deliberately NOT in `cuts`: prove has to harvest it. - condition: range('viewport.inline', { min: 1024 }), - layer: 'anm-system', - order: 0, - source: { file: 'src/Card.tsx' }, - }, - { - id: 'state-disabled', - selector: { - raw: '.anm-Card--disabled', - classNames: ['anm-Card--disabled'], - }, - declarations: [{ property: 'gap', value: '10px' }], - condition: eq('state:Card:disabled', true), - layer: 'anm-states', - order: 0, - source: { file: 'src/Card.tsx' }, - }, - { - id: 'hover', - selector: { - raw: '.anm-Card:hover', - classNames: ['anm-Card'], - pseudo: ['hover'], - }, - declarations: [{ property: 'border-color', value: 'red' }], - condition: TRUE, - layer: 'anm-states', - order: 1, - source: { file: 'src/Card.tsx' }, - }, - { - id: 'marker', - selector: { - raw: '.anm-Card::before', - classNames: ['anm-Card'], - pseudo: ['::before'], - }, - declarations: [{ property: 'content', value: '""' }], - condition: TRUE, - layer: 'anm-custom', - order: 1, - source: { file: 'src/Card.tsx' }, - }, - { - id: 'unresolved', - selector: { raw: '.anm-Card', classNames: ['anm-Card'] }, - declarations: [{ property: 'outline-color', value: 'var(--missing)' }], - condition: TRUE, - layer: 'anm-custom', - order: 0, - source: { file: 'src/Card.tsx' }, - }, - ...(options.important === true - ? [ - { - id: 'base-important', - selector: { raw: '.anm-Card', classNames: ['anm-Card'] }, - declarations: [ - { property: 'padding', value: '1px', important: true }, - ], - condition: TRUE, - layer: 'anm-base', - order: 9, - source: { file: 'src/Card.tsx' }, - }, - { - id: 'variants-important', - selector: { raw: '.anm-Card', classNames: ['anm-Card'] }, - declarations: [ - { property: 'padding', value: '2px', important: true }, - ], - condition: TRUE, - layer: 'anm-variants', - order: 9, - source: { file: 'src/Card.tsx' }, - }, - ] - : []), - { - id: 'panel-base', - selector: { raw: '.anm-Panel', classNames: ['anm-Panel'] }, - declarations: [{ property: 'padding', value: '3px' }], - condition: TRUE, - layer: 'anm-base', - order: 2, - source: { file: 'src/Panel.tsx' }, - origin: { component: 'Panel', method: 'styles' }, - }, - ], - components: [card, panel], - dimensions: - options.pseudoDimension === true - ? { - ...BASE_DIMENSIONS, - 'pseudo:hover': { kind: 'finite', values: [false, true] }, - } - : { ...BASE_DIMENSIONS }, - cuts: { 'viewport.inline': [768] }, - namedScenarios: { - 'compact.dark': { - mode: 'dark', - 'viewport.inline': 375, - 'variant:Card:size': 'small', - 'state:Card:disabled': false, +/** The only way this suite's world differs from the shared one: two rules its + * harvest must DISCOVER. `wide-1024` sits on a cut the domain does not + * declare, and `state-disabled` gives the fixpoint a state-guarded rule to + * reach. Everything else — components, dimensions, cuts, named scenarios, + * `classesFor` and the rule dependencies — is the shared fixture world, so a + * change to engine behaviour reaches this suite too. */ +const EXTRA_RULES: InMemoryHostConfig['rules'] = [ + { + id: 'wide-1024', + selector: { raw: '.anm-Card', classNames: ['anm-Card'] }, + declarations: [{ property: 'padding', value: '24px' }], + // 1024 is deliberately NOT in `cuts`: prove has to harvest it. + condition: range('viewport.inline', { min: 1024 }), + layer: 'anm-system', + order: 0, + source: { file: 'src/Card.tsx' }, + }, + { + id: 'state-disabled', + selector: { + raw: '.anm-Card--disabled', + classNames: ['anm-Card--disabled'], }, + declarations: [{ property: 'gap', value: '10px' }], + condition: eq('state:Card:disabled', true), + layer: 'anm-states', + order: 0, + source: { file: 'src/Card.tsx' }, }, - classesFor, - ruleDependencies: { 'base-card': ['src/Card.tsx'] }, -}); +]; -const host = (options: FixtureOptions = {}): OracleHost => ({ - ...createInMemoryHost(config(options)), - tokens: tokens(), - obligations, -}); +const config = (options: FixtureOptions = {}): InMemoryHostConfig => + fixtureConfig({ ...options, extraRules: EXTRA_RULES }); + +const host = (options: FixtureOptions = {}): OracleHost => + fixtureHost({ ...options, extraRules: EXTRA_RULES }); const dynamicObligation = (oracle: ReturnType) => { const found = oracle diff --git a/packages/oracle/__tests__/engine-simulate-diff.test.ts b/packages/oracle/__tests__/engine-simulate-diff.test.ts index d1f269c3..c2b5a70f 100644 --- a/packages/oracle/__tests__/engine-simulate-diff.test.ts +++ b/packages/oracle/__tests__/engine-simulate-diff.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest'; import { asRuleId } from '../src/core/identity'; import { TRUE } from '../src/core/predicate'; -import { applyDeltas } from '../src/core/world'; import { createOracle } from '../src/engines'; import { createInMemoryHost } from '../src/providers/in-memory'; import { config, host, smallNarrow } from './fixture-world'; @@ -165,17 +164,6 @@ describe('simulate — force-dimension', () => { }, ]; - it('narrows the scenario domain of the hypothetical world', () => { - const oracle = createOracle(host()); - const forced = applyDeltas(oracle.baselineWorld(), deltas); - - expect(forced.scenario['variant:Card:size']).toEqual({ - kind: 'finite', - values: ['large'], - }); - expect(forced.interventions).toEqual(deltas); - }); - it('reports the rules the forced binding activates', () => { const result = createOracle(host()).simulate({ target: 'Card', deltas }); const activated = semanticDiffOf(result).entries.filter( diff --git a/packages/oracle/__tests__/fixture-world.ts b/packages/oracle/__tests__/fixture-world.ts index 221388f0..945d5fdb 100644 --- a/packages/oracle/__tests__/fixture-world.ts +++ b/packages/oracle/__tests__/fixture-world.ts @@ -113,6 +113,12 @@ export const tokens = (): TokenProvider => ({ export interface FixtureOptions { pseudoDimension?: boolean; important?: boolean; + /** Rules a suite's world genuinely adds on top of the shared one. They are + * spliced at one fixed point (after `wide`, before the pseudo-guarded + * rules) rather than appended per caller: rule array order feeds the + * synthetic program hash, so a caller-chosen position would make two + * suites' worlds differ by more than their rules. */ + extraRules?: InMemoryHostConfig['rules']; } /** The axes every fixture models. `pseudo:hover` is deliberately absent: the @@ -209,6 +215,7 @@ export const config = (options: FixtureOptions = {}): InMemoryHostConfig => ({ order: 3, source: { file: 'src/Card.tsx' }, }, + ...(options.extraRules ?? []), { id: 'hover', selector: { diff --git a/packages/oracle/__tests__/host-universe.test.ts b/packages/oracle/__tests__/host-universe.test.ts index 85afa90a..a7aed5d3 100644 --- a/packages/oracle/__tests__/host-universe.test.ts +++ b/packages/oracle/__tests__/host-universe.test.ts @@ -229,7 +229,6 @@ describe('createAnimusHost — the style universe over the emitted artifacts', ( expect(again).toEqual(universe.rules.map((rule) => rule.id)); expect(new Set(again).size).toBe(again.length); - expect(createAnimusHost(input).program.hash).toBe(host.program.hash); }); it('refuses an unmodeled construct in a sheet instead of skipping it', () => { diff --git a/packages/oracle/__tests__/places-resolve.test.ts b/packages/oracle/__tests__/places-resolve.test.ts index 154c4dda..d28c4bbd 100644 --- a/packages/oracle/__tests__/places-resolve.test.ts +++ b/packages/oracle/__tests__/places-resolve.test.ts @@ -1,4 +1,3 @@ -import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; @@ -136,16 +135,4 @@ describe('unresolved invocations surface on the analysis', () => { it('reports nothing for a file outside the snapshot', () => { expect(analysis.unresolved('src/App.tsx')).toEqual([]); }); - - it('keeps unresolved spans addressable in the real source', () => { - // Every unresolved entry, when one exists, must carry a span that - // `at(file, offset)` semantics could point into — pin the contract on - // the shape even while the fixture holds no ambiguity. - const source = readFileSync(join(SOURCE_ROOT, 'src/Group.tsx'), 'utf8'); - expect(source.length).toBeGreaterThan(0); - for (const entry of analysis.unresolved('src/Group.tsx')) { - expect(entry.span[0]).toBeGreaterThanOrEqual(0); - expect(entry.span[1]).toBeLessThanOrEqual(source.length); - } - }); }); diff --git a/packages/oracle/__tests__/places-session.test.ts b/packages/oracle/__tests__/places-session.test.ts index 5664c750..0db7ec0a 100644 --- a/packages/oracle/__tests__/places-session.test.ts +++ b/packages/oracle/__tests__/places-session.test.ts @@ -301,7 +301,6 @@ describe('check — the correspondence guard as a CI gate', () => { ); expect(code).toBe(0); const envelope = parseCheckEnvelope(out.join('')); - expect(envelope.command).toBe('check'); expect(envelope.result.ok).toBe(true); expect(envelope.result.files.length).toBeGreaterThan(0); }); diff --git a/packages/properties/__tests__/properties.test.ts b/packages/properties/__tests__/properties.test.ts index fcdea095..0ae4aff3 100644 --- a/packages/properties/__tests__/properties.test.ts +++ b/packages/properties/__tests__/properties.test.ts @@ -13,30 +13,29 @@ describe('UNITLESS_PROPERTIES', () => { } }); - test('includes standard unitless properties', () => { - expect(UNITLESS_PROPERTIES.has('opacity')).toBe(true); - expect(UNITLESS_PROPERTIES.has('z-index')).toBe(true); - expect(UNITLESS_PROPERTIES.has('font-weight')).toBe(true); - expect(UNITLESS_PROPERTIES.has('line-height')).toBe(true); - expect(UNITLESS_PROPERTIES.has('flex')).toBe(true); - }); - - test('includes modern unitless properties', () => { - expect(UNITLESS_PROPERTIES.has('aspect-ratio')).toBe(true); - expect(UNITLESS_PROPERTIES.has('scale')).toBe(true); - }); - - test('includes legacy flexbox properties', () => { - expect(UNITLESS_PROPERTIES.has('box-flex')).toBe(true); - expect(UNITLESS_PROPERTIES.has('box-flex-group')).toBe(true); - expect(UNITLESS_PROPERTIES.has('box-ordinal-group')).toBe(true); - expect(UNITLESS_PROPERTIES.has('flex-order')).toBe(true); + test('includes spec-named unitless members (standard, modern, legacy flexbox)', () => { + // openspec/specs/css-property-data scenarios: 'Contains standard unitless + // properties', 'Contains modern unitless properties', 'Contains legacy + // flexbox unitless properties'. + const required = [ + 'opacity', + 'z-index', + 'font-weight', + 'line-height', + 'flex', + 'aspect-ratio', + 'scale', + 'box-flex', + 'box-flex-group', + 'box-ordinal-group', + 'flex-order', + ]; + expect(required.filter((p) => !UNITLESS_PROPERTIES.has(p))).toEqual([]); }); test('excludes length properties', () => { - expect(UNITLESS_PROPERTIES.has('padding')).toBe(false); - expect(UNITLESS_PROPERTIES.has('margin')).toBe(false); - expect(UNITLESS_PROPERTIES.has('width')).toBe(false); + const lengths = ['padding', 'margin', 'width']; + expect(lengths.filter((p) => UNITLESS_PROPERTIES.has(p))).toEqual([]); }); }); diff --git a/packages/system/__tests__/as-child.test.tsx b/packages/system/__tests__/as-child.test.tsx index 0e24780e..33aa5419 100644 --- a/packages/system/__tests__/as-child.test.tsx +++ b/packages/system/__tests__/as-child.test.tsx @@ -115,19 +115,6 @@ describe('asChild', () => { }).toThrow(); }); - it('variant props resolve to classes on child element', () => { - const html = renderToString( - createElement( - Box, - { size: 'sm', asChild: true }, - createElement('section', null, 'content') - ) - ); - - expect(html).toMatch(/^
{ const html = renderToString( createElement( @@ -150,15 +137,6 @@ describe('asChild', () => { expect(tagHasClass(html, 'div', '--size-sm')).toBe(true); }); - it('asChild prop does not appear on rendered DOM element', () => { - const html = renderToString( - createElement(Box, { asChild: true }, createElement('span', null, 'text')) - ); - - expect(html).not.toContain('asChild'); - expect(html).not.toContain('asChild'); - }); - it('forwards parent event handlers to the child element', () => { let clicks = 0; diff --git a/packages/system/__tests__/bootstrap-snippet.test.ts b/packages/system/__tests__/bootstrap-snippet.test.ts index 9db43386..f4caa962 100644 --- a/packages/system/__tests__/bootstrap-snippet.test.ts +++ b/packages/system/__tests__/bootstrap-snippet.test.ts @@ -160,15 +160,6 @@ describe('bootstrap snippet — tri-state restoration', () => { }); describe('bootstrap snippet — record version', () => { - it('removes the attribute for a future record version', () => { - const harness = runBootstrap( - { [RECORD_KEY]: '{"v":2,"mode":"midnight","theme":"default"}' }, - { [ATTRIBUTE]: 'paper' } - ); - - expect(harness.mutations).toEqual([`remove:${ATTRIBUTE}`]); - }); - it('removes the attribute for a version-less record', () => { const harness = runBootstrap( { [RECORD_KEY]: '{"mode":"midnight"}' }, @@ -180,13 +171,19 @@ describe('bootstrap snippet — record version', () => { it('a version mismatch is terminal — legacy is not consulted', () => { // Pins the interpretation: an unreadable-version record is still a - // RECORD, so the pre-record key does not get a second vote. - const harness = runBootstrap({ - [RECORD_KEY]: '{"v":2,"mode":"midnight"}', - [LEGACY_KEY]: 'paper', - }); + // RECORD, so the pre-record key does not get a second vote. The + // server-rendered attribute makes the removal observable in the markup — + // a future version leaves nothing behind. + const harness = runBootstrap( + { + [RECORD_KEY]: '{"v":2,"mode":"midnight"}', + [LEGACY_KEY]: 'paper', + }, + { [ATTRIBUTE]: 'paper' } + ); expect(harness.mutations).toEqual([`remove:${ATTRIBUTE}`]); + expect(harness.attributes[ATTRIBUTE]).toBeUndefined(); expect(harness.localStorage.getItem).not.toHaveBeenCalledWith(LEGACY_KEY); }); }); @@ -282,9 +279,7 @@ describe('bootstrap snippet — OS preference is never materialized', () => { [RECORD_KEY]: '{"v":1,"mode":"system","theme":"default"}', }); - for (const mutation of harness.mutations) { - expect(mutation).toBe(`remove:${ATTRIBUTE}`); - } + expect(harness.mutations).toEqual([`remove:${ATTRIBUTE}`]); }); it('touches only `document` and `localStorage` globals', () => { diff --git a/packages/system/__tests__/compose.test.tsx b/packages/system/__tests__/compose.test.tsx index 16dbea5d..d91b3b8e 100644 --- a/packages/system/__tests__/compose.test.tsx +++ b/packages/system/__tests__/compose.test.tsx @@ -87,9 +87,11 @@ describe('compose()', () => { { Root, Control, Label }, { shared: { size: true } } ); - expect(Family.Root.displayName).toContain('.Root'); - expect(Family.Control.displayName).toContain('.Control'); - expect(Family.Label.displayName).toContain('.Label'); + // No `name` option — the family name falls back to the literal 'Composed' + // (nothing is derived from the Root component), so pin the exact strings. + expect(Family.Root.displayName).toBe('Composed.Root'); + expect(Family.Control.displayName).toBe('Composed.Control'); + expect(Family.Label.displayName).toBe('Composed.Label'); }); it('throws without a Root slot', () => { @@ -268,13 +270,6 @@ describe('compose()', () => { expect(html).toContain('c'); }); - it('displayName fallback when Root has no displayName', () => { - const Family = compose({ Root, Control }, { shared: { size: true } }); - // Builder output initially has empty displayName — falls back to 'Composed' - expect(Family.Root.displayName).toContain('.Root'); - expect(Family.Control.displayName).toContain('.Control'); - }); - it('accepts an .asComponent() output as the Root slot', () => { const Family = compose( { Root: WrappedRoot, Control }, @@ -289,18 +284,6 @@ describe('compose()', () => { expect(tagHasClass(html, 'section', '--size-sm')).toBe(true); expect(tagLacksClass(html, 'input', '--size-sm')).toBe(true); }); - - it('compose has no context option — CSS-only propagation', () => { - const Family = compose({ Root, Control }, { shared: { size: true } }); - - const html = renderToString( - createElement(Family.Root, { size: 'sm' }, createElement(Family.Control)) - ); - - // Root has class, child does NOT — CSS-only propagation - expect(tagHasClass(html, 'div', '--size-sm')).toBe(true); - expect(tagLacksClass(html, 'input', '--size-sm')).toBe(true); - }); }); // ─── composeWithContext() Tests ──────────────────────────────── diff --git a/packages/system/__tests__/composed-family.test.tsx b/packages/system/__tests__/composed-family.test.tsx index 0dc906c0..437f9fa1 100644 --- a/packages/system/__tests__/composed-family.test.tsx +++ b/packages/system/__tests__/composed-family.test.tsx @@ -100,12 +100,6 @@ function mountAndGetRefNode( // ─── createComposedFamily() Tests ─────────────────────────────── describe('createComposedFamily()', () => { - it('returns exact slot keys (PascalCase)', () => { - const Family = createComposedFamily({ Root, Control }, { name: 'Card' }); - expect('Root' in Family).toBe(true); - expect('Control' in Family).toBe(true); - }); - it('sets displayName as `${name}.${slot}`', () => { const Family = createComposedFamily( { Root, Control, Label }, diff --git a/packages/system/__tests__/createClassResolver.test.ts b/packages/system/__tests__/createClassResolver.test.ts index 209a50a9..7a504521 100644 --- a/packages/system/__tests__/createClassResolver.test.ts +++ b/packages/system/__tests__/createClassResolver.test.ts @@ -3,13 +3,11 @@ import { describe, expect, it } from 'vitest'; import { createClassResolver } from '../src/runtime/createClassResolver'; describe('createClassResolver', () => { - it('returns base class on empty call', () => { + it('returns base class for both call forms on an empty config', () => { const resolver = createClassResolver('animus-card-abc', {}); + // Omitted props and `{}` must agree — the `props || {}` guard is the only + // difference between the two call forms. expect(resolver()).toBe('animus-card-abc'); - }); - - it('returns base class when called with empty props', () => { - const resolver = createClassResolver('animus-card-abc', {}); expect(resolver({})).toBe('animus-card-abc'); }); @@ -203,15 +201,4 @@ describe('createClassResolver', () => { class: 'animus-box-abc', }); }); - - it('preserves the callable string API alongside attrs', () => { - const resolver = createClassResolver('animus-card-abc', { - states: ['selected'], - }); - - expect(resolver({ selected: true })).toBe( - 'animus-card-abc animus-card-abc--selected' - ); - expect(resolver.attrs).toBeTypeOf('function'); - }); }); diff --git a/packages/system/__tests__/extend.test.ts b/packages/system/__tests__/extend.test.ts index 6e1c505e..33f2b272 100644 --- a/packages/system/__tests__/extend.test.ts +++ b/packages/system/__tests__/extend.test.ts @@ -572,12 +572,9 @@ describe('SystemBuilder extend()', () => { expect(first.groupRegistry.space).toEqual(['m', 'rogue']); const { system: second } = builder.build(); - const after = second.toConfig(); - expect(JSON.parse(after.propConfig)).not.toHaveProperty('rogue'); - expect(JSON.parse(after.groupRegistry)).not.toHaveProperty('rogueGroup'); - expect(JSON.parse(after.propConfig).m.scale).toBe('space'); - expect(JSON.parse(after.groupRegistry).space).toEqual(['m']); - expect(after).toEqual(before); + // `before` was captured pre-mutation, so exact equality subsumes every + // per-field absence check (no rogue entries, scale/space unchanged). + expect(second.toConfig()).toEqual(before); }); it('ignores post-build mutation of nested properties arrays and object scales', () => { diff --git a/packages/system/__tests__/serialized-config.test.ts b/packages/system/__tests__/serialized-config.test.ts index 015a7d03..b06d7e70 100644 --- a/packages/system/__tests__/serialized-config.test.ts +++ b/packages/system/__tests__/serialized-config.test.ts @@ -345,59 +345,29 @@ describe('serializeInstance contract', () => { 'transformSources', 'transforms', ]); - - // Carrier types: four are JSON *strings*, transforms is a live JS object. - expect(config.propConfig).toEqual(expect.any(String)); - expect(config.groupRegistry).toEqual(expect.any(String)); - expect(config.selectorAliases).toEqual(expect.any(String)); - expect(config.conditionAliases).toEqual(expect.any(String)); - expect(config.transforms).toEqual(expect.any(Object)); }); - it('pins the field set and value types of every propConfig entry', () => { + it('pins the exact serialized form of every propConfig entry', () => { const propConfig = parseFeaturePropConfig(buildFeatureSystem().propConfig); - // ASSERTION 2: exact per-entry field sets (sorted) + value types. - expect(Object.keys(propConfig).sort()).toEqual(['m', 'ratio', 'size']); - - // `m` — string scale + named transform + negative flag. - expect(Object.keys(propConfig.m).sort()).toEqual([ - 'negative', - 'property', - 'scale', - 'transform', - ]); - expect(propConfig.m.property).toEqual(expect.any(String)); - expect(propConfig.m.scale).toEqual(expect.any(String)); - expect(propConfig.m.negative).toEqual(expect.any(Boolean)); - expect(propConfig.m.negative).toBe(true); - expect(propConfig.m.transform).toEqual(expect.any(String)); - // transform serializes to the transform's NAME, not the function body. - expect(propConfig.m.transform).toBe('px'); - - // `size` — array `properties`, inline object scale, currentVar. - expect(Object.keys(propConfig.size).sort()).toEqual([ - 'currentVar', - 'properties', - 'property', - 'scale', - ]); - expect(propConfig.size.property).toEqual(expect.any(String)); - expect(Array.isArray(propConfig.size.properties)).toBe(true); - expect(propConfig.size.properties).toEqual(['width', 'height']); - expect(propConfig.size.scale).toEqual(expect.any(Object)); - expect(propConfig.size.scale).toEqual({ sm: '4px', lg: '8px' }); - expect(propConfig.size.currentVar).toEqual(expect.any(String)); - - // `ratio` — minimal prop: only `property`. - expect(Object.keys(propConfig.ratio)).toEqual(['property']); - expect(propConfig.ratio.property).toEqual(expect.any(String)); - - // Negative guard: `strict` and `variable` must NEVER be serialized. - for (const entry of Object.values(propConfig)) { - expect(entry).not.toHaveProperty('strict'); - expect(entry).not.toHaveProperty('variable'); - } + // Exact whole-value equality (no-leakage): `strict` and `variable` must + // NEVER be serialized, so extra keys have to fail, not just missing ones. + // `transform` serializes to the transform's NAME, not the function body. + expect(propConfig).toEqual({ + m: { + negative: true, + property: 'margin', + scale: 'space', + transform: 'px', + }, + size: { + currentVar: '--size', + properties: ['width', 'height'], + property: 'width', + scale: { sm: '4px', lg: '8px' }, + }, + ratio: { property: 'aspectRatio' }, + }); }); it('maps each group name to its exact ordered prop-name array', () => { diff --git a/packages/system/__tests__/system-scheme-emission.test.ts b/packages/system/__tests__/system-scheme-emission.test.ts index b3e924b5..f6b81e66 100644 --- a/packages/system/__tests__/system-scheme-emission.test.ts +++ b/packages/system/__tests__/system-scheme-emission.test.ts @@ -539,13 +539,6 @@ describe('browser color-scheme classification', () => { ).toContain('color-scheme: dark;'); }); - it('records the classification on the manifest', () => { - expect(buildFullyConfiguredTheme().manifest.browserColorScheme).toEqual({ - paper: 'light', - midnight: 'dark', - }); - }); - // ── Classification WITHOUT a system preference (a legal shape) ── function buildClassificationOnlyTheme() { diff --git a/packages/system/__tests__/theme-state-isolation.test.ts b/packages/system/__tests__/theme-state-isolation.test.ts index b4ee5d90..55a0a952 100644 --- a/packages/system/__tests__/theme-state-isolation.test.ts +++ b/packages/system/__tests__/theme-state-isolation.test.ts @@ -27,9 +27,17 @@ describe('ThemeBuilder state isolation', () => { const builtB = branchB.build(); const builtBase = base.build(); - expect(Object.keys(builtA.colors).sort()).toEqual(['brand', 'onlyA']); - expect(Object.keys(builtB.colors).sort()).toEqual(['brand', 'onlyB']); - expect(Object.keys(builtBase.colors)).toEqual(['brand']); + // Exact objects: an isolation failure is a leakage failure, so the whole + // value is the claim — extra keys must fail, not just missing ones. + expect(builtA.colors).toEqual({ + brand: { primary: '#111111' }, + onlyA: { x: '#222222' }, + }); + expect(builtB.colors).toEqual({ + brand: { primary: '#111111' }, + onlyB: { x: '#333333' }, + }); + expect(builtBase.colors).toEqual({ brand: { primary: '#111111' } }); }); it('build() output is a snapshot — later builder calls never mutate it', () => { diff --git a/packages/system/__tests__/theme.test.ts b/packages/system/__tests__/theme.test.ts index 9e27688d..316f55d3 100644 --- a/packages/system/__tests__/theme.test.ts +++ b/packages/system/__tests__/theme.test.ts @@ -162,41 +162,32 @@ describe('ThemeBuilder nested storage', () => { describe('ThemeManifest', () => { const theme = buildTestTheme(); - it('manifest.tokenMap includes breakpoints', () => { - const tokenMap = theme.manifest.tokenMap; - expect(tokenMap['breakpoints.xs']).toBe('480'); - expect(tokenMap['breakpoints.sm']).toBe('768'); - expect(tokenMap['breakpoints.md']).toBe('1024'); - expect(tokenMap['breakpoints.lg']).toBe('1200'); - expect(tokenMap['breakpoints.xl']).toBe('1440'); - }); - - it('manifest.tokenMap includes scales with dot-path keys', () => { - const tokenMap = theme.manifest.tokenMap; - expect(tokenMap['space.4']).toBe('0.25rem'); - expect(tokenMap['fontSizes.16']).toBe('1rem'); - }); - - it('manifest.tokenMap includes colors as var() refs with dot-path keys', () => { - const tokenMap = theme.manifest.tokenMap; - expect(tokenMap['colors.ember']).toBe('var(--color-ember)'); - expect(tokenMap['colors.gray.300']).toBe('var(--color-gray-300)'); - expect(tokenMap['colors.void']).toBe('var(--color-void)'); - }); - - it('manifest.tokenMap includes semantic aliases as var() refs', () => { - const tokenMap = theme.manifest.tokenMap; - expect(tokenMap['colors.primary']).toBe('var(--color-primary)'); - expect(tokenMap['colors.bg']).toBe('var(--color-bg)'); - expect(tokenMap['colors.muted']).toBe('var(--color-muted)'); + it('manifest.tokenMap maps breakpoints/scales to raw values and colors to var() refs', () => { + expect(theme.manifest.tokenMap).toMatchObject({ + 'breakpoints.xs': '480', + 'breakpoints.sm': '768', + 'breakpoints.md': '1024', + 'breakpoints.lg': '1200', + 'breakpoints.xl': '1440', + 'space.4': '0.25rem', + 'fontSizes.16': '1rem', + 'colors.ember': 'var(--color-ember)', + 'colors.gray.300': 'var(--color-gray-300)', + 'colors.void': 'var(--color-void)', + 'colors.primary': 'var(--color-primary)', + 'colors.bg': 'var(--color-bg)', + 'colors.muted': 'var(--color-muted)', + }); }); it('manifest.variableMap maps dot-path to dash-join CSS var names', () => { - const variableMap = theme.manifest.variableMap; - expect(variableMap['colors.ember']).toBe('--color-ember'); - expect(variableMap['colors.gray.300']).toBe('--color-gray-300'); - expect(variableMap['colors.primary']).toBe('--color-primary'); - expect(variableMap['breakpoints.xs']).toBeUndefined(); + expect(theme.manifest.variableMap).toMatchObject({ + 'colors.ember': '--color-ember', + 'colors.gray.300': '--color-gray-300', + 'colors.primary': '--color-primary', + }); + // Breakpoints never materialize as CSS variables. + expect(theme.manifest.variableMap['breakpoints.xs']).toBeUndefined(); }); it('manifest.variableCss contains :root and mode blocks', () => { @@ -209,13 +200,10 @@ describe('ThemeManifest', () => { }); it('manifest.modes contains resolved raw values', () => { - const modes = theme.manifest.modes; - expect(modes.dark).toBeDefined(); - expect(modes.light).toBeDefined(); - expect(modes.dark['colors.primary']).toBe('#ff2800'); // ember - expect(modes.dark['colors.bg']).toBe('#000000'); // void - expect(modes.light['colors.primary']).toBe('#000000'); // void - expect(modes.light['colors.bg']).toBe('#e8e0d0'); // bone + expect(theme.manifest.modes).toMatchObject({ + dark: { 'colors.primary': '#ff2800', 'colors.bg': '#000000' }, // ember / void + light: { 'colors.primary': '#000000', 'colors.bg': '#e8e0d0' }, // void / bone + }); }); }); @@ -224,29 +212,27 @@ describe('ThemeManifest', () => { describe('theme.serialize()', () => { const theme = buildTestTheme(); - it('returns all 4 JSON strings', () => { - const result = theme.serialize(); - expect(result.scalesJson).toBeTypeOf('string'); - expect(result.variableMapJson).toBeTypeOf('string'); - expect(result.variableCss).toBeTypeOf('string'); - expect(result.contextualVarsJson).toBeTypeOf('string'); - }); - + // Field stringness is subsumed: every serialized field below is either + // JSON.parsed or matched as a string by these tests. it('scalesJson parses to dot-path keyed token map with breakpoints', () => { - const scales = JSON.parse(theme.serialize().scalesJson); - expect(scales['space.4']).toBe('0.25rem'); - expect(scales['space.8']).toBe('0.5rem'); - expect(scales['fontSizes.16']).toBe('1rem'); - expect(scales['colors.ember']).toBe('var(--color-ember)'); - expect(scales['colors.gray.300']).toBe('var(--color-gray-300)'); - expect(scales['breakpoints.xs']).toBe('480'); + expect(JSON.parse(theme.serialize().scalesJson)).toMatchObject({ + 'space.4': '0.25rem', + 'space.8': '0.5rem', + 'fontSizes.16': '1rem', + 'colors.ember': 'var(--color-ember)', + 'colors.gray.300': 'var(--color-gray-300)', + 'breakpoints.xs': '480', + }); }); it('variableMapJson maps dot-path keys to dash-join CSS var names', () => { const varMap = JSON.parse(theme.serialize().variableMapJson); - expect(varMap['colors.ember']).toBe('--color-ember'); - expect(varMap['colors.gray.300']).toBe('--color-gray-300'); - expect(varMap['colors.primary']).toBe('--color-primary'); + expect(varMap).toMatchObject({ + 'colors.ember': '--color-ember', + 'colors.gray.300': '--color-gray-300', + 'colors.primary': '--color-primary', + }); + // Breakpoints never materialize as CSS variables. expect(varMap['breakpoints.xs']).toBeUndefined(); }); diff --git a/packages/vite-plugin/tests/appearance-bootstrap-injection.test.ts b/packages/vite-plugin/tests/appearance-bootstrap-injection.test.ts index 56390f94..283e0c9b 100644 --- a/packages/vite-plugin/tests/appearance-bootstrap-injection.test.ts +++ b/packages/vite-plugin/tests/appearance-bootstrap-injection.test.ts @@ -172,6 +172,9 @@ describe('Vite injection option: opt-in injection', () => { prodContext({ appearanceBootstrap: ARTIFACT, layerDeclaration: '' }) ); + // The exact array is also the pin on D6's delivery-only clause: the script + // carries `code` and nothing else the plugin was handed, so the artifact's + // `cspHash` has no position in the emitted document to leak into. expect(tags).toEqual([ { tag: 'script', @@ -182,14 +185,6 @@ describe('Vite injection option: opt-in injection', () => { ]); }); - test('the plugin never reads the artifact beyond `code` (no cspHash leakage)', () => { - const tags = buildIndexHtmlTags( - prodContext({ appearanceBootstrap: ARTIFACT }) - ); - - expect(JSON.stringify(tags)).not.toContain(ARTIFACT.cspHash); - }); - test('an empty-code artifact emits no script tag', () => { // A caller defect, not a configuration: the guard is symmetric with the // layer-declaration branch, so an empty artifact must not leave an inert @@ -201,17 +196,18 @@ describe('Vite injection option: opt-in injection', () => { expect(tags.some((t) => t.tag === 'script')).toBe(false); expect(tags).toEqual([PRE_CHANGE_LAYER_TAG]); expect(JSON.stringify(tags)).not.toContain('bootstrap'); - }); - test('an empty-code artifact with no layer declaration emits nothing at all', () => { - const tags = buildIndexHtmlTags( - prodContext({ - appearanceBootstrap: { code: '', cspHash: '' }, - layerDeclaration: '', - }) - ); - - expect(tags).toEqual([]); + // The bootstrap and layer guards are independent build-time branches, so + // the same empty artifact with the layer branch also empty leaves nothing + // at all behind. + expect( + buildIndexHtmlTags( + prodContext({ + appearanceBootstrap: { code: '', cspHash: '' }, + layerDeclaration: '', + }) + ) + ).toEqual([]); }); }); diff --git a/packages/vite-plugin/tests/post-process.test.ts b/packages/vite-plugin/tests/post-process.test.ts deleted file mode 100644 index ef6b01a7..00000000 --- a/packages/vite-plugin/tests/post-process.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -import browserslist from 'browserslist'; -import { - browserslistToTargets, - transform as lcssTransform, -} from 'lightningcss'; -import { describe, expect, test } from 'vitest'; - -/** - * Minimal postProcessCss implementation for testing — mirrors the plugin's function. - */ -function postProcessCss( - css: string, - opts: { minify: boolean; targets: ReturnType } -): string { - const result = lcssTransform({ - filename: 'test.css', - code: Buffer.from(css), - minify: opts.minify, - targets: opts.targets, - }); - return result.code.toString(); -} - -const targets = browserslistToTargets(browserslist('defaults')); - -describe('Lightning CSS: @layer preservation', () => { - test('preserves all 6 cascade layers in correct order', () => { - const input = `@layer global, base, variants, states, system, custom; - -@layer base { - .animus-Box-abc12345 { - display: flex; - flex-direction: column; - } -} - -@layer variants { - .animus-Box-abc12345--size-sm { - padding: 0.5rem; - } -} - -@layer states { - .animus-Box-abc12345--disabled { - opacity: 0.4; - } -} - -@layer system { - .animus-u-def67890 { - margin-top: 1rem; - } -} - -@layer custom { - .animus-dyn-aabb1122-density { - line-height: var(--animus-density); - } -}`; - - const result = postProcessCss(input, { minify: true, targets }); - - // All 6 layer names must appear in the output - expect(result).toContain('global'); - expect(result).toContain('base'); - expect(result).toContain('variants'); - expect(result).toContain('states'); - expect(result).toContain('system'); - expect(result).toContain('custom'); - - // Layer order must be preserved: global before base before variants etc. - const globalIdx = result.indexOf('@layer global'); - const baseIdx = result.indexOf('@layer base'); - const variantsIdx = result.indexOf('@layer variants'); - const statesIdx = result.indexOf('@layer states'); - const systemIdx = result.indexOf('@layer system'); - const customIdx = result.indexOf('@layer custom'); - - expect(globalIdx).toBeLessThan(baseIdx); - expect(baseIdx).toBeLessThan(variantsIdx); - expect(variantsIdx).toBeLessThan(statesIdx); - expect(statesIdx).toBeLessThan(systemIdx); - expect(systemIdx).toBeLessThan(customIdx); - }); - - test('does not merge layers with different names', () => { - const input = `@layer base { .a { color: red; } } -@layer system { .b { color: blue; } }`; - - const result = postProcessCss(input, { minify: true, targets }); - - // Both layer blocks should exist separately - expect(result).toContain('@layer base'); - expect(result).toContain('@layer system'); - }); -}); - -describe('Lightning CSS: var() preservation', () => { - test('preserves CSS custom property references', () => { - const input = `.foo { - color: var(--colors-primary); - background: var(--colors-background); - padding: var(--animus-p); -}`; - - const result = postProcessCss(input, { minify: true, targets }); - - expect(result).toContain('var(--colors-primary)'); - expect(result).toContain('var(--colors-background)'); - expect(result).toContain('var(--animus-p)'); - }); - - test('preserves :root variable declarations', () => { - const input = `:root { - --colors-primary: #ff2800; - --colors-background: #111; -}`; - - const result = postProcessCss(input, { minify: true, targets }); - - expect(result).toContain('--colors-primary'); - expect(result).toContain('--colors-background'); - expect(result).toContain('#ff2800'); - }); - - test('preserves per-breakpoint slot variables', () => { - const input = `@layer system { - .animus-dyn-p { padding: var(--animus-p); } -} -@media (min-width: 768px) { - @layer system { - .animus-dyn-p-sm { padding: var(--animus-p-sm); } - } -}`; - - const result = postProcessCss(input, { minify: true, targets }); - - expect(result).toContain('var(--animus-p)'); - expect(result).toContain('var(--animus-p-sm)'); - }); -}); - -describe('Lightning CSS: minification', () => { - test('minified output is smaller than raw input', () => { - const input = `@layer base { - .animus-Box-abc12345 { - display: flex; - flex-direction: column; - padding: 1.5rem; - background: var(--colors-background); - border: 1px solid currentColor; - border-color: var(--colors-border); - transition: border-color 0.2s ease; - } - .animus-Box-abc12345:hover { - border-color: var(--colors-primary); - } -} - -@layer variants { - .animus-Box-abc12345--elevation-flat { - box-shadow: none; - } - .animus-Box-abc12345--elevation-raised { - box-shadow: 0 0 4px rgba(255, 40, 0, 0.2); - } -}`; - - const minified = postProcessCss(input, { minify: true, targets }); - - expect(minified.length).toBeLessThan(input.length); - // Should not contain unnecessary whitespace - expect(minified).not.toContain(' '); - }); - - test('non-minified output preserves formatting', () => { - const input = `.foo { - display: flex; - color: red; -}`; - - const result = postProcessCss(input, { minify: false, targets }); - - // Should still have newlines and indentation - expect(result).toContain('\n'); - }); -}); - -describe('Lightning CSS: autoprefixing', () => { - test('adds vendor prefixes for older Safari targets', () => { - const safariTargets = browserslistToTargets(browserslist('safari >= 14')); - - const input = `.foo { - backdrop-filter: blur(8px); - user-select: none; -}`; - - const result = postProcessCss(input, { - minify: false, - targets: safariTargets, - }); - - expect(result).toContain('-webkit-backdrop-filter'); - expect(result).toContain('-webkit-user-select'); - // Originals should also be preserved - expect(result).toContain('backdrop-filter: blur(8px)'); - expect(result).toContain('user-select: none'); - }); -}); diff --git a/packages/vite-plugin/tests/property-registration-split.test.ts b/packages/vite-plugin/tests/property-registration-split.test.ts deleted file mode 100644 index 6b09cb97..00000000 --- a/packages/vite-plugin/tests/property-registration-split.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { assembleStylesheet } from '@animus-ui/extract/pipeline'; -import { describe, expect, test } from 'vitest'; - -/** - * End-to-end proof of the property-registration split contract through the REAL - * shared `assembleStylesheet` — the same function both the Vite and Next plugins - * call (packages/vite-plugin/src/virtual-modules.ts and - * packages/next-plugin/src/extraction-session.ts import it identically). - * - * `@property` registration rules ride at the head of the theme's variable CSS - * (emitted by createTheme's build()). This asserts assembleStylesheet keeps them - * in the `variables` part — before the `@layer` declaration owns the cascade — - * with no assembly change: they flow through purely because variableCss → the - * variables part. - */ - -// Exactly the shape createTheme's serialize().variableCss produces for a -// registered contextual var (see packages/system/__tests__/theme.test.ts). -const VARIABLE_CSS = [ - '@property --current-bg { syntax: ""; inherits: true; initial-value: transparent; }', - '', - ':root {\n --color-primary: #abc;\n}', -].join('\n'); - -const COMPONENT_CSS = '@layer anm-base { .animus-card { padding: 8px; } }'; - -describe('assembleStylesheet: @property registration split contract', () => { - test('places @property in the variables part, absent from body/declaration', () => { - const { declaration, variables, body } = assembleStylesheet({ - variableCss: VARIABLE_CSS, - componentCss: COMPONENT_CSS, - split: true, - }); - - expect(variables).toContain('@property --current-bg'); - expect(body).not.toContain('@property'); - expect(declaration).not.toContain('@property'); - // declaration remains only the @layer ordering statement. - expect(declaration).toMatch(/@layer\s+[\w-]+(\s*,\s*[\w-]+)*\s*;/); - }); - - test('concatenation invariant: rejoined split equals the non-split output', () => { - const split = assembleStylesheet({ - variableCss: VARIABLE_CSS, - componentCss: COMPONENT_CSS, - split: true, - }); - const nonSplit = assembleStylesheet({ - variableCss: VARIABLE_CSS, - componentCss: COMPONENT_CSS, - }); - - const rejoined = [split.declaration, split.variables, split.body] - .filter(Boolean) - .join('\n'); - expect(rejoined).toBe(nonSplit); - }); - - test('@property appears before the @layer declaration in assembled output', () => { - const nonSplit = assembleStylesheet({ - variableCss: VARIABLE_CSS, - componentCss: COMPONENT_CSS, - }); - - const propIdx = nonSplit.indexOf('@property --current-bg'); - const layerBaseIdx = nonSplit.indexOf('@layer anm-base {'); - const declIdx = nonSplit.search(/@layer\s+[\w-]+(\s*,\s*[\w-]+)*\s*;/); - - expect(propIdx).toBeGreaterThanOrEqual(0); - // @property sits in the variables part, after the ordering declaration line - // but before any component @layer block. - expect(propIdx).toBeGreaterThan(declIdx); - expect(layerBaseIdx).toBeGreaterThan(propIdx); - }); - - test('opt-in: variableCss without @property yields no @property anywhere', () => { - const nonSplit = assembleStylesheet({ - variableCss: ':root {\n --color-primary: #abc;\n}', - componentCss: COMPONENT_CSS, - }); - expect(nonSplit).not.toContain('@property'); - }); -}); diff --git a/packages/vite-plugin/tests/transform-source.test.ts b/packages/vite-plugin/tests/transform-source.test.ts index 1746ed06..2a8951cd 100644 --- a/packages/vite-plugin/tests/transform-source.test.ts +++ b/packages/vite-plugin/tests/transform-source.test.ts @@ -82,6 +82,10 @@ function makeProbe( describe('transform: the plugin never treats its own virtual modules as sources', () => { // Both `.js`-suffixed resolved ids pass the shared engine-transform file // class on their raw text, which is exactly why the `\0` guard comes first. + // A dev page load sends every one of these back through `transform`; the + // guard returns before any `fileCache` or analysis mutation, so an empty + // cache per id is also the statement that a full pass accumulates no + // permanent `\0` keys. const VIRTUAL_IDS = [ RESOLVED_COMPONENTS_ID, RESOLVED_BRIDGE_ID, @@ -104,19 +108,6 @@ describe('transform: the plugin never treats its own virtual modules as sources' } ); - it('leaves no `\\0` keys behind after a full virtual-module load pass', async () => { - const probe = makeProbe(); - - for (const id of VIRTUAL_IDS) { - await transformSource(probe.ctx, 'export default ``;', id); - } - - expect( - [...probe.ctx.fileCache.keys()].filter((key) => key.includes('\0')) - ).toEqual([]); - expect(probe.analyses).toBe(0); - }); - it('still transforms a real source file (the guard is not vacuous)', async () => { const probe = makeProbe({ knownFiles: { 'src/Button.tsx': ['Button#1'] } }); diff --git a/scripts/hygiene/__fixtures__/reconciler/cjs-export-equals.ts.in b/scripts/hygiene/__fixtures__/reconciler/cjs-export-equals.ts.in index 6559870f..f0e34406 100644 --- a/scripts/hygiene/__fixtures__/reconciler/cjs-export-equals.ts.in +++ b/scripts/hygiene/__fixtures__/reconciler/cjs-export-equals.ts.in @@ -1,5 +1,5 @@ -// CJS-style target re-exports. The reconciler's getExportsOfFile maps -// `export = X;` to the symbol `default`, but consumer barrels may import -// the binding under its original name. Live re-exports SHALL NOT be stripped. -import X from './cjs-target'; -export { X }; +// CJS-style target re-export. The reconciler's getExportsOfFile maps +// `export = X;` (TSExportAssignment) to the symbol `default`; a barrel +// re-exporting that default under a named binding is live and SHALL NOT +// be stripped. +export { default as X } from './cjs-target'; diff --git a/scripts/hygiene/delete-unused.test.ts b/scripts/hygiene/delete-unused.test.ts index c1e53263..67ac4c60 100644 --- a/scripts/hygiene/delete-unused.test.ts +++ b/scripts/hygiene/delete-unused.test.ts @@ -117,24 +117,6 @@ function diag( } describe('oxlint JSON shape contract', () => { - test('diagnostics use oxlint field shape: code/message/filename + labels[0].span', () => { - const sample = wireRecord( - diag("Variable 'unusedConst' is declared but never used.", 6), - 'oxlint diagnostic fixture' - ); - expect(isJsonString(sample.code)).toBe(true); - expect(isJsonString(sample.message)).toBe(true); - expect(isJsonString(sample.filename)).toBe(true); - expect(Array.isArray(sample.labels)).toBe(true); - const span = firstLabelSpan(sample); - expect(isJsonNumber(span.offset)).toBe(true); - expect(isJsonNumber(span.line)).toBe(true); - expect(isJsonNumber(span.column)).toBe(true); - // Biome 2.x fields MUST NOT be present on the expected oxlint shape - expect(sample.category).toBeUndefined(); - expect(sample.location).toBeUndefined(); - }); - test('live oxlint output uses `eslint(...)` code wrapper', () => { // Empirical assertion against the real oxlint binary (via vp lint). // Session 89 (2026-04-24, biome-era) caught a fictional-vs-real mismatch @@ -164,7 +146,12 @@ describe('oxlint JSON shape contract', () => { expect(unusedDiag.code.endsWith(')')).toBe(true); expect(isJsonString(wire.filename)).toBe(true); expect(Array.isArray(wire.labels)).toBe(true); - expect(isJsonNumber(firstLabelSpan(wire).offset)).toBe(true); + const span = firstLabelSpan(wire); + expect(isJsonNumber(span.offset)).toBe(true); + // The deleter reads span.line/span.column (delete-unused.ts) — pin them + // against real oxlint output, not a synthetic literal. + expect(isJsonNumber(span.line)).toBe(true); + expect(isJsonNumber(span.column)).toBe(true); } finally { rmSync(dir, { recursive: true, force: true }); } diff --git a/scripts/hygiene/presenter.test.ts b/scripts/hygiene/presenter.test.ts index eebdd130..e3b87b38 100644 --- a/scripts/hygiene/presenter.test.ts +++ b/scripts/hygiene/presenter.test.ts @@ -9,13 +9,7 @@ import { describe, expect, test } from 'vitest'; -import { - analyze, - LAYER_D_EXPORT_THRESHOLD, - LAYER_D_FILE_THRESHOLD, - parseReceipts, - type Verdict, -} from './presenter'; +import { analyze, parseReceipts, type Verdict } from './presenter'; import type { Receipt } from './_receipts'; @@ -80,10 +74,12 @@ describe('analyze: convergence verdict', () => { rec({ iter: 2, layer: 'A', verb: 'format', kind: 'format-only' }), ]; const v = analyze(records, 5); - expect(v.convergence).toBe('converged'); - expect(v.finalIteration).toBe(2); - expect(v.finalIterationDeletes).toBe(0); - expect(v.suggestedExitCode).toBe(0); + expect(v).toMatchObject({ + convergence: 'converged', + finalIteration: 2, + finalIterationDeletes: 0, + suggestedExitCode: 0, + }); expect(v.summaryLines[0]).toMatch(/converged in 2 iteration/); }); @@ -104,10 +100,12 @@ describe('analyze: convergence verdict', () => { rec({ iter: 1, layer: 'C', verb: 'delete', kind: 'const-decl' }), ]; const v = analyze(records, 5, 3); - expect(v.convergence).toBe('converged'); - expect(v.finalIteration).toBe(3); - expect(v.finalIterationDeletes).toBe(0); - expect(v.suggestedExitCode).toBe(0); + expect(v).toMatchObject({ + convergence: 'converged', + finalIteration: 3, + finalIterationDeletes: 0, + suggestedExitCode: 0, + }); }); test('ranIters override: cap-hit-clean when trailing clean iter equals cap', () => { @@ -127,9 +125,11 @@ describe('analyze: convergence verdict', () => { rec({ iter: 4, layer: 'C', verb: 'delete', kind: 'const-decl' }), ]; const v = analyze(records, 5, 2); - expect(v.finalIteration).toBe(4); - expect(v.finalIterationDeletes).toBe(1); - expect(v.convergence).toBe('cap-hit-divergent'); + expect(v).toMatchObject({ + convergence: 'cap-hit-divergent', + finalIteration: 4, + finalIterationDeletes: 1, + }); }); test('cap-hit-clean: 5 iters, last has zero deletes (cap=5)', () => { @@ -176,9 +176,11 @@ describe('analyze: convergence verdict', () => { ); } const v = analyze(records, 5); - expect(v.convergence).toBe('cap-hit-divergent'); - expect(v.finalIterationDeletes).toBe(3); - expect(v.suggestedExitCode).toBe(1); + expect(v).toMatchObject({ + convergence: 'cap-hit-divergent', + finalIterationDeletes: 3, + suggestedExitCode: 1, + }); expect(v.summaryLines[0]).toMatch(/WARN: cascade did not converge/); expect(v.summaryLines[0]).toMatch(/iteration 5/); expect(v.summaryLines[0]).toMatch(/Layer C\/D/); @@ -197,9 +199,11 @@ describe('analyze: Layer D volume NOTE', () => { }), ]; const v = analyze(records, 5); - expect(v.layerDVolume.files).toBe(1); - expect(v.riskyDeletion).toBe(true); - expect(v.suggestedExitCode).toBe(1); + expect(v).toMatchObject({ + layerDVolume: expect.objectContaining({ files: 1 }), + riskyDeletion: true, + suggestedExitCode: 1, + }); expect( v.summaryLines.some((l) => l.startsWith('MANUAL REVIEW REQUIRED')) ).toBe(true); @@ -234,11 +238,6 @@ describe('analyze: Layer D volume NOTE', () => { v.summaryLines.some((l) => l.startsWith('NOTE: Layer D removed')) ).toBe(false); }); - - test('threshold constants match spec', () => { - expect(LAYER_D_FILE_THRESHOLD).toBe(1); - expect(LAYER_D_EXPORT_THRESHOLD).toBe(5); - }); }); describe('analyze: code-drift', () => { @@ -321,9 +320,10 @@ describe('analyze: combined signals', () => { rec({ iter: 5, layer: 'A', verb: 'format', kind: 'format-only' }) ); const v: Verdict = analyze(records, 5); - expect(v.convergence).toBe('cap-hit-clean'); - expect(v.layerDVolume.files).toBe(0); - expect(v.layerDVolume.exports).toBe(8); + expect(v).toMatchObject({ + convergence: 'cap-hit-clean', + layerDVolume: expect.objectContaining({ files: 0, exports: 8 }), + }); expect(v.riskyDeletion).toBe(false); expect(v.suggestedExitCode).toBe(0); expect(v.summaryLines.length).toBe(2); // INFO + NOTE diff --git a/scripts/hygiene/presenter.ts b/scripts/hygiene/presenter.ts index f7cbd688..a624f16e 100644 --- a/scripts/hygiene/presenter.ts +++ b/scripts/hygiene/presenter.ts @@ -52,8 +52,7 @@ export interface Verdict { summaryLines: string[]; } -export const LAYER_D_FILE_THRESHOLD = 1; -export const LAYER_D_EXPORT_THRESHOLD = 5; +const LAYER_D_EXPORT_THRESHOLD = 5; const DEFAULT_RECEIPTS_PATH = '.hygiene/receipts.jsonl'; const DEFAULT_VERDICT_PATH = '.hygiene/verdict.json'; diff --git a/scripts/hygiene/reconcile-after-knip.test.ts b/scripts/hygiene/reconcile-after-knip.test.ts index 0bb66dfe..601445cf 100644 --- a/scripts/hygiene/reconcile-after-knip.test.ts +++ b/scripts/hygiene/reconcile-after-knip.test.ts @@ -290,6 +290,22 @@ describe('fixStaleBarrelReExports — `export * from` handling', () => { rmSync(dir, { recursive: true, force: true }); } }); + + test('local re-exports without a module specifier are NOT touched', () => { + const dir = scratch(); + try { + const barrel = write( + dir, + 'packages/a/src/index.ts', + ["import X from './target';", 'export { X };', ''].join('\n') + ); + const fixed = fixStaleBarrelReExports([barrel]); + expect(fixed).toEqual([]); + expect(readFileSync(barrel, 'utf-8')).toContain('export { X };'); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); }); describe('fixStaleBarrelReExports — type-only re-exports', () => { @@ -464,35 +480,53 @@ describe('fixStaleBarrelReExports — span-preserving partial removals', () => { }); describe('fixStaleBarrelReExports — CJS export = (Tier 3 corner case)', () => { - // Aspirational: getExportsOfFile maps `export = X;` to the symbol "default", - // which can mismatch consumer barrels that re-export under the original - // import binding name. The reconciler MUST NOT strip a live re-export; it - // is preferable to leave a true-positive stale re-export in place than to - // strip a false-positive live one. - test('does not strip live re-export of CJS-style import binding', () => { + // refine-code-hygiene-dx D10 / task 11.3: getExportsOfFile maps + // `export = X;` (TSExportAssignment) to the symbol "default". A barrel + // re-exporting that default under a named binding is live; stripping it + // is the regression this pins. The `from './cjs-target'` form forces the + // reconciler to resolve and read the target, so removing the + // TSExportAssignment mapping fails this test. + test('does not strip a live `default as X` re-export from an `export =` target', () => { const dir = scratch(); try { - // CJS target: `export = X;` produces a default-style export only. write( dir, 'packages/a/src/cjs-target.ts', ['const X = 42;', 'export = X;', ''].join('\n') ); - const barrel = write( + const fixturePath = join( + process.cwd(), + 'scripts/hygiene/__fixtures__/reconciler/cjs-export-equals.ts.in' + ); + const barrelSource = readFileSync(fixturePath, 'utf-8'); + const barrel = write(dir, 'packages/a/src/index.ts', barrelSource); + const fixed = fixStaleBarrelReExports([barrel]); + expect(fixed).toEqual([]); + expect(readFileSync(barrel, 'utf-8')).toBe(barrelSource); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); + +describe('fixStaleBarrelReExports — .d.ts targets (Tier 3 corner case)', () => { + // A live `.d.ts` target must be resolvable, or the caller's + // unresolvable-means-deleted branch strips a LIVE re-export and logs it as + // `target-deleted` — silent data loss. refine-code-hygiene-dx D10: prefer + // leaving a stale re-export in place over stripping a live one. + test('does not strip a live extensionless re-export whose target is a .d.ts file', () => { + const dir = scratch(); + try { + write( dir, - 'packages/a/src/index.ts', - ["import X from './cjs-target';", 'export { X };', ''].join('\n') + 'packages/a/src/types.d.ts', + 'export declare const X: number;\n' ); + const barrelSource = "export { X } from './types';\n"; + const barrel = write(dir, 'packages/a/src/index.ts', barrelSource); const fixed = fixStaleBarrelReExports([barrel]); - // Reconciler is conservative when the re-export form has no module- - // specifier (this barrel's `export { X }` is a local re-export). The - // path filter (`isRelative`) means the reconciler only touches - // re-exports with a relative module specifier — so this barrel is - // skipped entirely, which is the correct behavior for a live re-export. expect(fixed).toEqual([]); - const out = readFileSync(barrel, 'utf-8'); - expect(out).toContain("import X from './cjs-target'"); - expect(out).toContain('export { X };'); + expect(readFileSync(barrel, 'utf-8')).toBe(barrelSource); } finally { rmSync(dir, { recursive: true, force: true }); } diff --git a/scripts/hygiene/reconcile-after-knip.ts b/scripts/hygiene/reconcile-after-knip.ts index 2787e81a..ce7e27b5 100644 --- a/scripts/hygiene/reconcile-after-knip.ts +++ b/scripts/hygiene/reconcile-after-knip.ts @@ -219,8 +219,13 @@ function resolveRelativeModule( base, // explicit extension in specifier `${base}.ts`, `${base}.tsx`, + // Declaration files are live targets too: an unresolvable target is + // treated as deleted by the caller, so omitting .d.ts strips live + // re-exports as `target-deleted`. + `${base}.d.ts`, `${base}/index.ts`, `${base}/index.tsx`, + `${base}/index.d.ts`, ]; for (const c of candidates) { try { diff --git a/scripts/verify/owner-graph.test.ts b/scripts/verify/owner-graph.test.ts index 6cd43cb9..9790206c 100644 --- a/scripts/verify/owner-graph.test.ts +++ b/scripts/verify/owner-graph.test.ts @@ -445,7 +445,10 @@ describe('root verification graph', () => { expect(references).toEqual([]); }); - it('keeps the root graph materially below the calibrated 57 tasks', () => { + it('keeps the root graph at or below the 30-task anti-reproliferation budget (G1)', () => { + // Budget provenance: enforce-workspace-topology design G1 — the ceiling + // that redirected topology work into the existing lint task instead of + // growing the graph. expect(Object.keys(rootTasks()).length).toBeLessThanOrEqual(30); }); diff --git a/scripts/verify/topology.ts b/scripts/verify/topology.ts index 4111ded9..6154f99d 100644 --- a/scripts/verify/topology.ts +++ b/scripts/verify/topology.ts @@ -50,7 +50,11 @@ import { Visitor, parseSync } from 'oxc-parser'; import type { Argument, StringLiteral } from 'oxc-parser'; export type Tree = 'packages' | 'e2e' | 'legacy' | 'other'; -export type Vector = 'import' | 'tsconfig-path' | 'package-dependency'; +export type Vector = + | 'import' + | 'tsconfig-path' + | 'package-dependency' + | 'fixture-sibling'; export interface Violation { vector: Vector; @@ -477,20 +481,87 @@ function readJsonc(path: string): JsonValue | undefined { // Reads the workspace package name of every e2e/* member that has a manifest. export function readE2ePackageNames(repoRoot: string): string[] { - const base = join(repoRoot, 'e2e'); - if (!existsSync(base)) return []; - const names: string[] = []; + // Projection of e2eMembersByName so one place owns e2e manifest reading. + return [...e2eMembersByName(repoRoot).keys()].sort(); +} + +// The e2e member (fixture directory name) owning an absolute path, or +// undefined when the path is not under e2e/. +export function e2eMember( + repoRoot: string, + absPath: string +): string | undefined { + const rel = relative(repoRoot, absPath); + if (rel === '' || rel.startsWith('..')) return undefined; + const parts = rel.split(sep); + return parts[0] === 'e2e' && parts.length > 1 ? parts[1] : undefined; +} + +// Workspace package name -> owning e2e member directory. Mirrors +// readE2ePackageNames but keeps the directory each name was declared in, so a +// bare workspace specifier can be attributed to a member. +export function e2eMembersByName(repoRoot: string): Map { + const byName = new Map(); for (const dir of topLevelDirs(repoRoot, 'e2e')) { const manifest = join(dir, 'package.json'); if (!existsSync(manifest)) continue; const parsed = readJson(manifest); - // A manifest whose `name` is absent, empty, or not a string names no - // workspace package, so it contributes no specifier to match against. if (isJsonObject(parsed) && isJsonString(parsed.name) && parsed.name) { - names.push(parsed.name); + const member = e2eMember(repoRoot, dir); + if (member !== undefined) byName.set(parsed.name, member); } } - return names.sort(); + return byName; +} + +// Vector 4 — fixture-sibling imports. e2e fixtures must stay self-contained +// (e2e-workspace-convention › "New framework fixtures remain self-contained": +// each fixture builds from only its own source plus active packages/* +// dependencies). The Tree-level rule cannot express this edge — sibling and +// self are both e2e -> e2e — so it is scanned per-member here. This is a +// static-specifier PROXY for the spec's build-level claim, with the same +// accepted blind spot (dynamic/runtime resolution) as the rest of the +// one-way rule. +export function scanFixtureSiblingImports(repoRoot: string): Violation[] { + const membersByName = e2eMembersByName(repoRoot); + const files: string[] = []; + for (const dir of topLevelDirs(repoRoot, 'e2e')) { + walk(dir, repoRoot, SOURCE_EXT, files); + } + files.sort(); + + const violations: Violation[] = []; + const seen = new Set(); + for (const file of files) { + const fromMember = e2eMember(repoRoot, file); + if (fromMember === undefined) continue; + for (const spec of extractSpecifiers(readFileSync(file, 'utf8'), file)) { + let toMember: string | undefined; + if (spec.value.startsWith('.')) { + toMember = e2eMember(repoRoot, resolve(dirname(file), spec.value)); + } else { + for (const [name, member] of membersByName) { + if (spec.value === name || spec.value.startsWith(`${name}/`)) { + toMember = member; + break; + } + } + } + if (toMember === undefined || toMember === fromMember) continue; + const rel = relative(repoRoot, file); + const key = `${rel}::${spec.value}`; + if (seen.has(key)) continue; + seen.add(key); + violations.push({ + vector: 'fixture-sibling', + file: rel, + from: 'e2e', + to: 'e2e', + detail: `imports e2e/${toMember} via '${spec.value}'`, + }); + } + } + return violations; } // Vector 1 — source imports across a forbidden boundary. @@ -742,6 +813,7 @@ export function collectViolations(repoRoot: string): Violation[] { ...scanSourceImports(repoRoot), ...scanTsconfigPaths(repoRoot), ...scanPackageDependencies(repoRoot), + ...scanFixtureSiblingImports(repoRoot), ]; } @@ -749,7 +821,8 @@ export function formatReport(violations: Violation[]): string { const lines = [ 'ERROR: workspace topology violation(s) — forbidden cross-boundary dependency.', ' One-Way Dependency Rule (AGENTS.md § Workspace Topology): packages/* must', - ' not import e2e/* or legacy/*; e2e/* must not import legacy/*.', + ' not import e2e/* or legacy/*; e2e/* must not import legacy/*. Each e2e', + ' fixture stays self-contained: no imports from sibling e2e/* fixtures.', ]; for (const v of violations) { lines.push(` ${v.file}: [${v.vector}] ${v.from} -> ${v.to}: ${v.detail}`); @@ -765,7 +838,7 @@ export function main(repoRoot: string): number { const violations = collectViolations(repoRoot); if (violations.length === 0) { console.log( - '[topology] workspace boundaries clean — no packages->e2e, packages->legacy, or e2e->legacy edges' + '[topology] workspace boundaries clean — no packages->e2e, packages->legacy, e2e->legacy, or e2e sibling-fixture imports' ); return 0; } diff --git a/scripts/verify/workers-contracts.sh b/scripts/verify/workers-contracts.sh index 8865df9d..63fd5176 100644 --- a/scripts/verify/workers-contracts.sh +++ b/scripts/verify/workers-contracts.sh @@ -7,8 +7,7 @@ cd "$ROOT" bunx vp test run scripts/verify/workers-config.test.ts cd "$ROOT/e2e/vinext-app" -bunx vp test run --config vitest.config.ts \ - scripts/config.test.ts scripts/hydration.test.tsx +bunx vp test run --config vitest.config.ts scripts/hydration.test.tsx cd "$ROOT/e2e/react-router-app" exec bunx vp test run --config vitest.config.ts \