Files
Codeman/test/sse-registry-parity.test.ts
arkonandClaude Opus 4.8 d5f91e4cd7 test(ci): run the unit suite in CI + frontend-syntax gate; green pre-existing test debt
- CI: add a 'test' job running the unit suite via config/vitest.ci.config.ts. Excludes browser (Playwright/chromium) and perf tests (timing-flaky), like the existing test/mobile suite. Safe in CI: TmuxManager no-ops shell commands under VITEST (test/setup.ts).
- Add scripts/check-frontend-syntax.mjs (node --check on src/web/public/*.js), wired into the lint job — catches a class of frontend SyntaxError that passes lint today (lint globs only TS).
- Add test/security-regression.test.ts (wired Host/Origin guard, self-update CSRF, CSP/security headers, text/plain raw body, WS anti-CSWSH) + test/sse-registry-parity.test.ts (backend<->frontend SSE registry parity).
- Green pre-existing test debt surfaced by the new gate: stale 'Session not found' asserts -> 'not found' substring; drop tests for removed helpers (isError now internal; createSuccessResponse deleted); file-stream-manager: mock realpathSync + fix stale /tmp assertion; sse-subscription-filter: lifecycle events broadcast to all clients (only terminal stream filtered); session.test.ts: mkdir /tmp/test; skip one interactive-respawn test needing a real PTY (covered by respawn-controller.test.ts).
- Full non-mobile suite verified green locally (2680 passed, 12 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 20:02:15 +02:00

68 lines
2.9 KiB
TypeScript

/**
* SSE event registry parity — backend ⇄ frontend.
*
* CLAUDE.md mandates that the backend SSE registry (src/web/sse-events.ts) and the
* frontend SSE_EVENTS object (src/web/public/constants.js) stay in sync. They are
* hand-maintained in two files with no build-time link, so this asserts the set of
* event-string VALUES matches exactly — a drift here means the UI silently ignores
* (or never receives) an event.
*
* No port needed (pure file/module comparison).
*/
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import * as SseEvents from '../src/web/sse-events.js';
/** Every `export const X = '...' as const` in sse-events.ts is an event string. */
function backendEventValues(): Set<string> {
return new Set(Object.values(SseEvents).filter((v): v is string => typeof v === 'string'));
}
/** Extract the string values from the `const SSE_EVENTS = { ... }` block in constants.js. */
function frontendEventValues(): Set<string> {
const file = resolve(import.meta.dirname, '..', 'src', 'web', 'public', 'constants.js');
const src = readFileSync(file, 'utf8');
const start = src.indexOf('const SSE_EVENTS = {');
expect(start, 'SSE_EVENTS object not found in constants.js').toBeGreaterThanOrEqual(0);
// The object is closed by the first line that is exactly "};" after the declaration.
const after = src.slice(start);
const end = after.indexOf('\n};');
expect(end, 'SSE_EVENTS closing "};" not found').toBeGreaterThan(0);
const block = after.slice(0, end);
const values = new Set<string>();
// Match `KEY: 'value'` / `KEY: "value"` pairs (skip commented lines).
for (const line of block.split('\n')) {
const code = line.replace(/\/\/.*$/, '');
const m = code.match(/:\s*['"]([^'"]+)['"]/);
if (m) values.add(m[1]);
}
return values;
}
describe('SSE event registry parity (backend ⇄ frontend)', () => {
const backend = backendEventValues();
const frontend = frontendEventValues();
it('extracts a non-trivial number of events from both sources', () => {
// Guard against a parsing regression silently making this test vacuous.
expect(backend.size).toBeGreaterThan(100);
expect(frontend.size).toBeGreaterThan(100);
});
it('has no backend events missing from the frontend SSE_EVENTS registry', () => {
const missing = [...backend].filter((e) => !frontend.has(e)).sort();
expect(missing, `events in sse-events.ts but not constants.js SSE_EVENTS: ${missing.join(', ')}`).toEqual([]);
});
it('has no frontend events missing from the backend sse-events.ts registry', () => {
const extra = [...frontend].filter((e) => !backend.has(e)).sort();
expect(extra, `events in constants.js SSE_EVENTS but not sse-events.ts: ${extra.join(', ')}`).toEqual([]);
});
it('the two registries are exactly equal in size', () => {
expect(frontend.size).toBe(backend.size);
});
});