From 1f746dc822603fdfee4ad1a3ececb9e1b5fcdc67 Mon Sep 17 00:00:00 2001 From: "Sven (AAMOS AI)" Date: Sun, 9 Aug 2026 20:09:31 +0700 Subject: [PATCH] =?UTF-8?q?test(recipe-generation):=20h=C3=A5rda=20allerge?= =?UTF-8?q?n-tester=20d=C3=A4r=20h=C3=A4rledda=20allergener=20=C3=B6verk?= =?UTF-8?q?=C3=B6r=20AI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/recipe-generation/src/types.ts | 2 + .../recipe-generation/src/verification.ts | 6 ++- .../test/verification.test.ts | 48 +++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/recipe-generation/src/types.ts b/packages/recipe-generation/src/types.ts index 8e9b70c..38cadde 100644 --- a/packages/recipe-generation/src/types.ts +++ b/packages/recipe-generation/src/types.ts @@ -20,6 +20,8 @@ export interface RecipeCandidate { freezerFriendly: boolean; sourceType: "ai_generated"; confidence: number; + /** AI:s påstådda allergener — används aldrig som facit; alltid härledda ur ingredienser. */ + aiClaimedAllergens?: string[]; } export interface RecipeCandidateIngredient { diff --git a/packages/recipe-generation/src/verification.ts b/packages/recipe-generation/src/verification.ts index f36612a..0ae2c98 100644 --- a/packages/recipe-generation/src/verification.ts +++ b/packages/recipe-generation/src/verification.ts @@ -135,7 +135,11 @@ export async function verifyCandidate( } // ── 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) ──────────────────────────── const calcIngredients: RecipeIngredientForCalc[] = candidate.ingredients.map((ing) => ({ diff --git a/packages/recipe-generation/test/verification.test.ts b/packages/recipe-generation/test/verification.test.ts index c76ffb7..2f3f6eb 100644 --- a/packages/recipe-generation/test/verification.test.ts +++ b/packages/recipe-generation/test/verification.test.ts @@ -153,6 +153,54 @@ describe("verifyCandidate", () => { 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 = { + 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 () => { const dupSimilarity: SimilarityLookup = { async hasSimilarity() {