From 9c5adaa9215faed0a4f2ef8bc3e7e0ce6bba0fda Mon Sep 17 00:00:00 2001 From: "Sven (AAMOS AI)" Date: Mon, 10 Aug 2026 01:29:31 +0700 Subject: [PATCH] =?UTF-8?q?fix(recipe-generation):=20allergen-s=C3=A4kerhe?= =?UTF-8?q?t=20+=20re-derive=20+=20dedup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Lägger regressionstester: optional allergen-ingrediens deklareras, optional rå fågel kräver genomstekningssteg, optional mjölk deklareras. - Re-derive allergener för alla seedade recept (18 uppdaterade). - Raderar 4 recept med saknade ingredienser/steg. - Raderar 1 ordningsdubblett: Ugnsbakad lax med dill och citron. - 246 godkända recept kvar, 0 dublett-slugs. --- .../test/verification.test.ts | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/packages/recipe-generation/test/verification.test.ts b/packages/recipe-generation/test/verification.test.ts index 31eb921..b83174d 100644 --- a/packages/recipe-generation/test/verification.test.ts +++ b/packages/recipe-generation/test/verification.test.ts @@ -89,6 +89,42 @@ const mockIngredients: CanonicalIngredientLookup = { isBeef: false, isAlcohol: false, }, + sesame_seeds: { + id: "sesame_seeds", + nutritionPer100: { + basis: "per_100_g", + values: { kcal: 600, proteinG: 18, carbsG: 12, fatG: 50, saturatedFatG: 7, fiberG: 12, sugarG: 0, saltG: 0 }, + }, + defaultUnit: "GRAM", + densityGPerMl: 0.6, + gramsPerPiece: null, + allergens: ["sesame"], + isVegan: true, + isVegetarian: true, + containsGluten: false, + containsLactose: false, + isPork: false, + isBeef: false, + isAlcohol: false, + }, + milk_3: { + 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, + }, }; return db[id] ?? undefined; }, @@ -282,4 +318,53 @@ describe("verifyCandidate", () => { expect(result.status).toBe("unverified"); expect(result.reasons).toContain("Receptet innehåller rått kött/fågel/ägg/fisk men saknar steg för genomstekning/temperatur."); }); + + it("härleder allergen även från valfri ingrediens (optional sesame)", async () => { + const c = makeCandidate({ + ingredients: [ + { canonicalIngredientId: "kycklingfile", displayNameSv: "kycklingfilé", quantity: 300, unit: "GRAM", optional: false, note: null }, + { canonicalIngredientId: "ris", displayNameSv: "ris", quantity: 300, unit: "GRAM", optional: false, note: null }, + { canonicalIngredientId: "sesame_seeds", displayNameSv: "sesamfrön till garnering", quantity: 10, unit: "GRAM", optional: true, note: null }, + ], + steps: [ + { stepNumber: 1, instructionSv: "Stek kycklingen genomstekt.", timerSeconds: null, temperatureC: null, tip: null }, + { stepNumber: 2, instructionSv: "Tillsätt ris och sesamefrön.", timerSeconds: null, temperatureC: null, tip: null }, + ], + }); + const result = await verifyCandidate(c, mockIngredients, noSimilarity); + expect(result.allergens).toContain("sesame"); + }); + + it("kräver genomstekningssteg även för optional rå fågel", async () => { + const c = makeCandidate({ + ingredients: [ + { 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 }, + { canonicalIngredientId: "chicken_breast", displayNameSv: "kycklingfilé", quantity: 300, unit: "GRAM", optional: true, note: null }, + ], + steps: [ + { stepNumber: 1, instructionSv: "Koka riset enligt anvisningen.", timerSeconds: null, temperatureC: null, tip: null }, + { stepNumber: 2, instructionSv: "Tillsätt tomater och kyckling.", timerSeconds: null, temperatureC: null, tip: null }, + ], + }); + const result = await verifyCandidate(c, mockIngredients, noSimilarity); + expect(result.status).toBe("unverified"); + expect(result.reasons).toContain("Receptet innehåller rått kött/fågel/ägg/fisk men saknar steg för genomstekning/temperatur."); + }); + + it("härleder mjölk-allergen från optional mjölk", async () => { + const c = makeCandidate({ + ingredients: [ + { canonicalIngredientId: "kycklingfile", displayNameSv: "kycklingfilé", quantity: 300, unit: "GRAM", optional: false, note: null }, + { canonicalIngredientId: "ris", displayNameSv: "ris", quantity: 300, unit: "GRAM", optional: false, note: null }, + { canonicalIngredientId: "milk_3", displayNameSv: "mjölk till servering", quantity: 2, unit: "DECILITER", optional: true, note: null }, + ], + steps: [ + { stepNumber: 1, instructionSv: "Stek kycklingen genomstekt.", timerSeconds: null, temperatureC: null, tip: null }, + { stepNumber: 2, instructionSv: "Tillsätt ris och mjölk.", timerSeconds: null, temperatureC: null, tip: null }, + ], + }); + const result = await verifyCandidate(c, mockIngredients, noSimilarity); + expect(result.allergens).toContain("milk"); + }); });