Files
open-design/apps/daemon/tests/mocks-golden.test.ts
T
Marc ChanandGitHub d5d9a761ca refactor(daemon): organize source modules (#4454)
* docs(daemon): add agent guidance

* refactor(daemon): organize source modules

* chore(guard): update daemon design system import

* refactor(daemon): shorten domain module filenames

* fix(daemon): correct pitch-deck manifest path after test move

The pitch-deck manifest test moved one directory deeper into
tests/design-systems/ but kept the old '../../..' repoRoot, resolving
to apps/ instead of the repo root and failing to find
plugins/_official/examples/html-ppt-pitch-deck/open-design.json.

* fix(daemon): retarget media model verifier

Generated-By: looper 0.9.10 (runner=fixer, agent=codex)

* docs(daemon): refresh AGENTS legacy paths after module reorg

* test(e2e): allow manual edit P0 CI scaling

Generated-By: looper 0.9.10 (runner=fixer, agent=codex)

* test(daemon): fix frontmatter import path

Generated-By: looper 0.9.10 (runner=fixer, agent=opencode)

* fix(web): avoid replaying legacy terminal runs

Generated-By: looper 0.9.10 (runner=fixer, agent=opencode)

* fix(web): preserve legacy terminal replay recovery

Generated-By: looper 0.9.10 (runner=fixer, agent=opencode)

* fix(daemon): correct live-artifacts test data root

* fix(daemon): cover moved route registrars in context contract

Generated-By: looper 0.9.10 (runner=fixer, agent=opencode)

* fix(daemon): restore moved runtime artifact imports

Generated-By: looper 0.9.10 (runner=fixer, agent=opencode)
2026-06-20 16:08:20 +00:00

114 lines
4.4 KiB
TypeScript

// Golden daemon-event snapshots — addresses the regression-signal point
// from review on #3241: smoke-testing that mocks RUN catches only crashes
// or protocol-level garbage; it does NOT catch a parser change that
// semantically reshapes the events the daemon emits to the UI.
//
// This test replays representative recordings through the actual daemon
// stream handlers and asserts the emitted event sequence matches a
// committed `mocks/golden/<trace>.events.json`. A parser tweak that
// drops a tool_result, changes a usage shape, or renames an event type
// fails this test loudly.
//
// Update flow when a parser change is INTENTIONAL:
// MOCKS_GOLDEN_UPDATE=1 pnpm --filter @open-design/daemon test mocks-golden
// then `git diff mocks/golden/` and commit the new shapes.
//
// Auto-skips when the recording corpus hasn't been fetched yet (see
// `mocks/scripts/fetch-recordings.sh`); CI that exercises this test must
// fetch first.
import { describe, it, expect } from 'vitest';
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createClaudeStreamHandler } from '../src/runtimes/claude-stream.js';
import { createJsonEventStreamHandler } from '../src/runtimes/json-event-stream.js';
const HERE = dirname(fileURLToPath(import.meta.url));
const REPO = join(HERE, '../../..');
const MOCK_AGENT = join(REPO, 'mocks/mock-agent.mjs');
const GOLDEN_DIR = join(REPO, 'mocks/golden');
const RECORDINGS_DIR = join(REPO, 'mocks/recordings');
// Median-tool-count successful traces per agent (selected from manifest
// 2026-05-29). Each one's `.jsonl` lives in `mocks/recordings/` after
// `bash mocks/scripts/fetch-recordings.sh`.
const CASES: Array<{ agent: 'claude' | 'codex' | 'opencode'; trace: string }> = [
{ agent: 'claude', trace: '314d6833-0377-4ac4-ba11-2b8d7eca5511' },
{ agent: 'codex', trace: 'dcdff3b3-cd39-4dcd-be83-372830a29639' },
{ agent: 'opencode', trace: '9a9522ec-575f-432f-aeed-efc491e900aa' },
];
// Replace per-spawn-volatile fields with stable sentinels so the
// snapshot stays diffable across runs. Currently only `sessionId` —
// claude's mock emits a fresh UUID every spawn. Opencode/codex carry
// the recording's own session/thread id so they're already stable.
function normalizeVolatile(events: unknown[]): unknown[] {
return events.map(e => {
if (!e || typeof e !== 'object') return e;
const rec = e as Record<string, unknown>;
const out: Record<string, unknown> = { ...rec };
if ('sessionId' in out) out.sessionId = '<normalized>';
return out;
});
}
function runMockAndCollectEvents(agent: string, trace: string): unknown[] {
// Force no-delay so the spawn returns quickly + deterministically.
const proc = spawnSync(
process.execPath,
[MOCK_AGENT, '--as', agent, '--no-delay'],
{
env: { ...process.env, OD_MOCKS_TRACE: trace, OD_MOCKS_NO_DELAY: '1' },
input: 'golden-test-prompt',
encoding: 'utf-8',
timeout: 30_000,
maxBuffer: 50 * 1024 * 1024,
},
);
if (proc.status !== 0) {
throw new Error(
`mock-agent --as ${agent} exit ${proc.status}: ${proc.stderr.slice(0, 500)}`,
);
}
const events: unknown[] = [];
const sink = (e: unknown) => events.push(e);
const handler =
agent === 'claude'
? createClaudeStreamHandler(sink)
: createJsonEventStreamHandler(agent, sink);
handler.feed(proc.stdout);
return normalizeVolatile(events);
}
const recordingsAvailable =
existsSync(RECORDINGS_DIR) &&
CASES.every(c => existsSync(join(RECORDINGS_DIR, `${c.trace}.jsonl`)));
describe.skipIf(!recordingsAvailable)(
'mocks goldens — daemon event shape regression',
() => {
for (const { agent, trace } of CASES) {
it(`${agent} ${trace.slice(0, 8)}`, () => {
const events = runMockAndCollectEvents(agent, trace);
const goldenPath = join(GOLDEN_DIR, `${trace}.events.json`);
if (process.env.MOCKS_GOLDEN_UPDATE === '1') {
mkdirSync(GOLDEN_DIR, { recursive: true });
writeFileSync(
goldenPath,
JSON.stringify({ agent, trace, events }, null, 2) + '\n',
);
return;
}
const golden = JSON.parse(readFileSync(goldenPath, 'utf-8'));
expect({ agent, trace, events }).toEqual(golden);
});
}
},
);