import { prisma } from "@/lib/db"; /** * Was die Ende-zu-Ende-Abläufe hinterlassen, wieder wegräumen (#027). * * **Alles, was ein Ablauf anlegt, beginnt mit `ZZ`.** Das Präfix sortiert * die Zeilen in jeder Liste ans Ende und ist zugleich der Griff, an dem sie * wieder herausgehen — ohne gemerkte Kennungen, denn ein Lauf, der mitten im * Ablauf abbricht, merkt sich nichts. * * **Warum vor *und* nach dem Lauf geräumt wird.** Ein `afterAll` je Datei und * ein `globalTeardown` laufen bei einem Testfehler und bei einem Zeitablauf — * aber nicht, wenn jemand `Strg+C` drückt oder der Prozess hart stirbt. Dann * bleiben die Zeilen stehen, und der nächste Lauf findet sie vor. Deshalb räumt * derselbe Code auch im Vorlauf: was vom letzten Mal übrig ist, geht weg, bevor * der erste Ablauf startet. Ein Vorlauf kann nicht übersprungen werden. * * Aufgefallen ist das Ganze, weil in der Entwicklungsdatenbank 105 Vorlagen mit * Namen wie `ZZ E2E Editor 1790324192596 C` standen — aus einem Fenster von * sechzehn Stunden, in dem noch kein Ablauf aufräumte. Der Schaden liegt nicht * bei den Tests, sondern bei allem, was **danach** in dieselbe Datenbank sieht: * die Vorlagenliste, die Screenshots, jede Abfrage von Hand. Ein Fehler, der * „zu viele Treffer“ heißt, kostet dann eine Stunde. */ /** * Das Präfix, das jeder Ablauf seinen Zeilen gibt. * * **`ZZ` und nicht `ZZ E2E`**, weil die Abläufe es in fünf Schreibweisen * benutzen: `ZZ E2E Editor …`, `ZZ-E2E--PC-01` (Gerätenamen dürfen keine * Leerzeichen haben), `ZZPruef…`, `ZZQuelle…` und `ZZ Abteilung …`. Alle fünf * sind sinnvoll — ein Hostname sieht anders aus als ein Vorlagenname —, und * alle fünf beginnen mit `ZZ`. Darauf zu greifen deckt sie ab, ohne sechzehn * Dateien umzubenennen. * * Zwei Buchstaben sind kurz. Das ist hier in Ordnung: geräumt wird nur beim * Ende-zu-Ende-Lauf und nur gegen die Entwicklungsdatenbank, und `ZZ` am * **Anfang** eines Namens ist in echten Daten dieses Hauses nichts, was jemand * tippt. Der Grund für die Wahl ist ohnehin derselbe: `ZZ` sortiert in jeder * Liste ans Ende. */ export const E2E_PRAEFIX = "ZZ"; /** Was aufgeräumt wurde — damit der Aufrufer es melden kann. */ export type Aufgeraeumt = Readonly>; /** * Räumt alles mit `ZZ` weg und meldet, was es war. * * Die Reihenfolge ist nicht beliebig: erst was an einem Lauf hängt, dann die * Läufe, dann woran ein Lauf hängen könnte. Ein Fremdschlüssel, der sich * beschwert, ist dabei **kein** Fehler dieser Funktion, sondern die Auskunft, * dass ein Ablauf etwas verknüpft hat, das er nicht aufräumen kann — dann soll * es auffallen und nicht stillschweigend bleiben. */ export async function e2eRestlosAufraeumen(): Promise { const praefix = { startsWith: E2E_PRAEFIX }; const gezaehlt: Record = {}; const merken = (was: string, anzahl: number): void => { if (anzahl > 0) gezaehlt[was] = (gezaehlt[was] ?? 0) + anzahl; }; /* * `Protokoll.entitaetId` ist **kein** Fremdschlüssel: die Zeilen überleben * ihren Gegenstand absichtlich (ein archivierter Lauf soll seine Geschichte * behalten). Hier heißt das, dass sie als Waisen stehen bleiben, wenn sie * nicht mitgenommen werden — darum werden die Kennungen vorher gesammelt. */ const laeufe = await prisma.lauf.findMany({ where: { titel: praefix }, select: { id: true } }); const vorlagen = await prisma.vorlage.findMany({ where: { name: praefix }, select: { id: true, versionen: { select: { id: true } } }, }); const bausteine = await prisma.baustein.findMany({ where: { name: praefix }, select: { id: true, versionen: { select: { id: true } } }, }); const geraete = await prisma.geraet.findMany({ where: { OR: [{ bezeichnung: praefix }, { hostname: praefix }] }, select: { id: true }, }); const personen = await prisma.mitarbeiter.findMany({ where: { anzeigename: praefix }, select: { id: true }, }); const abteilungen = await prisma.abteilung.findMany({ where: { name: praefix }, select: { id: true }, }); const quellen = await prisma.verzeichnisquelle.findMany({ where: { name: praefix }, select: { id: true }, }); const benutzer = await prisma.benutzer.findMany({ where: { name: praefix }, select: { id: true }, }); /* * **Waisen werden nicht erraten.** Eine Protokollzeile, deren Gegenstand schon * fort ist, bleibt stehen — ihre `entitaetId` ist hier nicht mehr zu finden. * Sie über den Namen im Json zu greifen wäre naheliegend und falsch: derselbe * Name steht auch in echter Historie einer Zeile, die es weiterhin gibt (etwa * „Version veröffentlicht“ mit der Liste der eingesetzten Bausteine). Dass * eine solche Zeile ihren Gegenstand überlebt, ist genau der Grund, warum * `entitaetId` kein Fremdschlüssel ist. */ const laufIds = laeufe.map((zeile) => zeile.id); const betroffen = [ ...laufIds, ...vorlagen.flatMap((v) => [v.id, ...v.versionen.map((version) => version.id)]), ...bausteine.flatMap((b) => [b.id, ...b.versionen.map((version) => version.id)]), ...geraete.map((zeile) => zeile.id), ...personen.map((zeile) => zeile.id), ...abteilungen.map((zeile) => zeile.id), ...quellen.map((zeile) => zeile.id), ...benutzer.map((zeile) => zeile.id), ]; if (laufIds.length > 0) { merken("Anhänge", (await prisma.anhang.deleteMany({ where: { laufId: { in: laufIds } } })).count); merken("Kommentare", (await prisma.kommentar.deleteMany({ where: { laufId: { in: laufIds } } })).count); merken( "Schrittergebnisse", (await prisma.schrittergebnis.deleteMany({ where: { laufId: { in: laufIds } } })).count, ); merken( "Zustellungen", (await prisma.zustellung.deleteMany({ where: { laufId: { in: laufIds } } })).count, ); } if (betroffen.length > 0) { merken( "Protokollzeilen", ( await prisma.protokoll.deleteMany({ where: { OR: [{ entitaetId: { in: betroffen } }, { laufId: { in: laufIds } }] }, }) ).count, ); } merken("Läufe", (await prisma.lauf.deleteMany({ where: { titel: praefix } })).count); merken("Vorlagen", (await prisma.vorlage.deleteMany({ where: { name: praefix } })).count); merken("Bausteine", (await prisma.baustein.deleteMany({ where: { name: praefix } })).count); merken( "Geräte", ( await prisma.geraet.deleteMany({ where: { OR: [{ bezeichnung: praefix }, { hostname: praefix }] }, }) ).count, ); merken("Personen", (await prisma.mitarbeiter.deleteMany({ where: { anzeigename: praefix } })).count); merken("Abteilungen", (await prisma.abteilung.deleteMany({ where: { name: praefix } })).count); merken( "Verzeichnisquellen", (await prisma.verzeichnisquelle.deleteMany({ where: { name: praefix } })).count, ); merken("Benutzer", (await prisma.benutzer.deleteMany({ where: { name: praefix } })).count); return gezaehlt; } /** Dasselbe mit einer Zeile Ausgabe — für Vor- und Nachlauf. */ export async function e2eAufraeumenMitBericht(wann: "vor" | "nach"): Promise { const gezaehlt = await e2eRestlosAufraeumen(); const summe = Object.values(gezaehlt).reduce((a, b) => a + b, 0); if (summe === 0) return; const liste = Object.entries(gezaehlt) .map(([was, anzahl]) => `${anzahl} ${was}`) .join(", "); /* * Der Vorlauf **sagt es laut**: Zeilen, die er findet, sind der Rückstand * eines abgebrochenen Durchgangs. Still wegzuräumen hieße, die Ursache zu * verdecken — und ein Ablauf, der jedes Mal abbricht, fiele nie auf. */ console.info( wann === "vor" ? `E2E-Aufräumen (Rückstand aus einem früheren Lauf): ${liste}.` : `E2E-Aufräumen: ${liste}.`, ); }