143 行
4.2 KiB
JavaScript
143 行
4.2 KiB
JavaScript
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" });
|
|
});
|
|
});
|