/** * Loading and replaying contracts/fixtures. * * mockd answers from the golden files rather than from hand-written mock objects, so a * GUI or extension built against it is built against the same bytes the conformance suite * replays at the real daemon. If a fixture is wrong, everyone finds out at once. */ import { readFileSync, readdirSync, statSync } from 'node:fs'; import { join, relative } from 'node:path'; import { randomUUID } from 'node:crypto'; export interface Fixture { readonly file: string; readonly name: string; readonly description: string; readonly kind?: 'timeout'; readonly transport?: 'uds' | 'ws'; readonly request?: { jsonrpc: '2.0'; id: number | string; method: string; params?: unknown }; readonly notification?: { jsonrpc: '2.0'; method: string; params: unknown }; readonly response?: { jsonrpc: '2.0'; id: number | string; result?: unknown; error?: unknown } | null; readonly assertions?: readonly string[]; } function walk(dir: string): string[] { const out: string[] = []; for (const entry of readdirSync(dir)) { const full = join(dir, entry); if (statSync(full).isDirectory()) out.push(...walk(full)); else if (entry.endsWith('.json')) out.push(full); } return out; } export function loadFixtures(root: string): Fixture[] { return walk(root).map((file) => ({ ...(JSON.parse(readFileSync(file, 'utf8')) as Omit), file: relative(root, file), })); } /** * Placeholders stand for values a golden file cannot pin: a fresh uuid, the current time, * an opaque token. Resolving them here is what lets one fixture be replayed forever. */ export function resolvePlaceholders(value: unknown): unknown { if (typeof value === 'string') { switch (value) { case '$uuid': return randomUUID(); case '$isoDate': return new Date().toISOString(); case '$opaque': return Buffer.from(randomUUID() + randomUUID()).toString('base64url'); case '$any': return 'placeholder'; default: return value; } } if (Array.isArray(value)) return value.map(resolvePlaceholders); if (value && typeof value === 'object') { const out: Record = {}; for (const [k, v] of Object.entries(value as Record)) { out[k] = resolvePlaceholders(v); } return out; } return value; } /** Success fixtures indexed by method, so a request can be answered from a golden file. */ export function indexByMethod(fixtures: readonly Fixture[]): Map { const out = new Map(); for (const f of fixtures) { const method = f.request?.method; if (!method || !f.response || !('result' in f.response)) continue; if (!out.has(method)) out.set(method, f); } return out; } export function eventFixtures(fixtures: readonly Fixture[]): Map { const out = new Map(); for (const f of fixtures) { if (f.notification) out.set(f.notification.method, f); } return out; }