Files
Laufzettel/tests/e2e/aufraeumen.ts
T
Florian WildeandClaude Opus 5 80d97467da
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
fix: Reviewbefunde aus der Durchsicht von Phase 2 (#036, #027)
Acht Meilensteine lagen ohne fremden Blick auf main. Ein Review mit eigener
Datenbank, Gegenproben und vollem Suitenlauf hat sie gegen die
Akzeptanzkriterien und gegen CLAUDE.md geprueft.

Die Kernregeln halten: fuenf von fuenf Gegenproben gegen Versionierung,
Mandantengrenze, Abschlussregel und Selbstauskunft wurden rot. Alle Befunde
lagen am Rand — und die drei wertvollsten waren Gegenproben, die gruen blieben.

Schwer:

- Der Link in der Mail "Verzeichnis-Abgleich fehlgeschlagen" war ein 404
  (/verwaltung/verzeichnis statt /admin/verzeichnis). Die eine Meldung, die an
  alle Administratoren geht, im Moment eines Ausfalls. Ein Test prueft jetzt
  jeden Meldungspfad gegen die vorhandenen Routen.
- Der Export eines Entwurfs mit Baustein lieferte einen leeren Abschnitt unter
  dem Platzhalter "wird ersetzt" — der Zweck des Formats ist, eine Vorlage in
  eine zweite Installation zu bringen. Bausteine werden jetzt aufgeloest.

Mittel:

- Der Anhang-Handler hatte keinen Test; die ganze Rechtepruefung liess sich
  durch einen hartcodierten ADMIN ersetzen, Suite gruen. Es ist die einzige
  Stelle, die rohe Dateibytes ausliefert. tests/anhang-route.test.ts faehrt ihn
  jetzt echt, acht Pruefungen.
- /api/scim in der Proxy-Allowlist stand in keinem Test. OEFFENTLICH ist
  exportiert, der Test vergleicht beide Richtungen: eine Liste, die nur in einer
  Richtung geprueft wird, laeuft dem Code hinterher.
- Ein offener Ereignisstrom ueberlebte Abmelden, Deaktivieren und
  Sitzungswiderruf. Der Herzschlag prueft die Sitzung jetzt nach. Dass es kein
  Test zeigte, lag an einer Attrappe `sitzung: {}` — eine Attrappe ohne die Form
  der Wirklichkeit prueft etwas anderes als die Wirklichkeit.
- README kannte MITARBEITER nicht und behauptete, unbekannte Benutzer wuerden
  angelegt. Wer auf v0.2.0 hebt und nur das README liest, haette genau diese
  Leute ausgesperrt.
- sicherung.md nannte einen Anhaenge-Pfad, den es nirgends gibt — als
  Rueckfallwert in einem Skript mit rm -rf. Jetzt bricht es ab statt zu raten.
- Attributnamen wurden nur in der Server Action geprueft, waehrend der
  Graph-Leser sich im Kommentar darauf berief.

Dazu #027: die Ende-zu-Ende-Ablaeufe raeumen jetzt zentral auf, vor und nach
dem Lauf, und ein struktureller Test bewacht es. Beim ersten echten Lauf seit
dem 26. September waren sieben Ablaeufe rot, vier Ursachen, keine ein
Produktfehler — unbemerkt, weil pnpm test:e2e in keinem Tor steht. Das ist jetzt
#038 und keine Fussnote.

Ein Befund des Reviews war falsch (der Zustands-Handler habe keinen Test; er hat
acht direkte Aufrufe). Nachgeprueft, bevor etwas geaendert wurde.

1454 Vitest-Tests (vorher 1432), 113 E2E-Ablaeufe gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-11 01:32:09 +02:00

186 lines
7.7 KiB
TypeScript

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-<zeit>-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<Record<string, number>>;
/**
* 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<Aufgeraeumt> {
const praefix = { startsWith: E2E_PRAEFIX };
const gezaehlt: Record<string, number> = {};
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<void> {
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}.`,
);
}