/** * Modul: Meal-Planning-Test * Zweck: Validiert native Meal-Planning-Signal-Tabellen und Constraints. * Ausführen: node --experimental-sqlite test-meal-planning.js */ import { DatabaseSync } from 'node:sqlite'; import fs from 'node:fs'; let passed = 0; let failed = 0; function test(name, fn) { try { fn(); console.log(` ✓ ${name}`); passed++; } catch (err) { console.error(` ✗ ${name}: ${err.message}`); failed++; } } function assert(cond, msg) { if (!cond) throw new Error(msg || 'Assertion fehlgeschlagen'); } function migrationSql(version) { const source = fs.readFileSync(new URL('./server/db.js', import.meta.url), 'utf8'); const re = new RegExp('version:\\s*' + version + ',[\\s\\S]*?up:\\s*`([\\s\\S]*?)`'); const match = source.match(re); if (!match) throw new Error(`Migration v${version} nicht gefunden`); return match[1]; } const db = new DatabaseSync(':memory:'); db.exec('PRAGMA foreign_keys = ON;'); db.exec(` CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, display_name TEXT NOT NULL, avatar_color TEXT ); CREATE TABLE recipes ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL ); CREATE TABLE meals ( id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT NOT NULL, meal_type TEXT NOT NULL CHECK(meal_type IN ('breakfast', 'lunch', 'dinner', 'snack')), title TEXT NOT NULL ); `); db.exec(migrationSql(39)); console.log('\n[Meal-Planning-Test] Native Signaltabellen\n'); const tables = [ 'meal_cooking_rules', 'recipe_family_preferences', 'recipe_variation_meta', 'planned_meal_cooks', 'meal_plan_feedback', 'kids_cookbooks', ]; for (const table of tables) { test(`Tabelle "${table}" existiert`, () => { const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table); assert(row, `Tabelle ${table} fehlt`); }); } let userId, recipeId, mealId; test('Fixture-Daten erstellen', () => { userId = db.prepare("INSERT INTO users (display_name, avatar_color) VALUES ('Liv', '#FF6B9D')").run().lastInsertRowid; recipeId = db.prepare("INSERT INTO recipes (title) VALUES ('Pasta med tomat')").run().lastInsertRowid; mealId = db.prepare("INSERT INTO meals (date, meal_type, title) VALUES ('2026-05-12', 'dinner', 'Pasta med tomat')").run().lastInsertRowid; assert(userId > 0 && recipeId > 0 && mealId > 0); }); test('Recurring cook rule gemmes', () => { db.prepare('INSERT INTO meal_cooking_rules (user_id, weekday, meal_type, priority) VALUES (?, 0, ?, 100)').run(userId, 'dinner'); const row = db.prepare('SELECT * FROM meal_cooking_rules WHERE user_id = ?').get(userId); assert(row.weekday === 0 && row.meal_type === 'dinner'); }); test('Recipe preference/capability-signaler gemmes', () => { db.prepare(` INSERT INTO recipe_family_preferences ( recipe_id, user_id, preference, can_cook, can_help_cook, will_eat_modified, adult_only, swap_in_count, swap_away_count ) VALUES (?, ?, 'favorite', 1, 1, 1, 0, 2, 1) `).run(recipeId, userId); const row = db.prepare('SELECT * FROM recipe_family_preferences WHERE recipe_id = ? AND user_id = ?').get(recipeId, userId); assert(row.preference === 'favorite'); assert(row.can_cook === 1 && row.can_help_cook === 1 && row.will_eat_modified === 1 && row.adult_only === 0); assert(row.swap_in_count === 2 && row.swap_away_count === 1); }); test('Variation metadata gemmes', () => { db.prepare('INSERT INTO recipe_variation_meta (recipe_id, protein, style, kid_suitable_confidence) VALUES (?, ?, ?, 85)') .run(recipeId, 'vegetarian', 'quick'); const row = db.prepare('SELECT * FROM recipe_variation_meta WHERE recipe_id = ?').get(recipeId); assert(row.protein === 'vegetarian' && row.style === 'quick' && row.kid_suitable_confidence === 85); }); test('Planned meal cook assignment gemmes', () => { db.prepare('INSERT INTO planned_meal_cooks (meal_id, user_id, planned_for_date, meal_type, source_plan_id) VALUES (?, ?, ?, ?, ?)') .run(mealId, userId, '2026-05-12', 'dinner', 'plan-test'); const row = db.prepare('SELECT * FROM planned_meal_cooks WHERE meal_id = ?').get(mealId); assert(row.user_id === userId && row.source_plan_id === 'plan-test'); }); test('Feedback event gemmes', () => { const id = db.prepare(` INSERT INTO meal_plan_feedback (plan_id, meal_id, recipe_id, slot_date, meal_type, action, original_title, final_title, user_id) VALUES ('plan-test', ?, ?, '2026-05-12', 'dinner', 'accept', 'Pasta', 'Pasta med tomat', ?) `).run(mealId, recipeId, userId).lastInsertRowid; const row = db.prepare('SELECT * FROM meal_plan_feedback WHERE id = ?').get(id); assert(row.action === 'accept' && row.recipe_id === recipeId); }); test('Kids cookbook gemmes som JSON', () => { const content = JSON.stringify({ title: 'Pasta for børn', steps: ['Vask hænder', 'Rør sovs'] }); const id = db.prepare('INSERT INTO kids_cookbooks (recipe_id, title, content_json, created_by) VALUES (?, ?, ?, ?)') .run(recipeId, 'Pasta for børn', content, userId).lastInsertRowid; const row = db.prepare('SELECT * FROM kids_cookbooks WHERE id = ?').get(id); assert(JSON.parse(row.content_json).steps.length === 2); }); test('Ugyldig preference afvises', () => { let failedConstraint = false; try { db.prepare('INSERT INTO recipe_family_preferences (recipe_id, user_id, preference) VALUES (?, ?, ?)').run(recipeId, userId + 1, 'maybe'); } catch { failedConstraint = true; } assert(failedConstraint, 'CHECK constraint skulle afvise ugyldig preference'); }); test('FK cascade sletter recipe-signaler', () => { db.prepare('DELETE FROM recipes WHERE id = ?').run(recipeId); const prefs = db.prepare('SELECT count(*) AS n FROM recipe_family_preferences WHERE recipe_id = ?').get(recipeId).n; const meta = db.prepare('SELECT count(*) AS n FROM recipe_variation_meta WHERE recipe_id = ?').get(recipeId).n; assert(prefs === 0 && meta === 0); }); console.log(`\n[Meal-Planning-Test] Ergebnis: ${passed} bestanden, ${failed} fehlgeschlagen\n`); if (failed > 0) process.exit(1);