Schemas for the whole v1 surface: 38 methods, 9 events, 25 named types and the
JSON-RPC envelope, with x-privileged / x-transports / x-deadlineMs / x-errors
annotations that both generators emit as data rather than prose.
Four generators over one IR (contracts/codegen/schema_ir.py), so the C++ structs,
the TypeScript types and the OpenRPC document cannot disagree about what the
contract says:
gen_cpp.py -> core/generated/velox_proto.{hpp,cpp}
gen_ts.py -> extension/src/shared/protocol/
gen_openrpc.py -> contracts/openrpc.json
gen_cpp_conformance.py -> tests/conformance/cpp/fixture_dispatcher.hpp
Inbound parsing never throws: parse<T>() returns std::expected<T, ParseError> and
nlohmann's throwing ADL from_json is deliberately not emitted. Schema constraints
(minimum, maxLength, pattern, ...) become real runtime checks in both languages —
the daemon does not trust the extension and the extension does not trust the
daemon.
59 golden fixtures: a success case per method, 12 error cases, 9 events. Replayed
by tests/conformance/ against both the generated C++ and a live server over both
transports. tools/mockd serves the same fixtures with unhappy-path flags so the
GUI and EXT lanes never wait for veloxd.
run.sh also proves capture.offer fails open: with a daemon answering slower than
750 ms the client gives up and lets Firefox take the download.
core/generated/ is libveloxproto, a separate target from libveloxcore, which
still never sees JSON — see docs/adr/0009.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
90 lines
3.0 KiB
TypeScript
90 lines
3.0 KiB
TypeScript
/**
|
|
* 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<Fixture, 'file'>),
|
|
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<string, unknown> = {};
|
|
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
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<string, Fixture> {
|
|
const out = new Map<string, Fixture>();
|
|
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<string, Fixture> {
|
|
const out = new Map<string, Fixture>();
|
|
for (const f of fixtures) {
|
|
if (f.notification) out.set(f.notification.method, f);
|
|
}
|
|
return out;
|
|
}
|