|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | +// |
| 3 | +// Production-path witness + first-wiring ratchet for the SDUI JSX gate |
| 4 | +// (#12924, maintainer ruling 2026-08-29: wire it; execution point 3 demands a |
| 5 | +// witness that REALLY PARSES the checked-in manifest into `validateTree`). |
| 6 | +// |
| 7 | +// ── Why this file exists, stated as the blind spot it closes ────────────── |
| 8 | +// |
| 9 | +// Every other guard on `validateTree` in this repo constructs its manifest |
| 10 | +// IN MEMORY, so a green suite was compatible with the production gate being |
| 11 | +// parse-only for the whole life of the code — no test resolved a real |
| 12 | +// artefact, because there was nothing to resolve (#12924's finding). These |
| 13 | +// tests read the REAL repo-root `sdui.manifest.json` from DISK, feed it |
| 14 | +// through the REAL production entry points, and pin the arming delta itself. |
| 15 | +// |
| 16 | +// Two cross-package inputs, both declared so the graph can see them |
| 17 | +// (`check:examples-live-imports`, `@objectstack/lint#test` inputs in |
| 18 | +// turbo.json): the repo-root artefact, and the three shipped html pages. |
| 19 | + |
| 20 | +import { existsSync, readFileSync } from 'node:fs'; |
| 21 | +import { dirname, join } from 'node:path'; |
| 22 | +import { fileURLToPath } from 'node:url'; |
| 23 | +import { describe, expect, it } from 'vitest'; |
| 24 | + |
| 25 | +import { validateJsxPages } from './validate-jsx-pages.js'; |
| 26 | +import { runAuthoringRules } from './authoring-rules.js'; |
| 27 | + |
| 28 | +import { CapabilityMapPage } from '../../../examples/app-showcase/src/ui/pages/capability-map.page.js'; |
| 29 | +import { CommandCenterJsxPage } from '../../../examples/app-showcase/src/ui/pages/command-center-jsx.page.js'; |
| 30 | +import { StartHerePage } from '../../../examples/app-showcase/src/ui/pages/start-here.page.js'; |
| 31 | + |
| 32 | +const HERE = dirname(fileURLToPath(import.meta.url)); |
| 33 | + |
| 34 | +interface LedgerRow { |
| 35 | + page: string; |
| 36 | + rule: string; |
| 37 | + severity: string; |
| 38 | + tag: string; |
| 39 | + count: number; |
| 40 | +} |
| 41 | +// readFileSync rather than a JSON module import: under NodeNext the latter |
| 42 | +// needs an import attribute (TS1543), which would add to this package's frozen |
| 43 | +// TEST_DEBT tsc count for no behavioral gain. |
| 44 | +const ledger = JSON.parse(readFileSync(join(HERE, 'sdui-jsx-baseline.json'), 'utf8')) as { |
| 45 | + findings: LedgerRow[]; |
| 46 | +}; |
| 47 | + |
| 48 | +/** Walk up to the workspace root — the directory holding pnpm-workspace.yaml. */ |
| 49 | +function findUp(predicate: (dir: string) => boolean): string { |
| 50 | + let dir = HERE; |
| 51 | + for (;;) { |
| 52 | + if (predicate(dir)) return dir; |
| 53 | + const parent = dirname(dir); |
| 54 | + if (parent === dir) throw new Error('workspace root not found from ' + HERE); |
| 55 | + dir = parent; |
| 56 | + } |
| 57 | +} |
| 58 | +const REPO = findUp((dir) => existsSync(join(dir, 'pnpm-workspace.yaml'))); |
| 59 | + |
| 60 | +// The artefact, from DISK — the same bytes `resolveSduiManifest()` (packages/ |
| 61 | +// cli, path 1: join(process.cwd(), 'sdui.manifest.json')) picks up when the |
| 62 | +// gate runs from the repo root. Loud absence: an absent artefact silently |
| 63 | +// reverts production to parse-only, so this read failing IS the regression. |
| 64 | +const ARTEFACT = join(REPO, 'sdui.manifest.json'); |
| 65 | +const manifest = JSON.parse(readFileSync(ARTEFACT, 'utf8')); |
| 66 | + |
| 67 | +describe('production witness: the checked-in manifest reaches validateTree', () => { |
| 68 | + it('is the real artefact (57-component public tier, no intrinsic HTML tags)', () => { |
| 69 | + const keys = Object.keys(manifest.components); |
| 70 | + expect(keys.length).toBeGreaterThan(0); |
| 71 | + // The vocabulary facts the ratchet below stands on. If a regeneration |
| 72 | + // legitimately changes them, the ledger is re-derived in the same PR. |
| 73 | + expect(keys).toContain('flex'); |
| 74 | + expect(keys).toContain('html'); |
| 75 | + expect(keys).not.toContain('div'); |
| 76 | + }); |
| 77 | + |
| 78 | + it('arms full validation through validateJsxPages: manifest-only diagnostics fire', () => { |
| 79 | + const stack = { |
| 80 | + pages: [ |
| 81 | + { |
| 82 | + name: 'witness_page', |
| 83 | + kind: 'html', |
| 84 | + // `flex` is a real public component; `no-such-block` is not. Only |
| 85 | + // validateTree (fed by the DISK manifest) can tell them apart — |
| 86 | + // parse-only cannot emit unknown-component at all. |
| 87 | + source: '<flex direction="col" notARealProp="x"><no-such-block /></flex>', |
| 88 | + }, |
| 89 | + ], |
| 90 | + }; |
| 91 | + const wired = validateJsxPages(stack, { manifest }); |
| 92 | + const rules = new Set(wired.map((f) => f.rule)); |
| 93 | + expect(rules).toContain('jsx-unknown-component'); // no-such-block, judged by the manifest |
| 94 | + expect(rules).toContain('jsx-unknown-prop'); // notARealProp on flex, judged by flex's declared inputs |
| 95 | + |
| 96 | + // The arming delta itself: the SAME stack, parse-only, emits neither. |
| 97 | + const parseOnly = validateJsxPages(stack); |
| 98 | + const parseOnlyRules = new Set(parseOnly.map((f) => f.rule)); |
| 99 | + expect(parseOnlyRules).not.toContain('jsx-unknown-component'); |
| 100 | + expect(parseOnlyRules).not.toContain('jsx-unknown-prop'); |
| 101 | + }); |
| 102 | + |
| 103 | + it('threads through the production registry entry (runAuthoringRules ctx.sduiManifest)', () => { |
| 104 | + // The same seam `os validate`/`os build`/`os lint` drive: authoring-rules' |
| 105 | + // validateJsxPages entry reads ctx.sduiManifest — never a lookalike call. |
| 106 | + const stack = { |
| 107 | + pages: [{ name: 'witness_page', kind: 'html', source: '<no-such-block />' }], |
| 108 | + }; |
| 109 | + const withManifest = runAuthoringRules('validate', { |
| 110 | + normalized: stack, |
| 111 | + sduiManifest: manifest, |
| 112 | + }); |
| 113 | + expect(withManifest.some((f) => f.rule === 'jsx-unknown-component')).toBe(true); |
| 114 | + |
| 115 | + const without = runAuthoringRules('validate', { normalized: stack }); |
| 116 | + expect(without.some((f) => f.rule === 'jsx-unknown-component')).toBe(false); |
| 117 | + }); |
| 118 | +}); |
| 119 | + |
| 120 | +describe('first-wiring ratchet: the shipped pages against the wired gate (ui#6779 ratchet-to-zero)', () => { |
| 121 | + it('wired census over the three shipped html pages equals the ledger — both directions', () => { |
| 122 | + const stack = { pages: [CapabilityMapPage, CommandCenterJsxPage, StartHerePage] }; |
| 123 | + const findings = validateJsxPages(stack as never, { manifest }); |
| 124 | + |
| 125 | + const census = new Map<string, number>(); |
| 126 | + for (const f of findings) { |
| 127 | + const page = /page "([^"]+)"/.exec(f.where)?.[1] ?? '(unknown page)'; |
| 128 | + const tag = /<([a-zA-Z0-9:_-]+)>/.exec(f.where)?.[1] ?? '(no tag)'; |
| 129 | + const key = `${page}|${f.rule}|${f.severity}|${tag}`; |
| 130 | + census.set(key, (census.get(key) ?? 0) + 1); |
| 131 | + } |
| 132 | + |
| 133 | + const recorded = new Map<string, number>( |
| 134 | + ledger.findings.map((r): [string, number] => [`${r.page}|${r.rule}|${r.severity}|${r.tag}`, r.count]), |
| 135 | + ); |
| 136 | + |
| 137 | + const newViolations: string[] = []; |
| 138 | + for (const [key, count] of census) { |
| 139 | + const allowed = recorded.get(key) ?? 0; |
| 140 | + if (count > allowed) newViolations.push(`${key} — live ${count} vs ledger ${allowed}`); |
| 141 | + } |
| 142 | + const stale: string[] = []; |
| 143 | + for (const [key, count] of recorded) { |
| 144 | + const live = census.get(key) ?? 0; |
| 145 | + if (live < count) stale.push(`${key} — ledger ${count} vs live ${live}`); |
| 146 | + } |
| 147 | + |
| 148 | + expect( |
| 149 | + newViolations, |
| 150 | + 'NEW wired-gate findings beyond the ratchet ledger. Fix the page (or regenerate the manifest ' + |
| 151 | + 'if the vocabulary legitimately grew) — never grow packages/lint/src/sdui-jsx-baseline.json.', |
| 152 | + ).toEqual([]); |
| 153 | + expect( |
| 154 | + stale, |
| 155 | + 'STALE ledger rows — the live run no longer produces them. Ratchet-to-zero: delete these rows ' + |
| 156 | + 'from packages/lint/src/sdui-jsx-baseline.json in this same PR.', |
| 157 | + ).toEqual([]); |
| 158 | + }); |
| 159 | + |
| 160 | + it('parse-only over the same pages stays clean (today\'s pre-wiring behavior, pinned)', () => { |
| 161 | + const stack = { pages: [CapabilityMapPage, CommandCenterJsxPage, StartHerePage] }; |
| 162 | + expect(validateJsxPages(stack as never)).toEqual([]); |
| 163 | + }); |
| 164 | +}); |
0 commit comments