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.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user