Files
Cibello-app/packages/database/test/extra-recipes.test.ts
T
Claude 100d70dc2d feat(recept): Baka-kategori + 16 nya redaktionella recept
- Ny 'baking'-tagg (RECIPE_TAGS, text[] - ingen migration). Browse-vyn far
  en 'Baka'-chip som filtrerar pa taggen; befintliga lussekatter +
  saffransbullar taggas ocksa.
- extra-recipes.ts: 16 nya recept som fyller de glesa kategorierna -
  4 bak (kanelbullar, kardemummabullar, appelsmulpaj, frukostfrallor),
  3 frukost, 3 mellanmal (tomma kategorin -> fylld), 3 efterratt, 3 lunch.
- Endast verifierade canonical-ingredienser + berakningsbara enheter, sa
  seed harleder narings-/allergendata utan att krascha (run.ts 675/691).
- Nytt rent invariant-test (ingen DB) kor riktiga computeRecipeNutrition pa
  alla 16: id:n finns, naring berakningsbar, inga slug-krockar. 5 tester grona.
- Seed ar additiv (onConflictDoUpdate) - 'pnpm db:seed' lagger till + taggar
  om utan dataforlust. Full typecheck 20/20.
2026-08-18 23:35:47 +00:00

85 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, IngredientNutritionSource> {
const sources = new Map<string, IngredientNutritionSource>();
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 16 recept", () => {
expect(EXTRA_RECIPES.length).toBeGreaterThanOrEqual(16);
});
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<string>();
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);
});
});