import { describe, expect, it } from "vitest"; import { createOpenAICompatiblePiAdapter, } from "../src/index.js"; describe("pi-adapter structured errors", () => { it("returns ok:false with no_content when LLM returns empty", async () => { const server = await startMockLLM({ responseContent: "" }); try { const pi = createOpenAICompatiblePiAdapter({ baseUrl: server.baseUrl, apiKey: "test", defaultModel: "fast", }); const result = await pi.chat({ message: "hi", model: "fast", docs: [], availableActions: [], }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.kind).toBe("no_content"); expect(result.fallback.answer).toBe(""); expect(result.fallback.recommended_actions).toEqual([]); } } finally { await server.stop(); } }); it("returns ok:false with json_parse when response has no JSON object", async () => { const server = await startMockLLM({ responseContent: "Just plain text answer" }); try { const pi = createOpenAICompatiblePiAdapter({ baseUrl: server.baseUrl, apiKey: "test", defaultModel: "fast", }); const result = await pi.chat({ message: "hi", model: "fast", docs: [], availableActions: [], }); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.kind).toBe("json_parse"); expect(result.fallback.answer).toBe("Just plain text answer"); } } finally { await server.stop(); } }); it("returns ok:true when response is well-formed JSON", async () => { const server = await startMockLLM({ responseContent: JSON.stringify({ answer: "All good", recommended_actions: [], internal_docs: [], }), }); try { const pi = createOpenAICompatiblePiAdapter({ baseUrl: server.baseUrl, apiKey: "test", defaultModel: "fast", }); const result = await pi.chat({ message: "hi", model: "fast", docs: [], availableActions: [], }); expect(result.ok).toBe(true); if (result.ok) { expect(result.result.answer).toBe("All good"); } } finally { await server.stop(); } }); it("throws on non-OK HTTP response (transport error, not parse error)", async () => { const server = await startMockLLM({ status: 500, responseContent: "" }); try { const pi = createOpenAICompatiblePiAdapter({ baseUrl: server.baseUrl, apiKey: "test", defaultModel: "fast", }); await expect( pi.chat({ message: "hi", model: "fast", docs: [], availableActions: [], }), ).rejects.toThrow(/llm_request_failed:500/); } finally { await server.stop(); } }); it("filters recommended_actions to known ids and clamps scores", async () => { const server = await startMockLLM({ responseContent: JSON.stringify({ answer: "ok", recommended_actions: [ { type: "webhook", id: "dns-flush", confidence: 2.5, reason: "x" }, { type: "webhook", id: "unknown-id", confidence: 0.9, reason: "y" }, ], internal_docs: [], }), }); try { const pi = createOpenAICompatiblePiAdapter({ baseUrl: server.baseUrl, apiKey: "test", defaultModel: "fast", }); const result = await pi.chat({ message: "hi", model: "fast", docs: [], availableActions: [ { type: "webhook", id: "dns-flush", confidence: 0, reason: "r", requires_confirmation: true }, ], }); expect(result.ok).toBe(true); if (result.ok) { expect(result.result.recommended_actions).toHaveLength(1); expect(result.result.recommended_actions[0]?.id).toBe("dns-flush"); expect(result.result.recommended_actions[0]?.confidence).toBe(1); } } finally { await server.stop(); } }); }); import { createServer, type Server } from "node:http"; async function startMockLLM(opts: { responseContent: string; status?: number }): Promise<{ baseUrl: string; stop: () => Promise; }> { let s: Server; return await new Promise((resolve) => { s = createServer((_req, res) => { res.writeHead(opts.status ?? 200, { "content-type": "application/json" }); res.end( JSON.stringify({ id: "mock", object: "chat.completion", created: 0, model: "fast", choices: [ { index: 0, message: { role: "assistant", content: opts.responseContent }, finish_reason: "stop", }, ], }), ); }); s.listen(0, "127.0.0.1", () => { const address = s.address(); const port = typeof address === "object" && address ? address.port : 0; resolve({ baseUrl: `http://127.0.0.1:${port}/v1`, stop: () => new Promise((res) => { s!.close(() => res()); }), }); }); }); }