// Wächter über die Vollständigkeit der Historie. // // Die Versionierung hängt daran, dass JEDER inhaltsverändernde Endpunkt `touchScenario` // aufruft. Ein vergessener Pfad fällt nicht auf -- er erzeugt einfach still keine Version, // und die Historie hat eine Lücke, die man erst Wochen später bemerkt. Dieser Test liest die // Route-Dateien und prüft das statisch. import { describe, it, expect } from "vitest"; import { readFileSync, readdirSync } from "node:fs"; import path from "node:path"; const API = path.join(process.cwd(), "src", "app", "api"); function routeFiles(dir: string): string[] { const out: string[] = []; for (const entry of readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) out.push(...routeFiles(full)); else if (entry.name === "route.ts") out.push(full); } return out; } // Endpunkte, die schreiben, aber bewusst KEINE Version erzeugen -- jeweils mit Begründung. const EXEMPT: Record = { "auth/login": "kein Szenario betroffen", "auth/logout": "kein Szenario betroffen", "auth/register": "kein Szenario betroffen", "auth/change-password": "kein Szenario betroffen", "plans/[planId]": "ändert nur den Plan-Namen bzw. löscht den ganzen Plan – kein Szenario-Inhalt", "scenarios/[scenarioId]/versions": "erzeugt Versionen selbst (Hauptversion / Wiederherstellen)", "scenarios/[scenarioId]/versions/[versionId]": "erzeugt Versionen selbst", "plans/[planId]/actuals": "Ist-Werte sind eine Beobachtung, keine Planänderung – sie verändern kein Szenario", "plans/[planId]/actuals/[setId]": "dito (Löschen eines Ist-Satzes)", "plans/[planId]/analyses": "gespeicherte Analysen sind read-only Momentaufnahmen – kein Szenario betroffen", "plans/[planId]/analyses/[analysisId]": "dito (Öffnen/Löschen einer Analyse)", "plans/[planId]/reports": "Berichte sind erzeugte Dokumente – sie verändern kein Szenario", "plans/[planId]/reports/[reportId]": "dito (Herunterladen/Löschen eines Berichts)", }; describe("Versionierung: Abdeckung der Schreibpfade", () => { const files = routeFiles(API); it("findet überhaupt Endpunkte", () => { expect(files.length).toBeGreaterThan(10); }); it("ruft in jedem inhaltsverändernden Endpunkt touchScenario auf", () => { const missing: string[] = []; for (const file of files) { const src = readFileSync(file, "utf8"); const rel = path .relative(API, file) .replace(/\\/g, "/") .replace(/\/route\.ts$/, ""); // Schreibt dieser Endpunkt überhaupt? const mutates = /export async function (POST|PUT|PATCH|DELETE)/.test(src); if (!mutates) continue; if (rel in EXEMPT) continue; // Ein DELETE, das das Szenario selbst entfernt, braucht keine Version mehr. const onlyDeletesScenario = rel === "scenarios/[scenarioId]" && !/export async function (POST|PUT|PATCH)/.test(src); if (onlyDeletesScenario) continue; if (!src.includes("touchScenario(")) missing.push(rel); } expect( missing, `Diese Endpunkte verändern Inhalte, erzeugen aber keine Version:\n ${missing.join( "\n " )}\nEntweder touchScenario ergänzen oder in EXEMPT mit Begründung eintragen.` ).toEqual([]); }); it("hält die Ausnahmeliste frei von Karteileichen", () => { // Eine Ausnahme für einen Endpunkt, den es nicht mehr gibt, verschleiert später eine // echte Lücke. const known = new Set( files.map((f) => path.relative(API, f).replace(/\\/g, "/").replace(/\/route\.ts$/, "")) ); const stale = Object.keys(EXEMPT).filter((k) => !known.has(k)); expect(stale, `Ausnahmen ohne zugehörigen Endpunkt: ${stale.join(", ")}`).toEqual([]); }); });