128 lines
4.6 KiB
TypeScript
128 lines
4.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import {
|
|
invokeReferentialIa,
|
|
testReferentialIaConnection,
|
|
type ReferentielIaConnection,
|
|
} from "./referentialIa";
|
|
import { readSecretFile, resolveServerSecret } from "./_core/env";
|
|
|
|
const connection: ReferentielIaConnection = {
|
|
baseUrl: "https://referentiel-ia.recette.santinova-soft.org",
|
|
token: "test-token",
|
|
};
|
|
|
|
function jsonResponse(body: unknown, status = 200) {
|
|
return new Response(JSON.stringify(body), {
|
|
status,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
|
|
describe("Référentiel IA", () => {
|
|
it("lit un jeton serveur depuis un fichier monté sans le journaliser", () => {
|
|
const directory = mkdtempSync(join(tmpdir(), "referentiel-ia-"));
|
|
const secretFile = join(directory, "token");
|
|
try {
|
|
writeFileSync(secretFile, " server-only-token\n", { mode: 0o600 });
|
|
expect(readSecretFile(secretFile)).toBe("server-only-token");
|
|
expect(readSecretFile(join(directory, "absent"))).toBe("");
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("utilise le secret monté lorsque la variable d'environnement est vide", () => {
|
|
const directory = mkdtempSync(join(tmpdir(), "referentiel-ia-"));
|
|
const secretFile = join(directory, "token");
|
|
try {
|
|
writeFileSync(secretFile, " mounted-secret\n", { mode: 0o600 });
|
|
|
|
expect(resolveServerSecret("", secretFile)).toBe("mounted-secret");
|
|
expect(resolveServerSecret(" ", secretFile)).toBe("mounted-secret");
|
|
expect(resolveServerSecret("environment-secret", secretFile)).toBe("environment-secret");
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("transmet le slug du moteur décidé avant la requête OpenAI-compatible", async () => {
|
|
let receivedUrl = "";
|
|
let receivedInit: RequestInit | undefined;
|
|
let callCount = 0;
|
|
const result = await invokeReferentialIa(
|
|
{
|
|
messages: [{ role: "user", content: "Extrais la facture" }],
|
|
max_tokens: 1200,
|
|
},
|
|
connection,
|
|
async (url, init) => {
|
|
callCount += 1;
|
|
receivedUrl = String(url);
|
|
receivedInit = init;
|
|
if (callCount === 1) {
|
|
expect(receivedUrl).toBe("https://referentiel-ia.recette.santinova-soft.org/api/integration/v1/recommendations");
|
|
expect(JSON.parse(String(receivedInit?.body))).toEqual({
|
|
useCase: "texte",
|
|
optimizeCost: true,
|
|
needsFreeTier: false,
|
|
includeConnection: false,
|
|
});
|
|
return jsonResponse({
|
|
application: { name: "Démat. Facturation DSI" },
|
|
decision: { selected: { engine: { name: "SANTINO Expert", slug: "santino-expert" } } },
|
|
});
|
|
}
|
|
return jsonResponse({
|
|
id: "ref-1",
|
|
created: 1,
|
|
model: "selected-by-reference",
|
|
choices: [{ index: 0, message: { role: "assistant", content: "{}" }, finish_reason: "stop" }],
|
|
});
|
|
},
|
|
);
|
|
|
|
expect(receivedUrl).toBe("https://referentiel-ia.recette.santinova-soft.org/api/integration/v1/invoke");
|
|
expect(receivedInit?.headers).toMatchObject({ authorization: "Bearer test-token" });
|
|
expect(JSON.parse(String(receivedInit?.body))).toMatchObject({
|
|
engineSlug: "santino-expert",
|
|
useCase: "texte",
|
|
payload: {
|
|
messages: [{ role: "user", content: "Extrais la facture" }],
|
|
max_tokens: 1200,
|
|
stream: false,
|
|
},
|
|
});
|
|
expect(callCount).toBe(2);
|
|
expect(result.choices[0]?.message.content).toBe("{}");
|
|
});
|
|
|
|
it("retourne un diagnostic non sensible lorsqu'aucun jeton n'est configuré", async () => {
|
|
await expect(
|
|
invokeReferentialIa({ messages: [{ role: "user", content: "test" }] }, { baseUrl: "", token: "" }),
|
|
).rejects.toThrow("Référentiel IA");
|
|
});
|
|
|
|
it("confirme le moteur choisi sans déclencher d'analyse de facture", async () => {
|
|
const result = await testReferentialIaConnection(
|
|
connection,
|
|
async (_url, init) => {
|
|
expect(JSON.parse(String(init?.body))).toEqual({
|
|
useCase: "texte",
|
|
optimizeCost: true,
|
|
needsFreeTier: false,
|
|
includeConnection: false,
|
|
});
|
|
return jsonResponse({
|
|
application: { name: "Démat. Facturation DSI" },
|
|
decision: { selected: { engine: { name: "Manus Forge", slug: "manus-forge" }, connection: undefined } },
|
|
});
|
|
},
|
|
);
|
|
|
|
expect(result).toEqual({ applicationName: "Démat. Facturation DSI", engineName: "Manus Forge", model: null });
|
|
});
|
|
});
|