import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { HISTORY_CONTEXT_MARKER } from "../auto-reply/reply/history.js"; import { CURRENT_MESSAGE_MARKER } from "../auto-reply/reply/mentions.js"; import { emitAgentEvent } from "../infra/agent-events.js"; import { agentCommand, getFreePort, installGatewayTestHooks } from "./test-helpers.js"; installGatewayTestHooks({ scope: "suite" }); let enabledServer: Awaited>; let enabledPort: number; beforeAll(async () => { enabledPort = await getFreePort(); enabledServer = await startServer(enabledPort); }); afterAll(async () => { await enabledServer.close({ reason: "openai http enabled suite done" }); }); async function startServerWithDefaultConfig(port: number) { const { startGatewayServer } = await import("./server.js"); return await startGatewayServer(port, { host: "127.0.0.1", auth: { mode: "token", token: "secret" }, controlUiEnabled: false, openAiChatCompletionsEnabled: false, }); } async function startServer(port: number, opts?: { openAiChatCompletionsEnabled?: boolean }) { const { startGatewayServer } = await import("./server.js"); return await startGatewayServer(port, { host: "127.0.0.1", auth: { mode: "token", token: "secret" }, controlUiEnabled: false, openAiChatCompletionsEnabled: opts?.openAiChatCompletionsEnabled ?? true, }); } async function postChatCompletions(port: number, body: unknown, headers?: Record) { const res = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer secret", ...headers, }, body: JSON.stringify(body), }); return res; } function parseSseDataLines(text: string): string[] { return text .split("\n") .map((line) => line.trim()) .filter((line) => line.startsWith("data: ")) .map((line) => line.slice("data: ".length)); } describe("OpenAI-compatible HTTP API (e2e)", () => { it("rejects when disabled (default + config)", { timeout: 120_000 }, async () => { { const port = await getFreePort(); const server = await startServerWithDefaultConfig(port); try { const res = await postChatCompletions(port, { model: "clawdbot", messages: [{ role: "user", content: "hi" }], }); expect(res.status).toBe(404); } finally { await server.close({ reason: "test done" }); } } { const port = await getFreePort(); const server = await startServer(port, { openAiChatCompletionsEnabled: false, }); try { const res = await postChatCompletions(port, { model: "clawdbot", messages: [{ role: "user", content: "hi" }], }); expect(res.status).toBe(404); } finally { await server.close({ reason: "test done" }); } } }); it("handles request validation and routing", async () => { const port = enabledPort; const mockAgentOnce = (payloads: Array<{ text: string }>) => { agentCommand.mockReset(); agentCommand.mockResolvedValueOnce({ payloads } as never); }; try { { const res = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { method: "GET", headers: { authorization: "Bearer secret" }, }); expect(res.status).toBe(405); await res.text(); } { const res = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ messages: [{ role: "user", content: "hi" }] }), }); expect(res.status).toBe(401); await res.text(); } { mockAgentOnce([{ text: "hello" }]); const res = await postChatCompletions( port, { model: "clawdbot", messages: [{ role: "user", content: "hi" }] }, { "x-clawdbot-agent-id": "beta" }, ); expect(res.status).toBe(200); expect(agentCommand).toHaveBeenCalledTimes(1); const [opts] = agentCommand.mock.calls[0] ?? []; expect((opts as { sessionKey?: string } | undefined)?.sessionKey ?? "").toMatch( /^agent:beta:/, ); await res.text(); } { mockAgentOnce([{ text: "hello" }]); const res = await postChatCompletions(port, { model: "clawdbot:beta", messages: [{ role: "user", content: "hi" }], }); expect(res.status).toBe(200); expect(agentCommand).toHaveBeenCalledTimes(1); const [opts] = agentCommand.mock.calls[0] ?? []; expect((opts as { sessionKey?: string } | undefined)?.sessionKey ?? "").toMatch( /^agent:beta:/, ); await res.text(); } { mockAgentOnce([{ text: "hello" }]); const res = await postChatCompletions( port, { model: "clawdbot:beta", messages: [{ role: "user", content: "hi" }], }, { "x-clawdbot-agent-id": "alpha" }, ); expect(res.status).toBe(200); expect(agentCommand).toHaveBeenCalledTimes(1); const [opts] = agentCommand.mock.calls[0] ?? []; expect((opts as { sessionKey?: string } | undefined)?.sessionKey ?? "").toMatch( /^agent:alpha:/, ); await res.text(); } { mockAgentOnce([{ text: "hello" }]); const res = await postChatCompletions( port, { model: "clawdbot", messages: [{ role: "user", content: "hi" }] }, { "x-clawdbot-agent-id": "beta", "x-clawdbot-session-key": "agent:beta:openai:custom", }, ); expect(res.status).toBe(200); const [opts] = agentCommand.mock.calls[0] ?? []; expect((opts as { sessionKey?: string } | undefined)?.sessionKey).toBe( "agent:beta:openai:custom", ); await res.text(); } { mockAgentOnce([{ text: "hello" }]); const res = await postChatCompletions(port, { user: "alice", model: "clawdbot", messages: [{ role: "user", content: "hi" }], }); expect(res.status).toBe(200); const [opts] = agentCommand.mock.calls[0] ?? []; expect((opts as { sessionKey?: string } | undefined)?.sessionKey ?? "").toContain( "openai-user:alice", ); await res.text(); } { mockAgentOnce([{ text: "hello" }]); const res = await postChatCompletions(port, { model: "clawdbot", messages: [ { role: "user", content: [ { type: "text", text: "hello" }, { type: "input_text", text: "world" }, ], }, ], }); expect(res.status).toBe(200); const [opts] = agentCommand.mock.calls[0] ?? []; expect((opts as { message?: string } | undefined)?.message).toBe("hello\nworld"); await res.text(); } { mockAgentOnce([{ text: "I am Claude" }]); const res = await postChatCompletions(port, { model: "clawdbot", messages: [ { role: "system", content: "You are a helpful assistant." }, { role: "user", content: "Hello, who are you?" }, { role: "assistant", content: "I am Claude." }, { role: "user", content: "What did I just ask you?" }, ], }); expect(res.status).toBe(200); const [opts] = agentCommand.mock.calls[0] ?? []; const message = (opts as { message?: string } | undefined)?.message ?? ""; expect(message).toContain(HISTORY_CONTEXT_MARKER); expect(message).toContain("User: Hello, who are you?"); expect(message).toContain("Assistant: I am Claude."); expect(message).toContain(CURRENT_MESSAGE_MARKER); expect(message).toContain("User: What did I just ask you?"); await res.text(); } { mockAgentOnce([{ text: "hello" }]); const res = await postChatCompletions(port, { model: "clawdbot", messages: [ { role: "system", content: "You are a helpful assistant." }, { role: "user", content: "Hello" }, ], }); expect(res.status).toBe(200); const [opts] = agentCommand.mock.calls[0] ?? []; const message = (opts as { message?: string } | undefined)?.message ?? ""; expect(message).not.toContain(HISTORY_CONTEXT_MARKER); expect(message).not.toContain(CURRENT_MESSAGE_MARKER); expect(message).toBe("Hello"); await res.text(); } { mockAgentOnce([{ text: "hello" }]); const res = await postChatCompletions(port, { model: "clawdbot", messages: [ { role: "developer", content: "You are a helpful assistant." }, { role: "user", content: "Hello" }, ], }); expect(res.status).toBe(200); const [opts] = agentCommand.mock.calls[0] ?? []; const extraSystemPrompt = (opts as { extraSystemPrompt?: string } | undefined)?.extraSystemPrompt ?? ""; expect(extraSystemPrompt).toBe("You are a helpful assistant."); await res.text(); } { mockAgentOnce([{ text: "ok" }]); const res = await postChatCompletions(port, { model: "clawdbot", messages: [ { role: "system", content: "You are a helpful assistant." }, { role: "user", content: "What's the weather?" }, { role: "assistant", content: "Checking the weather." }, { role: "tool", content: "Sunny, 70F." }, ], }); expect(res.status).toBe(200); const [opts] = agentCommand.mock.calls[0] ?? []; const message = (opts as { message?: string } | undefined)?.message ?? ""; expect(message).toContain(HISTORY_CONTEXT_MARKER); expect(message).toContain("User: What's the weather?"); expect(message).toContain("Assistant: Checking the weather."); expect(message).toContain(CURRENT_MESSAGE_MARKER); expect(message).toContain("Tool: Sunny, 70F."); await res.text(); } { mockAgentOnce([{ text: "hello" }]); const res = await postChatCompletions(port, { stream: false, model: "clawdbot", messages: [{ role: "user", content: "hi" }], }); expect(res.status).toBe(200); const json = (await res.json()) as Record; expect(json.object).toBe("chat.completion"); expect(Array.isArray(json.choices)).toBe(true); const choice0 = (json.choices as Array>)[0] ?? {}; const msg = (choice0.message as Record | undefined) ?? {}; expect(msg.role).toBe("assistant"); expect(msg.content).toBe("hello"); } { const res = await postChatCompletions(port, { model: "clawdbot", messages: [{ role: "system", content: "yo" }], }); expect(res.status).toBe(400); const missingUserJson = (await res.json()) as Record; expect((missingUserJson.error as Record | undefined)?.type).toBe( "invalid_request_error", ); } } finally { // shared server } }); it("streams SSE chunks when stream=true", async () => { const port = enabledPort; try { { agentCommand.mockReset(); agentCommand.mockImplementationOnce(async (opts: unknown) => { const runId = (opts as { runId?: string } | undefined)?.runId ?? ""; emitAgentEvent({ runId, stream: "assistant", data: { delta: "he" } }); emitAgentEvent({ runId, stream: "assistant", data: { delta: "llo" } }); return { payloads: [{ text: "hello" }] } as never; }); const res = await postChatCompletions(port, { stream: true, model: "clawdbot", messages: [{ role: "user", content: "hi" }], }); expect(res.status).toBe(200); expect(res.headers.get("content-type") ?? "").toContain("text/event-stream"); const text = await res.text(); const data = parseSseDataLines(text); expect(data[data.length - 1]).toBe("[DONE]"); const jsonChunks = data .filter((d) => d !== "[DONE]") .map((d) => JSON.parse(d) as Record); expect(jsonChunks.some((c) => c.object === "chat.completion.chunk")).toBe(true); const allContent = jsonChunks .flatMap((c) => (c.choices as Array> | undefined) ?? []) .map((choice) => (choice.delta as Record | undefined)?.content) .filter((v): v is string => typeof v === "string") .join(""); expect(allContent).toBe("hello"); } { agentCommand.mockReset(); agentCommand.mockImplementationOnce(async (opts: unknown) => { const runId = (opts as { runId?: string } | undefined)?.runId ?? ""; emitAgentEvent({ runId, stream: "assistant", data: { delta: "hi" } }); emitAgentEvent({ runId, stream: "assistant", data: { delta: "hi" } }); return { payloads: [{ text: "hihi" }] } as never; }); const repeatedRes = await postChatCompletions(port, { stream: true, model: "clawdbot", messages: [{ role: "user", content: "hi" }], }); expect(repeatedRes.status).toBe(200); const repeatedText = await repeatedRes.text(); const repeatedData = parseSseDataLines(repeatedText); const repeatedChunks = repeatedData .filter((d) => d !== "[DONE]") .map((d) => JSON.parse(d) as Record); const repeatedContent = repeatedChunks .flatMap((c) => (c.choices as Array> | undefined) ?? []) .map((choice) => (choice.delta as Record | undefined)?.content) .filter((v): v is string => typeof v === "string") .join(""); expect(repeatedContent).toBe("hihi"); } { agentCommand.mockReset(); agentCommand.mockResolvedValueOnce({ payloads: [{ text: "hello" }], } as never); const fallbackRes = await postChatCompletions(port, { stream: true, model: "clawdbot", messages: [{ role: "user", content: "hi" }], }); expect(fallbackRes.status).toBe(200); const fallbackText = await fallbackRes.text(); expect(fallbackText).toContain("[DONE]"); expect(fallbackText).toContain("hello"); } } finally { // shared server } }); });