Files
Felis/panel/src/components/PhaseBadge.test.ts
T

62 lines
2.7 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { PHASE_COLOR, phaseColor, phaseVariant, startFailure } from "@/components/PhaseBadge";
import type { Phase } from "@/lib/types";
// The closed lifecycle set felis-api can emit — the Go source of truth is
// internal/apis/felis/v1alpha1 MinecraftServerPhase (Unknown/Stopped/Starting/
// Running/Stopping/Failed), passed through verbatim by the status projection
// (internal/api/k8scluster.go). `satisfies readonly Phase[]` is the real guard:
// it fails the build if any of these strings drops out of the TS Phase union, the
// exact desync that left a "Stopping" server rendering with no variant/colour.
// tsc enforces map↔union agreement, but ONLY this pins union↔backend agreement.
const BACKEND_PHASES = [
"Unknown",
"Stopped",
"Starting",
"Running",
"Stopping",
"Failed",
] as const satisfies readonly Phase[];
describe("phase palette parity", () => {
it("resolves every backend phase to a concrete colour and variant", () => {
for (const phase of BACKEND_PHASES) {
expect(phaseColor(phase), `colour for ${phase}`).toMatch(/^#[0-9a-f]{6}$/i);
expect(phaseVariant(phase), `variant for ${phase}`).toBeTruthy();
}
});
it("gives every phase a distinct hue so transitions are legible", () => {
const hues = BACKEND_PHASES.map((p) => phaseColor(p));
expect(new Set(hues).size).toBe(BACKEND_PHASES.length);
});
});
describe("unmodelled phase fallback", () => {
// `phase` crosses an unvalidated JSON boundary (api.status() is parsed, never
// schema-checked), so the backend emitting a phase the panel predates must NOT
// produce an undefined variant/colour — it degrades to the neutral Unknown
// styling. Casting simulates that out-of-union runtime value.
const future = "Hibernating" as Phase;
it("degrades an out-of-union phase to the Unknown colour", () => {
expect(phaseColor(future)).toBe(PHASE_COLOR.Unknown);
});
it("degrades an out-of-union phase to the Unknown variant", () => {
expect(phaseVariant(future)).toBe(phaseVariant("Unknown"));
});
});
describe("start failure", () => {
// A Failed phase only reads as a failed start while the server still wants to
// run; a stopped Failed server needs nothing from its owner.
it("reads a failed start only while the server wants to run", () => {
expect(startFailure({ phase: "Running", desiredState: "Running" })).toBeNull();
expect(startFailure({ phase: "Failed", desiredState: "Stopped" })).toBeNull();
expect(startFailure({ phase: "Failed" })).toBeNull();
expect(startFailure({ phase: "Failed", desiredState: "Running" })).toBe("retrying");
expect(startFailure({ phase: "Failed", desiredState: "Running", startGaveUp: true })).toBe("gaveUp");
});
});