import { describe, expect, it } from "vitest"; import { computeRecipeNutrition, type IngredientNutritionSource } from "@app/nutrition-engine"; import { SEED_INGREDIENTS } from "../src/seed/data/ingredients.js"; import { EXTRA_RECIPES } from "../src/seed/data/extra-recipes.js"; import { VERIFIED_RECIPES } from "../src/seed/data/verified-recipes.js"; /** * Rent invariant-test (ingen DB) för de nya extrarecepten. Speglar exakt de * kontroller som seed-körningen gör (run.ts rad 675 + 691) så att ett trasigt * recept fångas här i stället för mitt i en seed-körning: * - varje ingrediens-id måste finnas i katalogen, * - näringen måste gå att beräkna för varje ingrediens (rätt enhet), * - inga slug-krockar med den befintliga katalogen. */ const byId = new Map(SEED_INGREDIENTS.map((i) => [i.id, i])); function sourcesFor( recipe: (typeof EXTRA_RECIPES)[number], ): Map { const sources = new Map(); for (const ri of recipe.ingredients) { const info = byId.get(ri.ing); if (!info) continue; // fångas separat av id-testet nedan sources.set(ri.ing, { nutritionPer100: info.nutritionPer100, densityGPerMl: info.densityGPerMl ?? null, gramsPerPiece: info.gramsPerPiece ?? null, }); } return sources; } describe("extrarecept – seed-invarianter", () => { it("har minst 29 recept", () => { expect(EXTRA_RECIPES.length).toBeGreaterThanOrEqual(29); }); it("alla ingrediens-id:n finns i katalogen (run.ts rad 675)", () => { const unknown: string[] = []; for (const recipe of EXTRA_RECIPES) { for (const ri of recipe.ingredients) { if (!byId.has(ri.ing)) unknown.push(`${recipe.slug}: ${ri.ing}`); } } expect(unknown).toEqual([]); }); it("näringen går att beräkna för varje recept (run.ts rad 691)", () => { const failed: string[] = []; for (const recipe of EXTRA_RECIPES) { const calc = recipe.ingredients.map((ri) => ({ canonicalIngredientId: ri.ing, quantity: ri.qty, unit: ri.unit, optional: ri.optional ?? false, })); const nutrition = computeRecipeNutrition(calc, recipe.portions, sourcesFor(recipe)); if (nutrition.uncomputableIngredientIds.length > 0) { failed.push(`${recipe.slug}: ${nutrition.uncomputableIngredientIds.join(", ")}`); } // Rimlig näring per portion (fångar grova enhetsfel). expect(nutrition.perPortion.kcal).toBeGreaterThan(0); } expect(failed).toEqual([]); }); it("inga slug-krockar med befintliga recept och inga interna dubbletter", () => { const existing = new Set(VERIFIED_RECIPES.map((r) => r.slug)); const seen = new Set(); const collisions: string[] = []; for (const recipe of EXTRA_RECIPES) { if (existing.has(recipe.slug)) collisions.push(`krock: ${recipe.slug}`); if (seen.has(recipe.slug)) collisions.push(`dubblett: ${recipe.slug}`); seen.add(recipe.slug); } expect(collisions).toEqual([]); }); it("varje bak-recept är taggat 'baking' (driver Baka-kategorin)", () => { const baking = EXTRA_RECIPES.filter((r) => r.tags.includes("baking")); expect(baking.length).toBeGreaterThanOrEqual(4); }); });