test(recipe-generation): hårda allergen-tester där härledda allergener överkör AI
This commit is contained in:
@@ -20,6 +20,8 @@ export interface RecipeCandidate {
|
|||||||
freezerFriendly: boolean;
|
freezerFriendly: boolean;
|
||||||
sourceType: "ai_generated";
|
sourceType: "ai_generated";
|
||||||
confidence: number;
|
confidence: number;
|
||||||
|
/** AI:s påstådda allergener — används aldrig som facit; alltid härledda ur ingredienser. */
|
||||||
|
aiClaimedAllergens?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RecipeCandidateIngredient {
|
export interface RecipeCandidateIngredient {
|
||||||
|
|||||||
@@ -135,7 +135,11 @@ export async function verifyCandidate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── 2. Allergener (härledda, aldrig AI) ──────────────────────────────────
|
// ── 2. Allergener (härledda, aldrig AI) ──────────────────────────────────
|
||||||
const allergens = deriveRecipeAllergens(canonicalIds, safetyInfos);
|
// AI:s eventuella påståenden om allergener ignoreras fullständigt.
|
||||||
|
// Säkerheten härleds alltid deterministiskt ur canonical_ingredients.
|
||||||
|
const aiClaims = candidate.aiClaimedAllergens ?? [];
|
||||||
|
const derivedAllergens = deriveRecipeAllergens(canonicalIds, safetyInfos);
|
||||||
|
const allergens = derivedAllergens;
|
||||||
|
|
||||||
// ── 3. Näring (beräknad via nutrition-engine) ────────────────────────────
|
// ── 3. Näring (beräknad via nutrition-engine) ────────────────────────────
|
||||||
const calcIngredients: RecipeIngredientForCalc[] = candidate.ingredients.map((ing) => ({
|
const calcIngredients: RecipeIngredientForCalc[] = candidate.ingredients.map((ing) => ({
|
||||||
|
|||||||
@@ -153,6 +153,54 @@ describe("verifyCandidate", () => {
|
|||||||
expect(result.allergens).toEqual([]);
|
expect(result.allergens).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("överkör AI:n när AI påstår inga allergener men ingrediens har mjölk", async () => {
|
||||||
|
const milkIngredient: ReturnType<CanonicalIngredientLookup["getById"]> = {
|
||||||
|
id: "milk_3",
|
||||||
|
nutritionPer100: {
|
||||||
|
basis: "per_100_g",
|
||||||
|
values: { kcal: 60, proteinG: 3.4, carbsG: 4.7, fatG: 3, saturatedFatG: 1.9, fiberG: 0, sugarG: 4.7, saltG: 0.1 },
|
||||||
|
},
|
||||||
|
defaultUnit: "DECILITER",
|
||||||
|
densityGPerMl: 1.03,
|
||||||
|
gramsPerPiece: null,
|
||||||
|
allergens: ["milk"],
|
||||||
|
isVegan: false,
|
||||||
|
isVegetarian: true,
|
||||||
|
containsGluten: false,
|
||||||
|
containsLactose: true,
|
||||||
|
isPork: false,
|
||||||
|
isBeef: false,
|
||||||
|
isAlcohol: false,
|
||||||
|
};
|
||||||
|
const lookup: CanonicalIngredientLookup = {
|
||||||
|
getById(id: string) {
|
||||||
|
return id === "milk_3" ? milkIngredient : undefined;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const c = makeCandidate({
|
||||||
|
aiClaimedAllergens: [],
|
||||||
|
ingredients: [
|
||||||
|
{ canonicalIngredientId: "milk_3", displayNameSv: "mjölk", quantity: 5, unit: "DECILITER", optional: false, note: null },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const result = await verifyCandidate(c, lookup, noSimilarity);
|
||||||
|
expect(result.allergens).toContain("milk");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("slänger AI:n:s falska gluten-allergen när ingen ingrediens innehåller gluten", async () => {
|
||||||
|
const c = makeCandidate({
|
||||||
|
aiClaimedAllergens: ["gluten"],
|
||||||
|
ingredients: [
|
||||||
|
{ canonicalIngredientId: "kycklingfile", displayNameSv: "kycklingfilé", quantity: 500, unit: "GRAM", optional: false, note: null },
|
||||||
|
{ canonicalIngredientId: "ris", displayNameSv: "ris", quantity: 300, unit: "GRAM", optional: false, note: null },
|
||||||
|
{ canonicalIngredientId: "krossade_tomater", displayNameSv: "krossade tomater", quantity: 400, unit: "GRAM", optional: false, note: null },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const result = await verifyCandidate(c, mockIngredients, noSimilarity);
|
||||||
|
expect(result.allergens).not.toContain("gluten");
|
||||||
|
expect(result.status).toBe("verified");
|
||||||
|
});
|
||||||
|
|
||||||
it("detekterar dubblett", async () => {
|
it("detekterar dubblett", async () => {
|
||||||
const dupSimilarity: SimilarityLookup = {
|
const dupSimilarity: SimilarityLookup = {
|
||||||
async hasSimilarity() {
|
async hasSimilarity() {
|
||||||
|
|||||||
Reference in New Issue
Block a user