Просмотр исходного кода

feat(providers): add endpoint status mapping

ding113 2 недель назад
Родитель
Сommit
10065cc80e

+ 107 - 0
src/app/[locale]/settings/providers/_components/endpoint-status.ts

@@ -0,0 +1,107 @@
+import {
+  AlertTriangle,
+  Ban,
+  CheckCircle2,
+  HelpCircle,
+  type LucideIcon,
+  XCircle,
+} from "lucide-react";
+import type { ProviderEndpoint } from "@/types/provider";
+
+export type EndpointCircuitState = "closed" | "open" | "half-open";
+
+export type EndpointStatusSeverity = "success" | "error" | "warning" | "neutral";
+
+export type EndpointStatusToken =
+  | "healthy"
+  | "unhealthy"
+  | "unknown"
+  | "circuit-open"
+  | "circuit-half-open";
+
+export interface EndpointStatusModel {
+  status: EndpointStatusToken;
+  labelKey: string;
+  severity: EndpointStatusSeverity;
+  icon: LucideIcon;
+  color: string;
+  bgColor: string;
+  borderColor: string;
+}
+
+/**
+ * Determines the UI status model for an endpoint based on its probe snapshot and circuit state.
+ *
+ * Logic:
+ * 1. Circuit Open -> 'circuit-open' (Error)
+ * 2. Circuit Half-Open -> 'circuit-half-open' (Warning)
+ * 3. Circuit Closed (or missing):
+ *    - lastProbeOk === true -> 'healthy' (Success)
+ *    - lastProbeOk === false -> 'unhealthy' (Error)
+ *    - lastProbeOk === null -> 'unknown' (Neutral)
+ */
+export function getEndpointStatusModel(
+  endpoint: Pick<ProviderEndpoint, "lastProbeOk">,
+  circuitState?: EndpointCircuitState | null
+): EndpointStatusModel {
+  // 1. Circuit Breaker Priority
+  if (circuitState === "open") {
+    return {
+      status: "circuit-open",
+      labelKey: "settings.providers.endpointStatus.circuitOpen",
+      severity: "error",
+      icon: Ban,
+      color: "text-rose-500",
+      bgColor: "bg-rose-500/10",
+      borderColor: "border-rose-500/30",
+    };
+  }
+
+  if (circuitState === "half-open") {
+    return {
+      status: "circuit-half-open",
+      labelKey: "settings.providers.endpointStatus.circuitHalfOpen",
+      severity: "warning",
+      icon: AlertTriangle,
+      color: "text-amber-500",
+      bgColor: "bg-amber-500/10",
+      borderColor: "border-amber-500/30",
+    };
+  }
+
+  // 2. Probe Status Fallback (Circuit Closed)
+  if (endpoint.lastProbeOk === true) {
+    return {
+      status: "healthy",
+      labelKey: "settings.providers.endpointStatus.healthy",
+      severity: "success",
+      icon: CheckCircle2,
+      color: "text-emerald-500",
+      bgColor: "bg-emerald-500/10",
+      borderColor: "border-emerald-500/30",
+    };
+  }
+
+  if (endpoint.lastProbeOk === false) {
+    return {
+      status: "unhealthy",
+      labelKey: "settings.providers.endpointStatus.unhealthy",
+      severity: "error",
+      icon: XCircle,
+      color: "text-rose-500",
+      bgColor: "bg-rose-500/10",
+      borderColor: "border-rose-500/30",
+    };
+  }
+
+  // 3. Unknown
+  return {
+    status: "unknown",
+    labelKey: "settings.providers.endpointStatus.unknown",
+    severity: "neutral",
+    icon: HelpCircle,
+    color: "text-slate-400",
+    bgColor: "bg-slate-400/10",
+    borderColor: "border-slate-400/30",
+  };
+}

+ 101 - 0
tests/unit/settings/providers/endpoint-status.test.ts

@@ -0,0 +1,101 @@
+import { describe, expect, it } from "vitest";
+import {
+  type EndpointCircuitState,
+  getEndpointStatusModel,
+} from "@/app/[locale]/settings/providers/_components/endpoint-status";
+import { AlertTriangle, Ban, CheckCircle2, HelpCircle, XCircle } from "lucide-react";
+
+describe("getEndpointStatusModel", () => {
+  const createEndpoint = (lastProbeOk: boolean | null) => ({ lastProbeOk });
+
+  describe("Circuit Breaker Priority", () => {
+    it("should return circuit-open status when circuit is open, regardless of probe", () => {
+      const endpoint = createEndpoint(true); // Probe is OK
+      const result = getEndpointStatusModel(endpoint, "open");
+
+      expect(result).toEqual({
+        status: "circuit-open",
+        labelKey: "settings.providers.endpointStatus.circuitOpen",
+        severity: "error",
+        icon: Ban,
+        color: "text-rose-500",
+        bgColor: "bg-rose-500/10",
+        borderColor: "border-rose-500/30",
+      });
+    });
+
+    it("should return circuit-half-open status when circuit is half-open", () => {
+      const endpoint = createEndpoint(false); // Probe is bad
+      const result = getEndpointStatusModel(endpoint, "half-open");
+
+      expect(result).toEqual({
+        status: "circuit-half-open",
+        labelKey: "settings.providers.endpointStatus.circuitHalfOpen",
+        severity: "warning",
+        icon: AlertTriangle,
+        color: "text-amber-500",
+        bgColor: "bg-amber-500/10",
+        borderColor: "border-amber-500/30",
+      });
+    });
+  });
+
+  describe("Probe Status Fallback (Circuit Closed or Missing)", () => {
+    it.each([
+      { circuit: "closed" as EndpointCircuitState },
+      { circuit: null },
+      { circuit: undefined },
+    ])("should return healthy when probe is ok and circuit is $circuit", ({ circuit }) => {
+      const endpoint = createEndpoint(true);
+      const result = getEndpointStatusModel(endpoint, circuit);
+
+      expect(result).toEqual({
+        status: "healthy",
+        labelKey: "settings.providers.endpointStatus.healthy",
+        severity: "success",
+        icon: CheckCircle2,
+        color: "text-emerald-500",
+        bgColor: "bg-emerald-500/10",
+        borderColor: "border-emerald-500/30",
+      });
+    });
+
+    it.each([
+      { circuit: "closed" as EndpointCircuitState },
+      { circuit: null },
+      { circuit: undefined },
+    ])("should return unhealthy when probe is failed and circuit is $circuit", ({ circuit }) => {
+      const endpoint = createEndpoint(false);
+      const result = getEndpointStatusModel(endpoint, circuit);
+
+      expect(result).toEqual({
+        status: "unhealthy",
+        labelKey: "settings.providers.endpointStatus.unhealthy",
+        severity: "error",
+        icon: XCircle,
+        color: "text-rose-500",
+        bgColor: "bg-rose-500/10",
+        borderColor: "border-rose-500/30",
+      });
+    });
+
+    it.each([
+      { circuit: "closed" as EndpointCircuitState },
+      { circuit: null },
+      { circuit: undefined },
+    ])("should return unknown when probe is null and circuit is $circuit", ({ circuit }) => {
+      const endpoint = createEndpoint(null);
+      const result = getEndpointStatusModel(endpoint, circuit);
+
+      expect(result).toEqual({
+        status: "unknown",
+        labelKey: "settings.providers.endpointStatus.unknown",
+        severity: "neutral",
+        icon: HelpCircle,
+        color: "text-slate-400",
+        bgColor: "bg-slate-400/10",
+        borderColor: "border-slate-400/30",
+      });
+    });
+  });
+});