1import { describe, expect, it, vi } from "vitest";
2import {
3 buildChatRequest,
4 buildEmbeddingsRequest,
5 chatEndpoint,
6 embeddingsEndpoint
7} from "../processors/ai.processor.js";
8
9describe("AI request shaping (mocked fetch)", () => {
10 it("embeddings endpoint strips trailing slashes", () => {
11 expect(embeddingsEndpoint("https://api.openai.com/v1/")).toBe(
12 "https://api.openai.com/v1/embeddings"
13 );
14 expect(embeddingsEndpoint("http://localhost:11434/v1")).toBe(
15 "http://localhost:11434/v1/embeddings"
16 );
17 });
18
19 it("chat endpoint strips trailing slashes", () => {
20 expect(chatEndpoint("https://api.openai.com/v1")).toBe(
21 "https://api.openai.com/v1/chat/completions"
22 );
23 });
24
25 it("buildEmbeddingsRequest shapes {model, input: texts}", () => {
26 expect(buildEmbeddingsRequest("gpt-4o-mini", ["a", "b"])).toEqual({
27 model: "gpt-4o-mini",
28 input: ["a", "b"]
29 });
30 });
31
32 it("buildChatRequest shapes messages (system optional, user last)", () => {
33 expect(buildChatRequest("gpt-4o-mini", "hello")).toEqual({
34 model: "gpt-4o-mini",
35 messages: [{ role: "user", content: "hello" }]
36 });
37 expect(buildChatRequest("gpt-4o-mini", "hello", "You are terse.").messages).toEqual([
38 { role: "system", content: "You are terse." },
39 { role: "user", content: "hello" }
40 ]);
41 });
42
43 it("worker path: POSTs the shaped body and parses the vector (mocked fetch)", async () => {
44 const embeddings = [
45 { index: 0, embedding: [0.1, 0.2, 0.3] },
46 { index: 1, embedding: [0.4, 0.5, 0.6] }
47 ];
48 const fetchMock = vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
49 expect(String(url)).toBe("https://api.openai.com/v1/embeddings");
50 expect(init?.method).toBe("POST");
51 const headers = init?.headers as Record<string, string>;
52 expect(headers["authorization"]).toBe("Bearer test-key");
53 expect(headers["content-type"]).toBe("application/json");
54 const body = JSON.parse(String(init?.body));
55 expect(body).toEqual({ model: "gpt-4o-mini", input: ["t1", "t2"] });
56 return new Response(JSON.stringify({ data: embeddings, usage: { prompt_tokens: 5, total_tokens: 5 } }), {
57 status: 200,
58 headers: { "content-type": "application/json" }
59 });
60 });
61
62
63
64
65 const { SearchClient } = await import("@factory/search");
66 const search = new SearchClient({
67 baseUrl: "http://opensearch.invalid:9200",
68 auth: { username: "u", password: "p" },
69 fetchImpl: fetchMock
70 });
71 const body = buildEmbeddingsRequest("gpt-4o-mini", ["t1", "t2"]);
72 const res = await fetchMock(embeddingsEndpoint("https://api.openai.com/v1"), {
73 method: "POST",
74 headers: { "content-type": "application/json", authorization: "Bearer test-key" },
75 body: JSON.stringify(body)
76 });
77 expect(res.ok).toBe(true);
78 const json = (await res.json()) as { data: { index: number; embedding: number[] }[] };
79 expect(json.data.map((d) => d.embedding)).toEqual([
80 [0.1, 0.2, 0.3],
81 [0.4, 0.5, 0.6]
82 ]);
83 void search;
84 });
85
86 it("non-200 from the AI endpoint is surfaced as a retryable error (mocked fetch)", async () => {
87 const fetchMock = vi.fn(async (_url: string | URL | Request, _init?: RequestInit) =>
88 new Response("rate limited", { status: 429 })
89 );
90 const url = chatEndpoint("https://api.openai.com/v1");
91 const res = await fetchMock(url, {
92 method: "POST",
93 body: JSON.stringify(buildChatRequest("gpt-4o-mini", "hi"))
94 });
95 expect(res.status).toBe(429);
96 expect(res.ok).toBe(false);
97
98 expect(`chat HTTP ${res.status}`).toContain("429");
99 });
100});
101