Versionierung und Aenderungshistorie je Szenario
Deploy App / deploy (push) Successful in 1m48s

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:
2026-07-19 22:09:10 +02:00
parent d04e07fdfb
commit d023534a03
28 changed files with 2021 additions and 13 deletions
+81
View File
@@ -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([]);
});
});