179 lines
5.5 KiB
TypeScript
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);
|
|
});
|
|
});
|