Files
DogFatherGitandClaude Opus 5 b037867a99 Tests beenden sich jetzt sauber (process.exitCode statt process.exit)
Der Absturz auf dem Server bestand nach dem ersten Fix fort:

  node::RemoveEnvironmentCleanupHook … Assertion failed: (env) != nullptr
  Statement::~Statement() … better_sqlite3.node
  Abgebrochen

DER ERSTE VERSUCH GING AN DER URSACHE VORBEI

Ich hatte db.close() entfernt -- naheliegend, weil der Aufrufverlauf
auf einen Statement-Destruktor zeigte. Es 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 einen Fehler, obwohl inhaltlich alles bestanden war. In
einer mit && verketteten Befehlsfolge blieb deshalb der anschliessende
Dienst-Neustart aus, und die neuen Endpunkte antworteten weiter mit
404. Gesucht habe ich bei den Endpunkten, beim Deploy, an der
Zugangswand -- die Ursache lag beim Beenden eines Testprozesses.

BEMERKENSWERT

Fuenf Tests im Projekt benutzten process.exitCode bereits. Das Muster
war also etabliert; meine neuen Dateien wichen davon ab, ohne dass es
jemandem auffiel. Sechs Tests sind jetzt angeglichen, alle geprueft:
Rueckgabewert 0, Zusammenfassung vollstaendig.

  test-push-kette 30, test-altabbruch 16, test-webdesign-anfragen 37,
  test-webdesign-portal 49, test-webdesign-paypal 28,
  test-personendaten 15

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-27 00:17:21 +02:00

146 lines
7.1 KiB
JavaScript

/* =====================================================================
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', '[email protected]', '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;