Files
nexu-io--open-design/e2e/tests/critique-coverage.test.ts
wehub-resource-sync 070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:00:47 +08:00

275 lines
9.3 KiB
TypeScript

/**
* Critique surface coverage walker (Phase 13.2). Walks every named
* symbol of the Critique Theater feature (SSE event names, panelist
* roles, reducer state phases, i18n keys) and asserts that each one
* is referenced from at least one production file AND at least one
* test file across the workspace.
*
* Lives in `e2e/tests/` per the repo boundary rule (root `AGENTS.md`):
*
* > Cross-app, cross-runtime, or repository-resource consistency
* > checks belong in `e2e/tests/` when they need to observe more
* > than one app/package boundary.
*
* > App packages must not import another app's private `src/` or
* > `tests/` implementation as a shared helper.
*
* The walker is by definition cross-app: it reads the web reducer,
* the daemon critique module, the contracts package, and the e2e UI
* suite. Hosting it under `apps/web/tests/` would couple the web
* package's test lane to daemon and e2e file layout (so a
* daemon-only refactor could break the web lane), which is exactly
* the boundary the repo rule forbids. Siri-Ray P2 on PR #1318.
*
* Adding a new SSE event / role / phase / i18n key:
*
* 1. Add the symbol to its contract / dictionary in
* `packages/contracts/src/critique.ts` (SSE events, roles) or
* `apps/web/src/i18n/types.ts` plus every locale (i18n keys).
* 2. Add at least one production caller (reducer branch, role-
* keyed CSS, i18n consumer).
* 3. Add at least one test that exercises the new symbol.
* 4. Append the symbol literal to the right group below.
* `SSE_EVENTS` is auto-built from
* `CRITIQUE_SSE_EVENT_NAMES` so it stays in sync without
* manual upkeep; `PANELIST_ROLE_STRINGS` is auto-built from
* `PANELIST_ROLES` for the same reason. `PHASE_STRINGS` and
* `I18N_KEYS` are hand-maintained.
*
* What the walker DOES catch:
*
* Renaming an EXISTING symbol in production / tests without
* updating the walker array trips the gate. The walker still
* looks for the old name and fails to find it; the reviewer
* of the rename PR sees the failing assertion and asks for
* the walker update in the same diff.
*
* What the walker does NOT catch on its own:
*
* Adding a NEW hand-maintained symbol (phase string or i18n
* key) without adding it to the walker array leaves the gate
* green because the walker does not know to look for a symbol
* it was not told about. Mitigation: contracts-derived groups
* (`SSE_EVENTS`, `PANELIST_ROLE_STRINGS`) auto-grow so the
* contracts package is the only place that needs editing;
* the hand-maintained groups (`PHASE_STRINGS`, `I18N_KEYS`)
* are short enough that step 4 is a one-line edit alongside
* the contracts / i18n change.
*/
import { readFileSync, readdirSync, statSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
declare global {
interface ImportMeta {
glob<T = unknown>(pattern: string, options: { eager: true }): Record<string, T>;
}
}
type CritiqueContracts = {
CRITIQUE_SSE_EVENT_NAMES: readonly string[];
PANELIST_ROLES: readonly string[];
};
const contractsModules = import.meta.glob<CritiqueContracts>(
'../../packages/contracts/src/critique.ts',
{ eager: true },
);
const contracts = Object.values(contractsModules)[0];
if (!contracts) {
throw new Error(
'critique-coverage walker could not load packages/contracts/src/critique.ts via import.meta.glob; '
+ 'this almost always means the contracts file was renamed or moved.',
);
}
const { CRITIQUE_SSE_EVENT_NAMES, PANELIST_ROLES } = contracts;
const REPO_ROOT = fileURLToPath(new URL('../../', import.meta.url));
const SELF_PATH = fileURLToPath(import.meta.url);
const SRC_ROOTS = [
path.join(REPO_ROOT, 'apps/web/src'),
path.join(REPO_ROOT, 'apps/daemon/src/critique'),
path.join(REPO_ROOT, 'packages/contracts/src'),
];
const TEST_ROOTS = [
path.join(REPO_ROOT, 'apps/web/tests'),
path.join(REPO_ROOT, 'apps/daemon/tests'),
path.join(REPO_ROOT, 'packages/contracts/tests'),
path.join(REPO_ROOT, 'e2e/tests'),
path.join(REPO_ROOT, 'e2e/ui'),
];
const FILE_EXTENSIONS = /\.(ts|tsx|js|jsx|css|md)$/;
const SKIP_DIRS = new Set(['node_modules', 'dist', '.next', '.turbo']);
function walk(root: string): string[] {
const out: string[] = [];
const stack: string[] = [root];
while (stack.length > 0) {
const cur = stack.pop()!;
let stat;
try {
stat = statSync(cur);
} catch {
continue;
}
if (stat.isDirectory()) {
let entries: string[];
try {
entries = readdirSync(cur);
} catch {
continue;
}
for (const entry of entries) {
if (SKIP_DIRS.has(entry)) continue;
stack.push(path.join(cur, entry));
}
} else if (stat.isFile() && FILE_EXTENSIONS.test(cur)) {
out.push(cur);
}
}
return out;
}
function readCorpus(files: string[]): string {
return files.map((f) => {
try {
return readFileSync(f, 'utf8');
} catch {
return '';
}
}).join('\n');
}
const SRC_FILES = SRC_ROOTS.flatMap(walk);
// The walker walks e2e/tests, which contains this file. Hand-maintained
// PHASE_STRINGS and I18N_KEYS literals declared below would otherwise
// satisfy the test-side coverage assertion against themselves, so a real
// downstream test exercising a symbol could be deleted with the gate
// still green. Exclude this file from TEST_FILES so the corpus only
// holds independent evidence.
const TEST_FILES = TEST_ROOTS.flatMap(walk).filter((f) => path.resolve(f) !== SELF_PATH);
const SRC_CORPUS = readCorpus(SRC_FILES);
const TEST_CORPUS = readCorpus(TEST_FILES);
/**
* Strict source-side match: production code MUST reference the symbol
* by its exact wire form. A `critique.<event>` SSE name must appear
* as `critique.<event>`, not as the unprefixed PanelEvent type alias,
* so the SSE channel name stays load-bearing in production.
*/
function srcReferences(corpus: string, sym: string): boolean {
return corpus.includes(sym);
}
/**
* Lenient test-side match: reducer tests dispatch the PanelEvent shape
* directly (no `critique.` prefix on the SSE channel), so for an SSE
* event symbol the test corpus is allowed to satisfy via either the
* prefixed form (`critique.<event>`) or the unprefixed PanelEvent type
* form (`type: '<event>'`). Both forms prove an assertion exercises the
* event end-to-end.
*/
function testReferences(corpus: string, sym: string): boolean {
if (corpus.includes(sym)) return true;
if (sym.startsWith('critique.')) {
const unprefixed = sym.slice('critique.'.length);
return new RegExp(`type:\\s*'${unprefixed}'`).test(corpus);
}
return false;
}
const SSE_EVENTS = [...CRITIQUE_SSE_EVENT_NAMES];
const PANELIST_ROLE_STRINGS = PANELIST_ROLES.map((r) => `'${r}'`);
const PHASE_STRINGS = [
"'idle'",
"'running'",
"'shipped'",
"'degraded'",
"'interrupted'",
"'failed'",
];
const I18N_KEYS = [
'critiqueTheater.userFacingName',
'critiqueTheater.roundLabel',
'critiqueTheater.composite',
'critiqueTheater.threshold',
'critiqueTheater.interrupt',
'critiqueTheater.interrupted',
'critiqueTheater.degradedHeading',
'critiqueTheater.shippedSummary',
'critiqueTheater.interruptedSummary',
];
describe('critique-coverage walker (Phase 13.2)', () => {
describe('SSE event names', () => {
it.each(SSE_EVENTS)('production references %s', (sym) => {
expect(
srcReferences(SRC_CORPUS, sym),
`expected SRC corpus to mention SSE event "${sym}" at least once`,
).toBe(true);
});
it.each(SSE_EVENTS)('tests reference %s', (sym) => {
expect(
testReferences(TEST_CORPUS, sym),
`expected TEST corpus to mention SSE event "${sym}" (prefixed or as PanelEvent type) at least once`,
).toBe(true);
});
});
describe('Panelist roles', () => {
it.each(PANELIST_ROLE_STRINGS)('production references %s', (sym) => {
expect(
srcReferences(SRC_CORPUS, sym),
`expected SRC corpus to mention panelist role string ${sym} at least once`,
).toBe(true);
});
it.each(PANELIST_ROLE_STRINGS)('tests reference %s', (sym) => {
expect(
testReferences(TEST_CORPUS, sym),
`expected TEST corpus to mention panelist role string ${sym} at least once`,
).toBe(true);
});
});
describe('Reducer lifecycle phases', () => {
it.each(PHASE_STRINGS)('production references %s', (sym) => {
expect(
srcReferences(SRC_CORPUS, sym),
`expected SRC corpus to mention reducer phase string ${sym} at least once`,
).toBe(true);
});
it.each(PHASE_STRINGS)('tests reference %s', (sym) => {
expect(
testReferences(TEST_CORPUS, sym),
`expected TEST corpus to mention reducer phase string ${sym} at least once`,
).toBe(true);
});
});
describe('i18n keys', () => {
it.each(I18N_KEYS)('production references %s', (sym) => {
expect(
srcReferences(SRC_CORPUS, sym),
`expected SRC corpus to mention i18n key "${sym}" at least once`,
).toBe(true);
});
it.each(I18N_KEYS)('tests reference %s', (sym) => {
expect(
testReferences(TEST_CORPUS, sym),
`expected TEST corpus to mention i18n key "${sym}" at least once`,
).toBe(true);
});
});
});