/* ===================================================================== Prueft Migration 0018: Ein Projekt, das abgebrochen wurde, BEVOR es das Archiv-Kennzeichen gab, muss danach aus der laufenden Liste verschwinden -- ohne dass dabei etwas erfunden oder zerstoert wird. Der Test stellt den Altzustand echt nach: Er laesst alle Migrationen bis 0017 laufen, baut den Altfall, und laesst DANN 0018 laufen. Ein Test, der 0018 einfach zweimal auf eine leere Datenbank wirft, wuerde nichts beweisen. ===================================================================== */ import Database from "better-sqlite3"; import { readFileSync, readdirSync } from "fs"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; const HIER = dirname(fileURLToPath(import.meta.url)); const MIG = join(HIER, "..", "cloudflare-worker", "migrations"); let ok = 0, fehl = 0; const pruefe = (name, gut, info) => { if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); } else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); } }; const db = new Database(":memory:"); db.pragma("foreign_keys = ON"); /* ---- Alle Migrationen BIS 0017 (der Stand vor der Reparatur) ---- */ const alle = readdirSync(MIG).filter((f) => f.endsWith(".sql")).sort(); const bis17 = alle.filter((f) => f < "0018"); for (const f of bis17) db.exec(readFileSync(join(MIG, f), "utf8")); console.log("1. DER ALTZUSTAND"); /* Ein Kunde, weil wd_projekte einen Fremdschluessel darauf hat. */ db.prepare( `INSERT INTO wd_kunden (id, name, email, erstellt_am) VALUES ('k-alt', 'Testkunde', 'alt@example.com', '2026-08-01T10:00:00.000Z')` ).run(); /* Der Altfall: Status abgebrochen, aber archiviert = 0 und alle Abbruch-Felder leer -- genau wie P-2608-0001. */ db.prepare( `INSERT INTO wd_projekte (id, nummer, kunde_id, titel, paket, status, archiviert, wartet_auf, erstellt_am, aktualisiert_am) VALUES ('p-alt', 'P-2608-0001', 'k-alt', 'Altfall', 'start', 'abgebrochen', 0, 'kunde', '2026-08-01T10:00:00.000Z', '2026-08-01T10:00:00.000Z')` ).run(); /* Ein laufendes Projekt als Gegenprobe -- es darf NICHT angefasst werden. */ db.prepare( `INSERT INTO wd_projekte (id, nummer, kunde_id, titel, paket, status, archiviert, wartet_auf, erstellt_am, aktualisiert_am) VALUES ('p-lauf', 'P-2608-0002', 'k-alt', 'Laeuft', 'start', 'design', 0, 'kunde', '2026-08-02T10:00:00.000Z', '2026-08-02T10:00:00.000Z')` ).run(); /* Und ein bereits sauber abgebrochenes -- seine echten Abbruchdaten duerfen durch die Migration nicht ueberschrieben werden. */ db.prepare( `INSERT INTO wd_projekte (id, nummer, kunde_id, titel, paket, status, archiviert, wartet_auf, abbruch_am, abbruch_grund, abbruch_wer, abbruch_erstattung_cent, erstellt_am, aktualisiert_am) VALUES ('p-neu', 'P-2608-0003', 'k-alt', 'Sauber abgebrochen', 'start', 'abgebrochen', 1, 'niemand', '2026-08-20T09:00:00.000Z', 'kunde_zahlt_nicht', 'dogfather', 14900, '2026-08-03T10:00:00.000Z', '2026-08-20T09:00:00.000Z')` ).run(); const vorher = db.prepare(`SELECT * FROM wd_projekte WHERE id = 'p-alt'`).get(); pruefe("der Altfall steht in der laufenden Liste", vorher.archiviert === 0, "archiviert = " + vorher.archiviert); pruefe("und hat keine Abbruchdaten", !vorher.abbruch_am, String(vorher.abbruch_am)); console.log("\n2. NACH DER MIGRATION"); db.exec(readFileSync(join(MIG, "0018_webdesign_altabbrueche_archivieren.sql"), "utf8")); const alt = db.prepare(`SELECT * FROM wd_projekte WHERE id = 'p-alt'`).get(); pruefe("der Altfall ist archiviert", alt.archiviert === 1, "archiviert = " + alt.archiviert); pruefe("die Uhr steht formal still", alt.wartet_auf === "niemand", alt.wartet_auf); pruefe("der Status bleibt abgebrochen", alt.status === "abgebrochen", alt.status); /* Der Kern der Ehrlichkeitsregel aus dem Migrationskopf. */ pruefe("es wurde KEIN Abbruchdatum erfunden", alt.abbruch_am === null, String(alt.abbruch_am)); pruefe("es wurde KEIN Grund erfunden", alt.abbruch_grund === null, String(alt.abbruch_grund)); pruefe("es wurde KEINE Erstattung erfunden", alt.abbruch_erstattung_cent === null, String(alt.abbruch_erstattung_cent)); console.log("\n3. WAS NICHT ANGEFASST WERDEN DARF"); const lauf = db.prepare(`SELECT * FROM wd_projekte WHERE id = 'p-lauf'`).get(); pruefe("das laufende Projekt bleibt in der Liste", lauf.archiviert === 0, "archiviert = " + lauf.archiviert); pruefe("und sein Wartezustand bleibt unveraendert", lauf.wartet_auf === "kunde", lauf.wartet_auf); pruefe("und sein Aenderungsdatum wurde nicht angefasst", lauf.aktualisiert_am === "2026-08-02T10:00:00.000Z", lauf.aktualisiert_am); const neu = db.prepare(`SELECT * FROM wd_projekte WHERE id = 'p-neu'`).get(); pruefe("der echte Abbruch behaelt sein Datum", neu.abbruch_am === "2026-08-20T09:00:00.000Z", neu.abbruch_am); pruefe("und seinen Grund", neu.abbruch_grund === "kunde_zahlt_nicht", neu.abbruch_grund); pruefe("und seine Erstattung", neu.abbruch_erstattung_cent === 14900, String(neu.abbruch_erstattung_cent)); pruefe("und sein Aenderungsdatum wurde nicht angefasst", neu.aktualisiert_am === "2026-08-20T09:00:00.000Z", neu.aktualisiert_am); console.log("\n4. NOCHMAL LAUFEN LASSEN AENDERT NICHTS"); /* Eine Migration darf nie davon abhaengen, dass sie genau einmal laeuft -- beim Wiederherstellen einer Sicherung passiert genau das. */ const standVorher = db.prepare( `SELECT id, archiviert, wartet_auf, aktualisiert_am FROM wd_projekte ORDER BY id` ).all(); db.exec(readFileSync(join(MIG, "0018_webdesign_altabbrueche_archivieren.sql"), "utf8")); const standNachher = db.prepare( `SELECT id, archiviert, wartet_auf, aktualisiert_am FROM wd_projekte ORDER BY id` ).all(); pruefe("zweiter Durchlauf aendert nichts mehr", JSON.stringify(standVorher) === JSON.stringify(standNachher)); console.log(`\n===== ${ok} bestanden, ${fehl} fehlgeschlagen =====`); db.close(); /* ⚠️ process.exitCode STATT process.exit() (geaendert 27.08.2026). Auf dem Server brach dieser Test reproduzierbar ab: node::RemoveEnvironmentCleanupHook … Assertion failed: (env) != nullptr Statement::~Statement() … better_sqlite3.node Abgebrochen Ein erster Versuch entfernte db.close() -- das half NICHT. Die Ursache liegt eine Ebene tiefer: process.exit() beendet Node sofort, waehrend better-sqlite3 noch offene Statements haelt. Deren Aufraeumhaken laeuft dann ins Leere. process.exitCode setzt nur den Rueckgabewert; Node beendet sich danach von selbst, sobald nichts mehr aussteht -- und raeumt dabei in der richtigen Reihenfolge auf. WARUM DAS MEHR ALS EIN SCHOENHEITSFEHLER WAR Der Absturz kam NACH allen Pruefungen und VOR der Zusammenfassung. Der Test meldete also einen Fehler, obwohl inhaltlich alles bestanden war. In einer mit && verketteten Befehlsfolge blieb deshalb der anschliessende Dienst-Neustart aus, und neue Endpunkte antworteten weiter mit 404. Man sucht dann bei den Endpunkten -- die Ursache lag beim Beenden eines Testprozesses. */ process.exitCode = fehl ? 1 : 0;