/** * Deterministic mock of an OpenAI-compatible /v1/chat/completions endpoint that * speaks streaming SSE, for end-to-end testing the openai-compat backend without * a real model. tsx-runnable: * * PORT=11500 tsx scripts/mock-openai.ts * * Scripting (decided per request from the message history it receives): * - If the latest user message contains "ESCAPE", the first assistant turn * emits a write_file tool_call with path "../escape.md" (vault-escape probe). * - Otherwise the first assistant turn streams some text and a write_file * tool_call for projects/mock/mock.md (arguments split across fragments, and * id/name only on the first fragment, to exercise streamed-tool_call parsing). * - Once the history's last message is a tool result, it streams a final text * and finishes with finish_reason "stop". * * It logs a compact summary of every request's message roles to stderr so an * e2e harness can assert that a second turn replays prior history. */ import http from "node:http"; const PORT = Number(process.env.PORT ?? 11500); interface InMsg { role: string; content: string | null; tool_calls?: unknown[]; tool_call_id?: string; } function sseChunk(obj: unknown): string { return `data: ${JSON.stringify(obj)}\n\n`; } function baseDelta(delta: unknown, finish: string | null = null) { return { id: "chatcmpl-mock", object: "chat.completion.chunk", created: Math.floor(Date.now() / 1000), model: "mock-model", choices: [{ index: 0, delta, finish_reason: finish }], }; } const server = http.createServer((req, res) => { if (req.method !== "POST" || !req.url?.includes("/chat/completions")) { res.writeHead(404).end("not found"); return; } let body = ""; req.on("data", (c) => (body += c)); req.on("end", () => { let messages: InMsg[] = []; try { messages = (JSON.parse(body).messages ?? []) as InMsg[]; } catch { /* ignore */ } const roles = messages.map((m) => m.role).join(","); const userMsgs = messages.filter((m) => m.role === "user").length; process.stderr.write(`[mock] request roles=[${roles}] userMessages=${userMsgs}\n`); const last = messages[messages.length - 1]; const lastUser = [...messages].reverse().find((m) => m.role === "user"); const wantEscape = typeof lastUser?.content === "string" && lastUser.content.includes("ESCAPE"); res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", }); if (last?.role === "tool") { // Second iteration: finish the turn. res.write(sseChunk(baseDelta({ role: "assistant", content: "" }))); res.write(sseChunk(baseDelta({ content: "Done — I captured that into the vault. " }))); res.write(sseChunk(baseDelta({ content: "What else should we add? (1) goals? (2) stack?" }))); res.write(sseChunk(baseDelta({}, "stop"))); res.write("data: [DONE]\n\n"); res.end(); return; } // First iteration: stream text + a write_file tool_call across fragments. const targetPath = wantEscape ? "../escape.md" : "projects/mock/mock.md"; const fileContent = "---\ntags:\n - mock\n - mock/moc\n---\n\n# Mock\n\nCaptured by the mock backend.\n\n## TODO / Ideas\n\n\n"; const argsFull = JSON.stringify({ path: targetPath, content: fileContent }); const mid = Math.floor(argsFull.length / 2); res.write(sseChunk(baseDelta({ role: "assistant", content: "" }))); res.write(sseChunk(baseDelta({ content: "Got it. Let me start the note. " }))); // tool_call fragment 1: id + name + first half of arguments. res.write( sseChunk( baseDelta({ tool_calls: [ { index: 0, id: "call_mock_1", type: "function", function: { name: "write_file", arguments: argsFull.slice(0, mid) } }, ], }) ) ); // tool_call fragment 2: remaining arguments only (no id/name). res.write( sseChunk( baseDelta({ tool_calls: [{ index: 0, function: { arguments: argsFull.slice(mid) } }], }) ) ); res.write(sseChunk(baseDelta({}, "tool_calls"))); res.write("data: [DONE]\n\n"); res.end(); }); }); server.listen(PORT, () => { process.stderr.write(`[mock] listening on http://localhost:${PORT}\n`); });