#!/usr/bin/env node import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import assert from "node:assert/strict"; const DEFAULT_FIXTURE = "apps/api/engine_gateway/golden/engine_gateway_contract.v1.json"; const DEFAULT_SCHEMA = "apps/api/engine_gateway/golden/engine_gateway_schema.v1.json"; const EXPECTED_EVENTS = ["token", "done", "error"]; function usage() { return [ "Usage: node scripts/check-engine-gateway-contract.mjs [--fixture path] [--schema path] [--json]", "", "Validates the engine gateway v1 golden fixture without importing Python code.", ].join("\n"); } function parseArgs(argv) { const args = { fixture: DEFAULT_FIXTURE, schema: DEFAULT_SCHEMA, json: false, }; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; if (arg === "--fixture") { args.fixture = argv[i + 1]; i += 1; } else if (arg === "--schema") { args.schema = argv[i + 1]; i += 1; } else if (arg === "--json") { args.json = true; } else if (arg === "--help" || arg === "-h") { console.log(usage()); process.exit(0); } else { throw new Error(`Unknown argument: ${arg}\n${usage()}`); } } if (!args.fixture || !args.schema) { throw new Error(`Missing path argument.\n${usage()}`); } return args; } function readJson(path) { return JSON.parse(readFileSync(resolve(path), "utf8")); } function assertPlainObject(value, label) { assert.equal(typeof value, "object", `${label} must be an object`); assert.notEqual(value, null, `${label} must not be null`); assert.equal(Array.isArray(value), false, `${label} must not be an array`); } function assertOnlyKeys(value, allowedKeys, label) { for (const key of Object.keys(value)) { assert.ok(allowedKeys.includes(key), `${label}.${key} is not in the v1 contract`); } } function assertRequired(value, requiredKeys, label) { for (const key of requiredKeys) { assert.ok(Object.hasOwn(value, key), `${label}.${key} is required`); } } function assertNullableObject(value, label) { if (value === null || value === undefined) return; assertPlainObject(value, label); } function assertInteger(value, label) { assert.equal(Number.isInteger(value), true, `${label} must be an integer`); } function assertNumber(value, label) { assert.equal(typeof value, "number", `${label} must be a number`); assert.equal(Number.isFinite(value), true, `${label} must be finite`); } function assertString(value, label) { assert.equal(typeof value, "string", `${label} must be a string`); } function schemaDef(schema, name) { const def = schema?.$defs?.[name]; assertPlainObject(def, `$defs.${name}`); return def; } function assertRootShape(schema, fixture) { assert.deepEqual(schema["x-engine-gateway-sse-events"], EXPECTED_EVENTS); assertRequired(fixture, schema.required, "fixture"); assertOnlyKeys(fixture, Object.keys(schema.properties), "fixture"); assert.equal(fixture.version, schema.properties.version.const); } function assertGenerateRequest(schema, request) { const requestDef = schemaDef(schema, "GenerateRequest"); const messageDef = schemaDef(schema, "EngineMessage"); const allowedRequestKeys = Object.keys(requestDef.properties); const messageRoles = messageDef.properties.role.enum; const aiRoles = requestDef.properties.ai_role.enum; assertPlainObject(request, "generate_request"); assertRequired(request, requestDef.required, "generate_request"); assertOnlyKeys(request, allowedRequestKeys, "generate_request"); assert.ok(aiRoles.includes(request.ai_role), "generate_request.ai_role must be a known role"); assert.ok(Array.isArray(request.messages), "generate_request.messages must be an array"); assert.ok(request.messages.length > 0, "generate_request.messages must not be empty"); request.messages.forEach((message, index) => { assertPlainObject(message, `generate_request.messages[${index}]`); assertRequired(message, messageDef.required, `generate_request.messages[${index}]`); assertOnlyKeys(message, Object.keys(messageDef.properties), `generate_request.messages[${index}]`); assert.ok(messageRoles.includes(message.role), `generate_request.messages[${index}].role is invalid`); assertString(message.content, `generate_request.messages[${index}].content`); if (Object.hasOwn(message, "cache")) { assert.equal(typeof message.cache, "boolean", `generate_request.messages[${index}].cache must be boolean`); } }); if (Object.hasOwn(request, "model")) { assert.ok(request.model === null || typeof request.model === "string", "generate_request.model must be string or null"); } if (Object.hasOwn(request, "session_id")) { assert.ok( request.session_id === null || typeof request.session_id === "string", "generate_request.session_id must be string or null", ); } if (Object.hasOwn(request, "max_tokens")) assertInteger(request.max_tokens, "generate_request.max_tokens"); if (Object.hasOwn(request, "temperature")) assertNumber(request.temperature, "generate_request.temperature"); if (Object.hasOwn(request, "structured_schema")) assertNullableObject(request.structured_schema, "generate_request.structured_schema"); if (Object.hasOwn(request, "metadata")) assertPlainObject(request.metadata, "generate_request.metadata"); } function assertGenerateResponse(schema, response) { const responseDef = schemaDef(schema, "GenerateResponse"); assertPlainObject(response, "generate_response"); assertRequired(response, responseDef.required, "generate_response"); assertOnlyKeys(response, Object.keys(responseDef.properties), "generate_response"); assertString(response.text, "generate_response.text"); assertString(response.model, "generate_response.model"); assertString(response.provider, "generate_response.provider"); if (Object.hasOwn(response, "tokens_in")) assertInteger(response.tokens_in, "generate_response.tokens_in"); if (Object.hasOwn(response, "tokens_out")) assertInteger(response.tokens_out, "generate_response.tokens_out"); if (Object.hasOwn(response, "cost_usd")) assertNumber(response.cost_usd, "generate_response.cost_usd"); if (Object.hasOwn(response, "inference_geo")) { assert.ok( response.inference_geo === null || typeof response.inference_geo === "string", "generate_response.inference_geo must be string or null", ); } if (Object.hasOwn(response, "structured")) assertNullableObject(response.structured, "generate_response.structured"); } function parseSseFrame(frame) { assertString(frame, "stream_frame"); let event = null; const dataLines = []; for (const line of frame.split(/\r?\n/)) { if (line === "") continue; if (line.startsWith("event:")) { event = line.slice("event:".length).trim(); } else if (line.startsWith("data:")) { dataLines.push(line.slice("data:".length).trimStart()); } } assert.ok(event, "stream frame must contain an event line"); assert.ok(EXPECTED_EVENTS.includes(event), `unknown stream event: ${event}`); assert.ok(dataLines.length > 0, `stream event ${event} must contain data`); return packetFromEventData(event, dataLines.join("\n")); } function packetFromEventData(event, data) { if (data === "[DONE]") return null; if (event === "token") { let payload; try { payload = JSON.parse(data); } catch { payload = { text: data }; } assertPlainObject(payload, "token payload"); assertString(payload.text, "token payload.text"); assertOnlyKeys(payload, ["text"], "token payload"); return { event, payload }; } let payload; try { payload = JSON.parse(data); } catch (error) { throw new Error(`stream event ${event} must contain JSON data: ${error.message}`); } if (event === "done") { assertPlainObject(payload, "done payload"); assertRequired(payload, ["provider", "model"], "done payload"); assertOnlyKeys(payload, ["provider", "model", "tokens_in", "tokens_out", "cost_usd", "turns"], "done payload"); assertString(payload.provider, "done payload.provider"); assertString(payload.model, "done payload.model"); if (Object.hasOwn(payload, "tokens_in")) assertInteger(payload.tokens_in, "done payload.tokens_in"); if (Object.hasOwn(payload, "tokens_out")) assertInteger(payload.tokens_out, "done payload.tokens_out"); if (Object.hasOwn(payload, "cost_usd")) assertNumber(payload.cost_usd, "done payload.cost_usd"); if (Object.hasOwn(payload, "turns")) assertInteger(payload.turns, "done payload.turns"); return { event, payload }; } assert.equal(event, "error"); assertPlainObject(payload, "error payload"); assertRequired(payload, ["detail"], "error payload"); assertOnlyKeys(payload, ["detail"], "error payload"); assertString(payload.detail, "error payload.detail"); return { event, payload }; } function assertStreamContract(fixture) { assert.ok(Array.isArray(fixture.stream_frames), "stream_frames must be an array"); assert.ok(Array.isArray(fixture.stream_packets), "stream_packets must be an array"); assert.ok(Array.isArray(fixture.compatibility_lines), "compatibility_lines must be an array"); const decodedPackets = []; for (const frame of fixture.stream_frames) { const packet = parseSseFrame(frame); if (packet) decodedPackets.push(packet); } assert.deepEqual(decodedPackets, fixture.stream_packets); for (const line of fixture.compatibility_lines) { assertString(line, "compatibility line"); if (line.trim() === "data: [DONE]") continue; throw new Error(`unsupported compatibility line: ${line}`); } } function validateContract(schema, fixture) { assertRootShape(schema, fixture); assertGenerateRequest(schema, fixture.generate_request); assertGenerateResponse(schema, fixture.generate_response); assertStreamContract(fixture); return { version: fixture.version, streamEvents: schema["x-engine-gateway-sse-events"], streamFrameCount: fixture.stream_frames.length, decodedPacketCount: fixture.stream_packets.length, compatibilityLineCount: fixture.compatibility_lines.length, }; } function main() { const args = parseArgs(process.argv.slice(2)); const schema = readJson(args.schema); const fixture = readJson(args.fixture); const result = validateContract(schema, fixture); if (args.json) { console.log(JSON.stringify({ ok: true, ...result }, null, 2)); } else { console.log( `engine_gateway_contract.v${result.version} OK: ${result.decodedPacketCount} packets, ${result.compatibilityLineCount} compatibility lines`, ); } } try { main(); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); }