f22b0a3f27
Deploy App / deploy (push) Successful in 1m55s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
89 lines
3.7 KiB
TypeScript
89 lines
3.7 KiB
TypeScript
// 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<string, string> = {
|
||
"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([]);
|
||
});
|
||
});
|