Files
oikos/test-meal-planning.js
2026-05-11 23:10:38 +02:00

141 lines
5.9 KiB
JavaScript

/**
* 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);