import { describe, expect, it, afterAll } from "vitest"; import { config as loadDotenv } from "dotenv"; import path from "node:path"; import { createDatabase, closeDatabase } from "@app/database"; import { SEED_RECIPES } from "@app/database/seed"; import { verifyCandidate } from "../src/verification.js"; // Ladda monorepo-root .env (samma mönster som @app/database seed/tests) loadDotenv({ path: path.resolve(process.cwd(), "../../.env") }); const { db } = createDatabase(); async function buildLookup() { const allIngs = await db.query.canonicalIngredients.findMany(); return { getById(id: string) { return allIngs.find((i) => i.id === id) ?? undefined; }, }; } afterAll(async () => { await closeDatabase(); }); describe("allergen invariant", () => { it("varje seedat recept har allergens som matchar färsk derivering från canonical_ingredients", async () => { const lookup = await buildLookup(); const seedTitles = new Set(SEED_RECIPES.map((r) => r.titleSv)); const recipes = (await db.query.recipes.findMany()).filter((r) => seedTitles.has(r.titleSv)); const mismatches: string[] = []; for (const recipe of recipes) { const ingredients = await db.query.recipeIngredients.findMany({ where: (t, { eq }) => eq(t.recipeId, recipe.id), }); const steps = await db.query.recipeSteps.findMany({ where: (t, { eq }) => eq(t.recipeId, recipe.id), }); if (ingredients.length === 0 || steps.length === 0) { // Inkompletta recept får aldrig lagras som verified i produktion. continue; } const candidate = { titleSv: recipe.titleSv, descriptionSv: recipe.descriptionSv, cuisine: recipe.cuisine, mealTypes: recipe.mealTypes, prepTimeMinutes: recipe.prepTimeMinutes, cookTimeMinutes: recipe.cookTimeMinutes, totalTimeMinutes: recipe.totalTimeMinutes, portions: recipe.portions, spiceLevel: recipe.spiceLevel, ingredients: ingredients.map((i) => ({ canonicalIngredientId: i.canonicalIngredientId, displayNameSv: i.displayNameSv, quantity: i.quantity, unit: i.unit, optional: i.optional, note: i.note, })), steps: steps.map((s) => ({ stepNumber: s.stepNumber, instructionSv: s.instructionSv, timerSeconds: s.timerSeconds, temperatureC: s.temperatureC, tip: s.tip, })), storageGuidanceSv: recipe.storageGuidanceSv, mealPrepFriendly: recipe.mealPrepFriendly, freezerFriendly: recipe.freezerFriendly, sourceType: recipe.sourceType, confidence: 1, }; const result = await verifyCandidate(candidate as any, lookup, null); const derived = [...result.allergens].sort(); const stored = [...(recipe.allergens ?? [])].sort(); if (JSON.stringify(derived) !== JSON.stringify(stored)) { mismatches.push(`${recipe.titleSv}: stored=${JSON.stringify(stored)} derived=${JSON.stringify(derived)}`); } } if (mismatches.length > 0) { console.error("Allergen mismatches:\n" + mismatches.join("\n")); } expect(mismatches).toEqual([]); }); });