const { AiProviderError, buildAiProvider, __testables } = require("../../../src/services/ai/provider"); describe("AI provider service", () => { const originalFetch = global.fetch; afterEach(() => { global.fetch = originalFetch; vi.restoreAllMocks(); }); it("preserves base path for openai-compatible chat completions endpoint", () => { expect(__testables.resolveChatCompletionsUrl("https://example.com/v1")).toBe( "https://example.com/v1/chat/completions", ); expect(__testables.resolveChatCompletionsUrl("https://example.com/v1/")).toBe( "https://example.com/v1/chat/completions", ); expect(__testables.resolveChatCompletionsUrl("https://example.com/chat/completions")).toBe( "https://example.com/chat/completions", ); }); it("returns deterministic mock suggestion payload", async () => { const provider = buildAiProvider({ aiProvider: "mock" }); const result = await provider.generateSuggestion({ targetType: "reimbursement", snapshot: { targetId: "r-1", formNumber: "BX-001", status: "reviewing", totalAmount: "123.00", itemCount: 2, budgetHint: { exceedsBudget: false, exceedsThreshold: true }, invoiceSummary: { abnormalCount: 1 }, }, promptSpec: { systemPrompt: "system", userPrompt: "user" }, }); expect(result.provider).toBe("mock"); expect(result.suggestion.summary).toContain("BX-001"); expect(result.suggestion.riskHints.length).toBeGreaterThan(0); }); it("maps http 4xx failures to upstream_4xx", async () => { global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 400, json: vi.fn().mockResolvedValue({ error: "bad_request" }), }); const provider = buildAiProvider({ aiProvider: "http", aiBaseUrl: "https://example.com/v1", aiModel: "gpt-test", aiTimeoutMs: 500, }); await expect( provider.generateSuggestion({ targetType: "reimbursement", snapshot: {}, promptSpec: { systemPrompt: "system", userPrompt: "user" }, }), ).rejects.toMatchObject({ code: "upstream_4xx" }); }); it("maps http 5xx failures to upstream_5xx", async () => { global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 503, json: vi.fn().mockResolvedValue({ error: "service_unavailable" }), }); const provider = buildAiProvider({ aiProvider: "http", aiBaseUrl: "https://example.com/v1", aiModel: "gpt-test", aiTimeoutMs: 500, }); await expect( provider.generateSuggestion({ targetType: "purchase_request", snapshot: {}, promptSpec: { systemPrompt: "system", userPrompt: "user" }, }), ).rejects.toMatchObject({ code: "upstream_5xx" }); }); it("maps aborted requests to timeout", async () => { global.fetch = vi.fn().mockRejectedValue({ name: "AbortError" }); const provider = buildAiProvider({ aiProvider: "http", aiBaseUrl: "https://example.com/v1", aiModel: "gpt-test", aiTimeoutMs: 500, }); await expect( provider.generateSuggestion({ targetType: "asset_card", snapshot: {}, promptSpec: { systemPrompt: "system", userPrompt: "user" }, }), ).rejects.toMatchObject({ code: "timeout" }); }); it("rejects invalid json payload from upstream", async () => { global.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, json: vi.fn().mockResolvedValue({ choices: [{ message: { content: "```json\nnot-valid-json\n```" } }], usage: {}, }), }); const provider = buildAiProvider({ aiProvider: "http", aiBaseUrl: "https://example.com/v1", aiModel: "gpt-test", aiTimeoutMs: 500, }); await expect( provider.generateSuggestion({ targetType: "asset_card", snapshot: {}, promptSpec: { systemPrompt: "system", userPrompt: "user" }, }), ).rejects.toBeInstanceOf(AiProviderError); await expect( provider.generateSuggestion({ targetType: "asset_card", snapshot: {}, promptSpec: { systemPrompt: "system", userPrompt: "user" }, }), ).rejects.toMatchObject({ code: "invalid_response" }); }); });