Files
florian.wildeandClaude Opus 5.5 7f6cf00a0e feat(verlauf): Filter, Wiederholen und Aufräumen
Verlauf filterbar (URL, API, proxdeploy history); Wiederholen füllt den Assistenten aus einem alten Auftrag; Aufräumen löscht nach Fehlschlag oder Abbruch den halb angelegten Gast – nur, wenn dessen Beschreibung auf den Auftrag verweist. Deshalb setzt das Klonen die Beschreibung jetzt sofort. Auf pve-demo geprüft (CT 109).

Closes #014

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:39:58 +02:00

163 lines
5.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { afterEach, describe, expect, it } from "vitest";
import { phasenAusfuehren, type EreignisSenke } from "@/engine/ablauf/ablauf";
import { aufraeumSchritte } from "@/engine/ablaeufe/aufraeumen";
import { ProxmoxClient } from "@/engine/proxmox/client";
import { MOCK_TOKEN_GEHEIMNIS, MOCK_TOKEN_ID, ProxmoxMock } from "../proxmox-mock/mock";
import { szenarioEinrichten } from "../proxmox-mock/szenario";
const UPID = (art: string) => `UPID:pve02:00001234:00ABCDEF:6710F00D:${art}:130:proxdeploy@pve!test:`;
const aufraeumen: Array<() => Promise<void>> = [];
afterEach(async () => {
while (aufraeumen.length) await aufraeumen.pop()!();
});
type Gast = {
type: "qemu" | "lxc";
node: string;
status: string;
template?: number;
config: Record<string, unknown>;
};
async function aufbauen(gast: Gast | null) {
const mock = await new ProxmoxMock().starten();
szenarioEinrichten(mock, { version: "9.2.1" });
mock
.route("GET", "/cluster/resources", () => ({
data: [
{ vmid: 100, type: "qemu", node: "pve01", status: "running", name: "anderer" },
...(gast
? [
{
vmid: 120,
type: gast.type,
node: gast.node,
status: gast.status,
name: "web01",
template: gast.template ?? 0,
},
]
: []),
],
}))
.route("GET", /^\/nodes\/pve02\/(qemu|lxc)\/120\/config$/, () => ({ data: gast?.config ?? {} }))
.route("POST", /^\/nodes\/pve02\/(qemu|lxc)\/120\/status\/stop$/, () => ({ data: UPID("qmstop") }))
.route("DELETE", /^\/nodes\/pve02\/(qemu|lxc)\/120$/, () => ({ data: UPID("qmdestroy") }))
.route("GET", /\/tasks\/[^/]+\/status$/, () => ({ data: { status: "stopped", exitstatus: "OK" } }));
const client = new ProxmoxClient({
url: mock.url,
tokenId: MOCK_TOKEN_ID,
tokenGeheimnis: MOCK_TOKEN_GEHEIMNIS,
tlsPruefen: false,
fingerabdruck: mock.fingerabdruck,
});
aufraeumen.push(async () => {
await client.schliessen();
await mock.stoppen();
});
const phasen: Record<string, { status: string; detail?: string; ursache?: string }> = {};
const senke: EreignisSenke = {
phasenAnlegen: async () => {},
phaseGestartet: async () => {},
phaseDetail: async () => {},
phaseBeendet: async (s, status, a) => {
phasen[s] = { status, detail: a.detail, ursache: a.fehler?.ursache };
},
log: async () => {},
};
const ausfuehren = (typ: "VM" | "LXC" = "VM") =>
phasenAusfuehren(
aufraeumSchritte({ client, typ, vmid: 120, knoten: "pve01", auftragKurzId: "d-abcde" }),
senke,
);
const schreibend = () =>
mock.anfragen.filter((a) => a.methode !== "GET").map((a) => `${a.methode} ${a.pfad}`);
return { mock, ausfuehren, phasen, schreibend };
}
const EIGEN = "Ausgerollt mit ProxDeploy (Auftrag d-abcde)\nDistribution: Debian 13";
describe("Aufräumen", () => {
it("stoppt und löscht den eigenen Gast – auch wenn er inzwischen auf einem anderen Node liegt", async () => {
const t = await aufbauen({
type: "qemu",
node: "pve02",
status: "running",
config: { name: "web01", description: EIGEN },
});
expect((await t.ausfuehren()).ok).toBe(true);
expect(t.schreibend()).toEqual([
"POST /nodes/pve02/qemu/120/status/stop",
"DELETE /nodes/pve02/qemu/120",
]);
const loeschen = t.mock.anfragen.find((a) => a.methode === "DELETE")!;
expect(loeschen.abfrage.get("destroy-unreferenced-disks")).toBe("1");
expect(t.phasen["loeschen"]!.detail).toBe("VM 120 mit Disks gelöscht");
});
it("Container, der nicht läuft: Stoppen übersprungen", async () => {
const t = await aufbauen({
type: "lxc",
node: "pve02",
status: "stopped",
config: { hostname: "ct01", description: EIGEN },
});
expect((await t.ausfuehren("LXC")).ok).toBe(true);
expect(t.phasen["stoppen"]!.status).toBe("UEBERSPRUNGEN");
expect(t.schreibend()).toEqual(["DELETE /nodes/pve02/lxc/120"]);
});
it("schon weg: Erfolg ohne Löschen", async () => {
const t = await aufbauen(null);
expect((await t.ausfuehren()).ok).toBe(true);
expect(t.phasen["pruefungen"]!.detail).toMatch(/gibt es nicht mehr/);
expect(t.schreibend()).toEqual([]);
});
it.each([
[
"fremde Beschreibung",
{ type: "qemu", node: "pve02", status: "running", config: { description: "Produktiv-DB" } },
/gehört nicht erkennbar/,
],
[
"anderer Auftrag",
{
type: "qemu",
node: "pve02",
status: "running",
config: { description: "Ausgerollt mit ProxDeploy (Auftrag d-zzzzz)" },
},
/gehört nicht erkennbar/,
],
[
"Template",
{
type: "qemu",
node: "pve02",
status: "stopped",
template: 1,
config: { description: EIGEN, template: 1 },
},
/ist ein Template/,
],
[
"gesperrt",
{ type: "qemu", node: "pve02", status: "stopped", config: { description: EIGEN, lock: "clone" } },
/gesperrt \(clone\)/,
],
[
"falscher Typ",
{ type: "lxc", node: "pve02", status: "stopped", config: { description: EIGEN } },
/kein VM/,
],
] as Array<[string, Gast, RegExp]>)("%s: bricht ab und fasst nichts an", async (_name, gast, meldung) => {
const t = await aufbauen(gast);
const ergebnis = await t.ausfuehren("VM");
expect(ergebnis.ok).toBe(false);
expect(t.phasen["pruefungen"]!.ursache).toMatch(meldung);
expect(t.schreibend()).toEqual([]);
});
});