Version A.B: B automatisch je Bearbeitungssitzung (10-Minuten-Fenster, unveraenderte Staende erzeugen keine), A manuell mit Pflichtkommentar. Eine Version haelt den vollstaendigen Zustand als PlanInput-JSON -- dadurch ist die Versionsauswahl in allen vier Analysewerkzeugen fast kostenlos, bei Monte-Carlo je Szenario einzeln. Wiederherstellen erhaelt die IDs (sonst verlieren Kind-Szenarien ihre Diff-Basis) und legt den Stand selbst als neue Version an. Wo ein Bezug trotzdem bricht, warnt der Dialog vorher namentlich. Statischer Waechter-Test: jeder schreibende Endpunkt loest eine Version aus. Migration gegen echtes Postgres verifiziert. Spezifikation 0.19 (3.8 und 9.28 neu), 25 Tests (139 -> 164). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
// 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",
|
||||
};
|
||||
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user