Initial commit (unpacked platform)

This commit is contained in:
Sven (AAMOS AI)
2026-08-05 19:21:11 +07:00
commit ac5340195a
314 changed files with 57584 additions and 0 deletions
+299
View File
@@ -0,0 +1,299 @@
import { describe, expect, it } from "vitest";
import {
applySubstitution,
checkRecipeSafety,
computeCoverage,
deriveRecipeAllergens,
estimateCost,
isRecipeSafe,
scaleIngredients,
type IngredientSafetyInfo,
} from "../src/index.js";
const INFO = new Map<string, IngredientSafetyInfo>([
[
"chicken",
{
id: "chicken",
allergens: [],
isVegan: false,
isVegetarian: false,
containsGluten: false,
containsLactose: false,
isPork: false,
isBeef: false,
isAlcohol: false,
},
],
[
"milk",
{
id: "milk",
allergens: ["milk"],
isVegan: false,
isVegetarian: true,
containsGluten: false,
containsLactose: true,
isPork: false,
isBeef: false,
isAlcohol: false,
},
],
[
"pasta",
{
id: "pasta",
allergens: ["gluten"],
mayContainAllergens: ["eggs"],
isVegan: true,
isVegetarian: true,
containsGluten: true,
containsLactose: false,
isPork: false,
isBeef: false,
isAlcohol: false,
},
],
[
"bacon",
{
id: "bacon",
allergens: [],
isVegan: false,
isVegetarian: false,
containsGluten: false,
containsLactose: false,
isPork: true,
isBeef: false,
isAlcohol: false,
},
],
[
"tofu",
{
id: "tofu",
allergens: ["soy"],
isVegan: true,
isVegetarian: true,
containsGluten: false,
containsLactose: false,
isPork: false,
isBeef: false,
isAlcohol: false,
},
],
]);
describe("KRITISKT: deterministisk allergisäkerhet (spec §61.2)", () => {
it("blockerar recept med användarens allergen", () => {
const violations = checkRecipeSafety(
{ ingredients: [{ canonicalIngredientId: "milk", optional: false }], spiceLevel: 0 },
{ allergens: ["milk"] },
INFO,
);
expect(isRecipeSafe(violations)).toBe(false);
expect(violations[0]?.code).toBe("allergen");
expect(violations[0]?.severity).toBe("blocker");
});
it("'kan innehålla spår' ger varning när användaren har allergi", () => {
const violations = checkRecipeSafety(
{ ingredients: [{ canonicalIngredientId: "pasta", optional: false }], spiceLevel: 0 },
{ allergens: ["eggs"] },
INFO,
);
expect(violations.some((v) => v.code === "allergen_may_contain")).toBe(true);
});
it("okänd ingrediens ger varning aldrig tyst OK (försiktighetsprincipen)", () => {
const violations = checkRecipeSafety(
{ ingredients: [{ canonicalIngredientId: "mystery", optional: false }], spiceLevel: 0 },
{ allergens: [] },
INFO,
);
expect(violations.some((v) => v.code === "unverified_data")).toBe(true);
});
it("valfri ingrediens med allergen → varning, inte blocker", () => {
const violations = checkRecipeSafety(
{ ingredients: [{ canonicalIngredientId: "milk", optional: true }], spiceLevel: 0 },
{ allergens: ["milk"] },
INFO,
);
expect(isRecipeSafe(violations)).toBe(true);
expect(violations[0]?.severity).toBe("warning");
});
it("vegan blockerar kött och mjölk", () => {
const violations = checkRecipeSafety(
{
ingredients: [
{ canonicalIngredientId: "chicken", optional: false },
{ canonicalIngredientId: "milk", optional: false },
],
spiceLevel: 0,
},
{ allergens: [], dietPattern: "vegan" },
INFO,
);
expect(violations.filter((v) => v.code === "diet_pattern")).toHaveLength(2);
});
it("halal blockerar fläsk", () => {
const violations = checkRecipeSafety(
{ ingredients: [{ canonicalIngredientId: "bacon", optional: false }], spiceLevel: 0 },
{ allergens: [], religiousRule: "halal" },
INFO,
);
expect(isRecipeSafe(violations)).toBe(false);
});
it("härleder receptallergener ur ingredienserna", () => {
expect(deriveRecipeAllergens(["pasta", "milk", "chicken"], INFO)).toEqual(["gluten", "milk"]);
});
});
describe("lagermatchning (spec §17)", () => {
it("beräknar täckning, saknat och utgående", () => {
const result = computeCoverage(
[
{
canonicalIngredientId: "chicken",
displayNameSv: "Kyckling",
quantity: 500,
unit: "GRAM",
optional: false,
},
{
canonicalIngredientId: "pasta",
displayNameSv: "Pasta",
quantity: 300,
unit: "GRAM",
optional: false,
},
],
[
{
id: "1",
canonicalIngredientId: "chicken",
quantity: 600,
unit: "GRAM",
bestBeforeDate: "2026-08-03",
},
],
new Map(),
new Date("2026-08-02T00:00:00Z"), // UTC testet ska vara sant i alla tidszoner
);
expect(result.coverage).toBe(0.5);
expect(result.missing.map((m) => m.canonicalIngredientId)).toEqual(["pasta"]);
expect(result.expiringUsed[0]?.canonicalIngredientId).toBe("chicken");
});
it("mjölkprincipen: passerat bäst före räknas som tillgängligt, inte som borta", () => {
// Mjölken som gick ut igår är inte automatiskt dålig recepten ska
// fortsätta föreslå den (användaren luktar/smakar) i stället för att
// tyst behandla varan som obefintlig och driva nyinköp/svinn.
const result = computeCoverage(
[
{
canonicalIngredientId: "milk",
displayNameSv: "Mjölk",
quantity: 500,
unit: "MILLILITER",
optional: false,
},
],
[
{
id: "1",
canonicalIngredientId: "milk",
quantity: 1000,
unit: "MILLILITER",
bestBeforeDate: "2026-08-01", // gick ut igår
},
],
new Map(),
new Date("2026-08-02T00:00:00Z"), // UTC testet ska vara sant i alla tidszoner
);
expect(result.coverage).toBe(1);
expect(result.missing).toHaveLength(0);
expect(result.expiringUsed[0]?.canonicalIngredientId).toBe("milk"); // prioriteras "räddar mjölken"
});
});
describe("skalning", () => {
it("skalar mängder och avrundar köksvänligt", () => {
const scaled = scaleIngredients(
[
{
canonicalIngredientId: "chicken",
displayNameSv: "Kyckling",
quantity: 500,
unit: "GRAM",
optional: false,
},
{
canonicalIngredientId: "salt",
displayNameSv: "Salt",
quantity: 1,
unit: "TEASPOON",
optional: false,
},
],
4,
6,
);
expect(scaled[0]?.quantity).toBe(750);
expect(scaled[1]?.quantity).toBe(1.5);
});
it("kastar på ogiltiga portioner", () => {
expect(() => scaleIngredients([], 0, 4)).toThrow();
});
});
describe("substitution (spec §20)", () => {
it("byter ingrediens med mängdfaktor och varnar för fel kontext", () => {
const result = applySubstitution(
[
{
canonicalIngredientId: "milk",
displayNameSv: "Mjölk",
quantity: 200,
unit: "MILLILITER",
optional: false,
},
],
{
fromIngredientId: "milk",
toIngredientId: "tofu",
ratio: 0.8,
instructionsSv: "Tillsätt sist.",
notRecommendedFor: ["whipping"],
},
"Tofu",
"whipping",
);
expect(result.ingredients[0]?.canonicalIngredientId).toBe("tofu");
expect(result.ingredients[0]?.quantity).toBe(160);
expect(result.applied.warningSv).toContain("rekommenderas inte");
});
it("kastar när ingrediensen inte finns i receptet", () => {
expect(() =>
applySubstitution(
[],
{ fromIngredientId: "x", toIngredientId: "y", ratio: 1, notRecommendedFor: [] },
"Y",
),
).toThrow();
});
});
describe("kostnadsuppskattning (spec §26)", () => {
it("beräknar pris per portion med täckningsgrad", () => {
const result = estimateCost(
[
{ canonicalIngredientId: "chicken", quantity: 500, unit: "GRAM", optional: false },
{ canonicalIngredientId: "unknown", quantity: 100, unit: "GRAM", optional: false },
],
4,
// 130 kr/kg = 13000 minor/kg; 500 g -> 6500 minor totalt, 1625 minor/portion.
new Map([["chicken", { pricePerKgMinor: 13_000, source: "default" as const }]]),
);
expect(result.totalMinor).toBe(6500);
expect(result.perPortionMinor).toBe(1625);
expect(result.priceCoverage).toBe(0.5);
});
});