import { expect } from "chai"; import { runAuditLowConfidenceJob } from "../../src/jobs/audit-low-confidence.js"; /** * Stubs `globalThis.fetch` directly (this worker's own `api-client.ts` is * a thin wrapper around it) — same convention `apps/api`'s * `the-meal-db.ts` test uses for the same reason: no real network call, * no mocking library needed for a single-function dependency. */ function stubFetch(responses: Record): { url: string; body: unknown }[] { const calls: { url: string; body: unknown }[] = []; globalThis.fetch = (async (url: string | URL, init?: RequestInit) => { const href = String(url); const body = typeof init?.body === "string" ? JSON.parse(init.body) : undefined; calls.push({ url: href, body }); for (const [pathFragment, response] of Object.entries(responses)) { if (href.includes(pathFragment)) { return new Response(JSON.stringify(response), { status: 200 }); } } throw new Error(`stubFetch: no response configured for ${href}`); }) as typeof fetch; return calls; } describe("runAuditLowConfidenceJob", () => { const originalFetch = globalThis.fetch; afterEach(() => { globalThis.fetch = originalFetch; }); it("proposes a suggestion only when the LLM disagrees with the NLP anchor", async () => { const calls = stubFetch({ "audit-batch": [ { stepId: 1, recipeId: 1, clauseText: "jusqu'à ce que ce soit doré", anchorKey: "fry", intentKey: null, score: 0.5, locale: "fr", }, { stepId: 2, recipeId: 1, clauseText: "laisser reposer un instant", anchorKey: "rest", intentKey: "rest", score: 0.6, locale: "fr", }, ], "training-suggestions": { created: 1 }, }); const llm = { judgeClause: async (text: string) => (text.includes("doré") ? "roast" : "rest"), }; const count = await runAuditLowConfidenceJob(llm, { locale: "fr", limit: 10 }); expect(count).to.equal(1); const postCall = calls.find((call) => call.url.includes("training-suggestions")); if (!postCall) throw new Error("expected a POST to training-suggestions"); expect(postCall.body).to.deep.equal({ suggestions: [ { techStepKey: "roast", locale: "fr", suggestedSynonyms: [], suggestedUtterances: ["jusqu'à ce que ce soit doré"], sourceType: "llm_audit", }, ], }); }); it("posts nothing when the LLM agrees with the anchor or has no opinion", async () => { const calls = stubFetch({ "audit-batch": [ { stepId: 1, recipeId: 1, clauseText: "agrees", anchorKey: "cook", intentKey: "cook", score: 0.5, locale: "fr", }, { stepId: 2, recipeId: 1, clauseText: "no opinion", anchorKey: "boil", intentKey: null, score: 0.4, locale: "fr", }, ], }); const llm = { judgeClause: async (text: string) => (text === "agrees" ? "cook" : null), }; const count = await runAuditLowConfidenceJob(llm, { locale: "fr", limit: 10 }); expect(count).to.equal(0); expect(calls.some((call) => call.url.includes("training-suggestions"))).to.equal(false); }); it("passes locale/limit through to GET /internal/tech-steps/audit-batch", async () => { const calls = stubFetch({ "audit-batch": [] }); const llm = { judgeClause: async () => null }; await runAuditLowConfidenceJob(llm, { locale: "en", limit: 7 }); const getCall = calls.find((call) => call.url.includes("audit-batch")); if (!getCall) throw new Error("expected a GET to audit-batch"); expect(getCall.url).to.include("locale=en"); expect(getCall.url).to.include("limit=7"); }); });