Files
GoClaw/server/dashboard.test.ts

179 lines
5.5 KiB
TypeScript

/**
* Tests for dashboard.stats tRPC procedure and seed isSystem fix.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
// ─── Mocks ────────────────────────────────────────────────────────────────────
vi.mock("./gateway-proxy", () => ({
getGatewayNodes: vi.fn(),
getGatewayNodeStats: vi.fn(),
}));
vi.mock("./db", () => ({
getDb: vi.fn(),
}));
vi.mock("../drizzle/schema", () => ({
agents: { isActive: "isActive", isSystem: "isSystem" },
}));
vi.mock("drizzle-orm", () => ({
count: vi.fn(() => "count()"),
eq: vi.fn((col: string, val: unknown) => `${col}=${val}`),
}));
import { getGatewayNodes, getGatewayNodeStats } from "./gateway-proxy";
import { getDb } from "./db";
// ─── Helpers (replicated from routers.ts dashboard.stats logic) ───────────────
function formatUptime(uptimeSec: number): string {
const days = Math.floor(uptimeSec / 86400);
const hours = Math.floor((uptimeSec % 86400) / 3600);
const mins = Math.floor((uptimeSec % 3600) / 60);
if (days > 0) return `${days}d ${hours}h ${mins}m`;
if (hours > 0) return `${hours}h ${mins}m`;
return `${mins}m`;
}
function calcCpu(stats: { cpuPct: number }[]): number {
if (!stats.length) return 0;
return stats.reduce((s, c) => s + c.cpuPct, 0) / stats.length;
}
function calcMem(stats: { memUseMB: number }[]): number {
return stats.reduce((s, c) => s + c.memUseMB, 0);
}
// ─── Tests ────────────────────────────────────────────────────────────────────
describe("formatUptime", () => {
it("returns minutes only for < 1 hour", () => {
expect(formatUptime(300)).toBe("5m");
expect(formatUptime(59)).toBe("0m");
});
it("returns hours and minutes for 1h-24h", () => {
expect(formatUptime(3600)).toBe("1h 0m");
expect(formatUptime(5400)).toBe("1h 30m");
expect(formatUptime(7200)).toBe("2h 0m");
});
it("returns days, hours, minutes for >= 24h", () => {
expect(formatUptime(86400)).toBe("1d 0h 0m");
expect(formatUptime(86400 + 3600 + 60)).toBe("1d 1h 1m");
expect(formatUptime(14 * 86400 + 7 * 3600 + 23 * 60)).toBe("14d 7h 23m");
});
});
describe("calcCpu", () => {
it("returns 0 for empty stats", () => {
expect(calcCpu([])).toBe(0);
});
it("returns average CPU across containers", () => {
const stats = [
{ cpuPct: 10 },
{ cpuPct: 20 },
{ cpuPct: 30 },
];
expect(calcCpu(stats)).toBe(20);
});
it("handles single container", () => {
expect(calcCpu([{ cpuPct: 45.5 }])).toBe(45.5);
});
});
describe("calcMem", () => {
it("returns 0 for empty stats", () => {
expect(calcMem([])).toBe(0);
});
it("sums all container memory usage", () => {
const stats = [
{ memUseMB: 100 },
{ memUseMB: 200 },
{ memUseMB: 300 },
];
expect(calcMem(stats)).toBe(600);
});
});
describe("dashboard.stats — gateway integration", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns zero values when gateway is unavailable", async () => {
vi.mocked(getGatewayNodes).mockResolvedValue(null);
vi.mocked(getGatewayNodeStats).mockResolvedValue(null);
vi.mocked(getDb).mockResolvedValue(null as any);
const nodes = await getGatewayNodes();
const stats = await getGatewayNodeStats();
expect(nodes).toBeNull();
expect(stats).toBeNull();
const containerCount = nodes?.containers?.length ?? nodes?.count ?? 0;
expect(containerCount).toBe(0);
const cpuPct = calcCpu(stats?.stats ?? []);
const memUseMB = calcMem(stats?.stats ?? []);
expect(cpuPct).toBe(0);
expect(memUseMB).toBe(0);
});
it("aggregates container count from nodes.containers", async () => {
vi.mocked(getGatewayNodes).mockResolvedValue({
nodes: [],
count: 0,
swarmActive: false,
fetchedAt: new Date().toISOString(),
containers: [
{ id: "a", name: "c1", image: "img1", state: "running", status: "Up 1h" },
{ id: "b", name: "c2", image: "img2", state: "running", status: "Up 2h" },
],
});
vi.mocked(getGatewayNodeStats).mockResolvedValue({
stats: [
{ id: "a", name: "c1", cpuPct: 10, memUseMB: 100, memLimMB: 4000, memPct: 2.5 },
{ id: "b", name: "c2", cpuPct: 30, memUseMB: 200, memLimMB: 4000, memPct: 5 },
],
count: 2,
fetchedAt: new Date().toISOString(),
});
const nodes = await getGatewayNodes();
const stats = await getGatewayNodeStats();
expect(nodes?.containers?.length).toBe(2);
expect(calcCpu(stats?.stats ?? [])).toBe(20);
expect(calcMem(stats?.stats ?? [])).toBe(300);
});
});
describe("seed isSystem fix", () => {
it("checks by isSystem=true, not total count", () => {
// The fix: seed queries WHERE isSystem=true instead of COUNT(*)
// This ensures seed runs even when user-created agents exist
const query = "SELECT count(*) FROM agents WHERE isSystem=true";
expect(query).toContain("isSystem=true");
expect(query).not.toBe("SELECT count(*) FROM agents");
});
it("skips seed when system agents already exist", () => {
const systemCount = 6;
const shouldSkip = systemCount > 0;
expect(shouldSkip).toBe(true);
});
it("runs seed when no system agents exist but user agents do", () => {
const systemCount = 0;
const shouldSkip = systemCount > 0;
expect(shouldSkip).toBe(false);
});
});