/** * Tests for nodes tRPC procedures (Docker Swarm monitoring). * These tests mock the Go Gateway HTTP calls so they run without a real Gateway. */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { getGatewayNodes, getGatewayNodeStats, type GatewayNodesResult, type GatewayNodeStatsResult, } from "./gateway-proxy"; // ─── Mock fetch ─────────────────────────────────────────────────────────────── const mockFetch = vi.fn(); vi.stubGlobal("fetch", mockFetch); function mockResponse(data: unknown, status = 200) { return { ok: status >= 200 && status < 300, status, json: async () => data, text: async () => JSON.stringify(data), }; } // ─── Test data ──────────────────────────────────────────────────────────────── const MOCK_NODES_RESULT: GatewayNodesResult = { nodes: [ { id: "abc123def456", hostname: "goclaw-manager-01", role: "manager", status: "ready", availability: "active", ip: "192.168.1.10", os: "linux", arch: "x86_64", cpuCores: 4, memTotalMB: 8192, dockerVersion: "24.0.7", isLeader: true, managerAddr: "192.168.1.10:2377", labels: { env: "production" }, updatedAt: "2026-03-21T10:00:00Z", }, { id: "def789abc012", hostname: "goclaw-worker-01", role: "worker", status: "ready", availability: "active", ip: "192.168.1.11", os: "linux", arch: "x86_64", cpuCores: 8, memTotalMB: 16384, dockerVersion: "24.0.7", isLeader: false, labels: {}, updatedAt: "2026-03-21T10:00:00Z", }, ], count: 2, swarmActive: true, managers: 1, totalNodes: 2, fetchedAt: "2026-03-21T10:00:00Z", }; const MOCK_STATS_RESULT: GatewayNodeStatsResult = { stats: [ { id: "abc123", name: "goclaw-gateway", cpuPct: 12.5, memUseMB: 256.0, memLimMB: 2048.0, memPct: 12.5, }, { id: "def456", name: "goclaw-control-center", cpuPct: 5.2, memUseMB: 128.0, memLimMB: 1024.0, memPct: 12.5, }, ], count: 2, fetchedAt: "2026-03-21T10:00:00Z", }; // ─── Tests ──────────────────────────────────────────────────────────────────── describe("getGatewayNodes", () => { beforeEach(() => { mockFetch.mockReset(); }); it("returns nodes list when gateway is available", async () => { mockFetch.mockResolvedValueOnce(mockResponse(MOCK_NODES_RESULT)); const result = await getGatewayNodes(); expect(result).not.toBeNull(); expect(result!.nodes).toHaveLength(2); expect(result!.swarmActive).toBe(true); expect(result!.count).toBe(2); }); it("returns manager node with isLeader=true", async () => { mockFetch.mockResolvedValueOnce(mockResponse(MOCK_NODES_RESULT)); const result = await getGatewayNodes(); const manager = result!.nodes.find((n) => n.role === "manager"); expect(manager).toBeDefined(); expect(manager!.isLeader).toBe(true); expect(manager!.hostname).toBe("goclaw-manager-01"); expect(manager!.managerAddr).toBe("192.168.1.10:2377"); }); it("returns worker node with correct resources", async () => { mockFetch.mockResolvedValueOnce(mockResponse(MOCK_NODES_RESULT)); const result = await getGatewayNodes(); const worker = result!.nodes.find((n) => n.role === "worker"); expect(worker).toBeDefined(); expect(worker!.cpuCores).toBe(8); expect(worker!.memTotalMB).toBe(16384); expect(worker!.isLeader).toBe(false); }); it("returns null when gateway is unreachable", async () => { mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED")); const result = await getGatewayNodes(); expect(result).toBeNull(); }); it("returns null on HTTP error status", async () => { mockFetch.mockResolvedValueOnce(mockResponse({ error: "internal error" }, 500)); const result = await getGatewayNodes(); expect(result).toBeNull(); }); it("calls correct gateway endpoint", async () => { mockFetch.mockResolvedValueOnce(mockResponse(MOCK_NODES_RESULT)); await getGatewayNodes(); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining("/api/nodes"), expect.any(Object) ); }); }); describe("getGatewayNodeStats", () => { beforeEach(() => { mockFetch.mockReset(); }); it("returns container stats when gateway is available", async () => { mockFetch.mockResolvedValueOnce(mockResponse(MOCK_STATS_RESULT)); const result = await getGatewayNodeStats(); expect(result).not.toBeNull(); expect(result!.stats).toHaveLength(2); expect(result!.count).toBe(2); }); it("returns correct CPU and memory stats", async () => { mockFetch.mockResolvedValueOnce(mockResponse(MOCK_STATS_RESULT)); const result = await getGatewayNodeStats(); const gateway = result!.stats.find((s) => s.name === "goclaw-gateway"); expect(gateway).toBeDefined(); expect(gateway!.cpuPct).toBe(12.5); expect(gateway!.memUseMB).toBe(256.0); expect(gateway!.memLimMB).toBe(2048.0); expect(gateway!.memPct).toBe(12.5); }); it("returns null when gateway is unreachable", async () => { mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED")); const result = await getGatewayNodeStats(); expect(result).toBeNull(); }); it("returns null on HTTP error status", async () => { mockFetch.mockResolvedValueOnce(mockResponse({ error: "not found" }, 404)); const result = await getGatewayNodeStats(); expect(result).toBeNull(); }); it("calls correct gateway endpoint", async () => { mockFetch.mockResolvedValueOnce(mockResponse(MOCK_STATS_RESULT)); await getGatewayNodeStats(); expect(mockFetch).toHaveBeenCalledWith( expect.stringContaining("/api/nodes/stats"), expect.any(Object) ); }); }); describe("nodes data validation", () => { it("standalone mode returns swarmActive=false", async () => { const standaloneResult: GatewayNodesResult = { nodes: [ { id: "local-01", hostname: "localhost", role: "standalone", status: "ready", availability: "active", ip: "127.0.0.1", os: "", arch: "", cpuCores: 0, memTotalMB: 0, dockerVersion: "unknown", isLeader: false, labels: {}, updatedAt: new Date().toISOString(), }, ], count: 1, swarmActive: false, fetchedAt: new Date().toISOString(), }; mockFetch.mockResolvedValueOnce(mockResponse(standaloneResult)); const result = await getGatewayNodes(); expect(result!.swarmActive).toBe(false); expect(result!.nodes[0].role).toBe("standalone"); }); it("node labels are preserved correctly", async () => { mockFetch.mockResolvedValueOnce(mockResponse(MOCK_NODES_RESULT)); const result = await getGatewayNodes(); const manager = result!.nodes[0]; expect(manager.labels).toEqual({ env: "production" }); }); it("worker node has empty labels object", async () => { mockFetch.mockResolvedValueOnce(mockResponse(MOCK_NODES_RESULT)); const result = await getGatewayNodes(); const worker = result!.nodes[1]; expect(worker.labels).toEqual({}); }); });