fix(recipe-generation): allergen-säkerhet + re-derive + dedup

- 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.
This commit is contained in:
Sven (AAMOS AI)
2026-08-10 01:29:31 +07:00
parent 2fda1a5785
commit 9c5adaa921
@@ -89,6 +89,42 @@ const mockIngredients: CanonicalIngredientLookup = {
isBeef: false, isBeef: false,
isAlcohol: 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; return db[id] ?? undefined;
}, },
@@ -282,4 +318,53 @@ describe("verifyCandidate", () => {
expect(result.status).toBe("unverified"); 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."); 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");
});
}); });