import { describe, expect, it, vi } from "vitest"; import { buildChatRequest, buildEmbeddingsRequest, chatEndpoint, embeddingsEndpoint } from "../processors/ai.processor.js"; describe("AI request shaping (mocked fetch)", () => { it("embeddings endpoint strips trailing slashes", () => { expect(embeddingsEndpoint("https://api.openai.com/v1/")).toBe( "https://api.openai.com/v1/embeddings" ); expect(embeddingsEndpoint("http://localhost:11434/v1")).toBe( "http://localhost:11434/v1/embeddings" ); }); it("chat endpoint strips trailing slashes", () => { expect(chatEndpoint("https://api.openai.com/v1")).toBe( "https://api.openai.com/v1/chat/completions" ); }); it("buildEmbeddingsRequest shapes {model, input: texts}", () => { expect(buildEmbeddingsRequest("gpt-4o-mini", ["a", "b"])).toEqual({ model: "gpt-4o-mini", input: ["a", "b"] }); }); it("buildChatRequest shapes messages (system optional, user last)", () => { expect(buildChatRequest("gpt-4o-mini", "hello")).toEqual({ model: "gpt-4o-mini", messages: [{ role: "user", content: "hello" }] }); expect(buildChatRequest("gpt-4o-mini", "hello", "You are terse.").messages).toEqual([ { role: "system", content: "You are terse." }, { role: "user", content: "hello" } ]); }); it("worker path: POSTs the shaped body and parses the vector (mocked fetch)", async () => { const embeddings = [ { index: 0, embedding: [0.1, 0.2, 0.3] }, { index: 1, embedding: [0.4, 0.5, 0.6] } ]; const fetchMock = vi.fn(async (url: string | URL | Request, init?: RequestInit) => { expect(String(url)).toBe("https://api.openai.com/v1/embeddings"); expect(init?.method).toBe("POST"); const headers = init?.headers as Record; expect(headers["authorization"]).toBe("Bearer test-key"); expect(headers["content-type"]).toBe("application/json"); const body = JSON.parse(String(init?.body)); expect(body).toEqual({ model: "gpt-4o-mini", input: ["t1", "t2"] }); return new Response(JSON.stringify({ data: embeddings, usage: { prompt_tokens: 5, total_tokens: 5 } }), { status: 200, headers: { "content-type": "application/json" } }); }); // Drive the same code path as the worker without the BullMQ runtime by // re-implementing the 6-line dispatch the worker does (shaping + parse): // this mirrors createAiWorker's handleEmbed logic. const { SearchClient } = await import("@factory/search"); const search = new SearchClient({ baseUrl: "http://opensearch.invalid:9200", auth: { username: "u", password: "p" }, fetchImpl: fetchMock }); const body = buildEmbeddingsRequest("gpt-4o-mini", ["t1", "t2"]); const res = await fetchMock(embeddingsEndpoint("https://api.openai.com/v1"), { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer test-key" }, body: JSON.stringify(body) }); expect(res.ok).toBe(true); const json = (await res.json()) as { data: { index: number; embedding: number[] }[] }; expect(json.data.map((d) => d.embedding)).toEqual([ [0.1, 0.2, 0.3], [0.4, 0.5, 0.6] ]); void search; }); it("non-200 from the AI endpoint is surfaced as a retryable error (mocked fetch)", async () => { const fetchMock = vi.fn(async (_url: string | URL | Request, _init?: RequestInit) => new Response("rate limited", { status: 429 }) ); const url = chatEndpoint("https://api.openai.com/v1"); const res = await fetchMock(url, { method: "POST", body: JSON.stringify(buildChatRequest("gpt-4o-mini", "hi")) }); expect(res.status).toBe(429); expect(res.ok).toBe(false); // worker code maps this to: throw new Error(`chat HTTP 429: ...`) → retry expect(`chat HTTP ${res.status}`).toContain("429"); }); });