All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
195 lines
5.5 KiB
TypeScript
195 lines
5.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi, afterEach } from "vitest";
|
|
import type { Response } from "express";
|
|
import { AgentStatus } from "@prisma/client";
|
|
import { OrchestratorController } from "./orchestrator.controller";
|
|
import { PrismaService } from "../prisma/prisma.service";
|
|
import { AuthGuard } from "../auth/guards/auth.guard";
|
|
|
|
describe("OrchestratorController", () => {
|
|
const mockPrismaService = {
|
|
agent: {
|
|
findMany: vi.fn(),
|
|
},
|
|
};
|
|
|
|
let controller: OrchestratorController;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
controller = new OrchestratorController(mockPrismaService as unknown as PrismaService);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
describe("getAgents", () => {
|
|
it("returns active agents with API widget shape", async () => {
|
|
mockPrismaService.agent.findMany.mockResolvedValue([
|
|
{
|
|
id: "agent-1",
|
|
name: "Planner",
|
|
status: AgentStatus.WORKING,
|
|
role: "planner",
|
|
createdAt: new Date("2026-02-28T10:00:00.000Z"),
|
|
},
|
|
]);
|
|
|
|
const result = await controller.getAgents();
|
|
|
|
expect(result).toEqual([
|
|
{
|
|
id: "agent-1",
|
|
name: "Planner",
|
|
status: AgentStatus.WORKING,
|
|
type: "planner",
|
|
createdAt: new Date("2026-02-28T10:00:00.000Z"),
|
|
},
|
|
]);
|
|
|
|
expect(mockPrismaService.agent.findMany).toHaveBeenCalledWith({
|
|
where: {
|
|
status: {
|
|
not: AgentStatus.TERMINATED,
|
|
},
|
|
},
|
|
orderBy: {
|
|
createdAt: "desc",
|
|
},
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
status: true,
|
|
role: true,
|
|
createdAt: true,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("falls back to type=agent when role is missing", async () => {
|
|
mockPrismaService.agent.findMany.mockResolvedValue([
|
|
{
|
|
id: "agent-2",
|
|
name: null,
|
|
status: AgentStatus.IDLE,
|
|
role: null,
|
|
createdAt: new Date("2026-02-28T11:00:00.000Z"),
|
|
},
|
|
]);
|
|
|
|
const result = await controller.getAgents();
|
|
|
|
expect(result[0]).toMatchObject({
|
|
id: "agent-2",
|
|
type: "agent",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("streamEvents", () => {
|
|
it("sets SSE headers and writes initial data payload", async () => {
|
|
const onHandlers: Record<string, (() => void) | undefined> = {};
|
|
const mockRes = {
|
|
setHeader: vi.fn(),
|
|
write: vi.fn(),
|
|
end: vi.fn(),
|
|
on: vi.fn((event: string, handler: () => void) => {
|
|
onHandlers[event] = handler;
|
|
return mockRes;
|
|
}),
|
|
} as unknown as Response;
|
|
|
|
mockPrismaService.agent.findMany.mockResolvedValue([
|
|
{
|
|
id: "agent-1",
|
|
name: "Worker",
|
|
status: AgentStatus.WORKING,
|
|
role: "worker",
|
|
createdAt: new Date("2026-02-28T12:00:00.000Z"),
|
|
},
|
|
]);
|
|
|
|
await controller.streamEvents(mockRes);
|
|
|
|
expect(mockRes.setHeader).toHaveBeenCalledWith("Content-Type", "text/event-stream");
|
|
expect(mockRes.setHeader).toHaveBeenCalledWith("Cache-Control", "no-cache");
|
|
expect(mockRes.setHeader).toHaveBeenCalledWith("Connection", "keep-alive");
|
|
expect(mockRes.setHeader).toHaveBeenCalledWith("X-Accel-Buffering", "no");
|
|
|
|
expect(mockRes.write).toHaveBeenCalledWith(
|
|
expect.stringContaining('"type":"agents:updated"')
|
|
);
|
|
expect(typeof onHandlers.close).toBe("function");
|
|
});
|
|
|
|
it("polls every 5 seconds and only emits when payload changes", async () => {
|
|
vi.useFakeTimers();
|
|
|
|
const onHandlers: Record<string, (() => void) | undefined> = {};
|
|
const mockRes = {
|
|
setHeader: vi.fn(),
|
|
write: vi.fn(),
|
|
end: vi.fn(),
|
|
on: vi.fn((event: string, handler: () => void) => {
|
|
onHandlers[event] = handler;
|
|
return mockRes;
|
|
}),
|
|
} as unknown as Response;
|
|
|
|
const firstPayload = [
|
|
{
|
|
id: "agent-1",
|
|
name: "Worker",
|
|
status: AgentStatus.WORKING,
|
|
role: "worker",
|
|
createdAt: new Date("2026-02-28T12:00:00.000Z"),
|
|
},
|
|
];
|
|
const secondPayload = [
|
|
{
|
|
id: "agent-1",
|
|
name: "Worker",
|
|
status: AgentStatus.WAITING,
|
|
role: "worker",
|
|
createdAt: new Date("2026-02-28T12:00:00.000Z"),
|
|
},
|
|
];
|
|
|
|
mockPrismaService.agent.findMany
|
|
.mockResolvedValueOnce(firstPayload)
|
|
.mockResolvedValueOnce(firstPayload)
|
|
.mockResolvedValueOnce(secondPayload);
|
|
|
|
await controller.streamEvents(mockRes);
|
|
|
|
// 1 initial data event
|
|
const getDataEventCalls = () =>
|
|
mockRes.write.mock.calls.filter(
|
|
(call) => typeof call[0] === "string" && call[0].startsWith("data: ")
|
|
);
|
|
|
|
expect(getDataEventCalls()).toHaveLength(1);
|
|
|
|
// No change after first poll => no new data event
|
|
await vi.advanceTimersByTimeAsync(5000);
|
|
expect(getDataEventCalls()).toHaveLength(1);
|
|
|
|
// Status changed on second poll => emits new data event
|
|
await vi.advanceTimersByTimeAsync(5000);
|
|
expect(getDataEventCalls()).toHaveLength(2);
|
|
|
|
onHandlers.close?.();
|
|
expect(mockRes.end).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe("security", () => {
|
|
it("uses AuthGuard at the controller level", () => {
|
|
const guards = Reflect.getMetadata("__guards__", OrchestratorController) as unknown[];
|
|
const guardClasses = guards.map((guard) => guard);
|
|
|
|
expect(guardClasses).toContain(AuthGuard);
|
|
});
|
|
});
|
|
});
|