import { describe, expect, it } from "vitest"; import { phasenAusfuehren, type EreignisSenke } from "@/engine/ablauf/ablauf"; import { AblaufFehler } from "@/engine/ablauf/fehler"; import { ProxmoxFehler } from "@/engine/proxmox/fehler"; /** Eine Senke, die mitschreibt – so sieht ein Test genau, was Web und CLI zu sehen bekämen. */ function mitschreiber() { const ereignisse: string[] = []; const senke: EreignisSenke = { phasenAnlegen: async (p) => void ereignisse.push(`anlegen ${p.map((x) => x.schluessel).join(",")}`), phaseGestartet: async (s) => void ereignisse.push(`start ${s}`), phaseDetail: async (s, d) => void ereignisse.push(`detail ${s}: ${d}`), phaseBeendet: async (s, status, a) => void ereignisse.push( `ende ${s} ${status}${a.detail ? `: ${a.detail}` : ""}${a.fehler ? ` – ${a.fehler.ursache} → ${a.fehler.hinweis}` : ""}`, ), log: async (stufe, n) => void ereignisse.push(`log ${stufe} ${n}`), }; return { senke, ereignisse }; } describe("Phasen-Runner", () => { it("führt Phasen der Reihe nach aus und meldet Detail, Überspringen und Erfolg", async () => { const { senke, ereignisse } = mitschreiber(); const ergebnis = await phasenAusfuehren( [ { schluessel: "pruefen", titel: "Prüfungen", ausfuehren: async () => ({ detail: "VMID 120 frei" }) }, { schluessel: "template", titel: "Template", ausfuehren: async () => ({ uebersprungen: true, detail: "9013 vorhanden" }), }, { schluessel: "klonen", titel: "Klonen", ausfuehren: async (k) => { await k.detail("läuft …"); await k.log("clone gestartet"); return { detail: "full clone 9013 → 120" }; }, }, ], senke, ); expect(ergebnis).toEqual({ ok: true }); expect(ereignisse).toEqual([ "anlegen pruefen,template,klonen", "start pruefen", "ende pruefen ERLEDIGT: VMID 120 frei", "start template", "ende template UEBERSPRUNGEN: 9013 vorhanden", "start klonen", "detail klonen: läuft …", "log info clone gestartet", "ende klonen ERLEDIGT: full clone 9013 → 120", ]); }); it("bricht bei einem Fehler ab: Ursache und Hinweis, folgende Phasen bleiben offen", async () => { const { senke, ereignisse } = mitschreiber(); let danach = false; const ergebnis = await phasenAusfuehren( [ { schluessel: "klonen", titel: "Klonen", ausfuehren: async () => { throw new ProxmoxFehler( "recht", "Permission check failed (/vms/120, VM.Clone)", "Der Rolle fehlt VM.Clone auf /vms/120.", ); }, }, { schluessel: "start", titel: "Start", ausfuehren: async () => void (danach = true) }, ], senke, ); expect(ergebnis).toMatchObject({ ok: false, abgebrochen: false, phase: "klonen" }); expect(danach).toBe(false); expect(ereignisse).toContain( "ende klonen FEHLER – Permission check failed (/vms/120, VM.Clone) → Der Rolle fehlt VM.Clone auf /vms/120.", ); expect(ereignisse.some((e) => e.startsWith("start start"))).toBe(false); }); it("prüft vor jeder Phase auf Abbruch", async () => { const { senke } = mitschreiber(); let abbrechen = false; const ergebnis = await phasenAusfuehren( [ { schluessel: "a", titel: "A", ausfuehren: async () => void (abbrechen = true) }, { schluessel: "b", titel: "B", ausfuehren: async () => ({}) }, ], senke, { abbruchGewuenscht: async () => abbrechen }, ); expect(ergebnis).toMatchObject({ ok: false, abgebrochen: true, phase: "b" }); }); it("benennt unerwartete Fehler ehrlich", async () => { const { senke } = mitschreiber(); const ergebnis = await phasenAusfuehren( [ { schluessel: "x", titel: "X", ausfuehren: async () => { throw new TypeError("kaputt"); }, }, ], senke, ); expect(ergebnis.ok || ergebnis.fehler.ursache).toBe("Unerwarteter Fehler: kaputt"); const bewusst = await phasenAusfuehren( [ { schluessel: "y", titel: "Y", ausfuehren: async () => { throw new AblaufFehler("U", "H"); }, }, ], senke, ); expect(bewusst.ok || bewusst.fehler).toEqual({ ursache: "U", hinweis: "H" }); }); });