From 5a2e404f03bdfd93d65274d3892028b18e1ed76a Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sat, 7 Mar 2026 12:02:59 -0600 Subject: [PATCH] test(orchestrator): add service unit tests for agent services --- .../api/agents/agent-control.service.spec.ts | 140 ++++++++++++++++++ .../api/agents/agent-messages.service.spec.ts | 103 +++++++++++++ .../src/api/agents/agent-tree.service.spec.ts | 106 +++++++++++++ 3 files changed, 349 insertions(+) create mode 100644 apps/orchestrator/src/api/agents/agent-control.service.spec.ts create mode 100644 apps/orchestrator/src/api/agents/agent-messages.service.spec.ts create mode 100644 apps/orchestrator/src/api/agents/agent-tree.service.spec.ts diff --git a/apps/orchestrator/src/api/agents/agent-control.service.spec.ts b/apps/orchestrator/src/api/agents/agent-control.service.spec.ts new file mode 100644 index 0000000..641e881 --- /dev/null +++ b/apps/orchestrator/src/api/agents/agent-control.service.spec.ts @@ -0,0 +1,140 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { AgentControlService } from "./agent-control.service"; +import { PrismaService } from "../../prisma/prisma.service"; + +describe("AgentControlService", () => { + let service: AgentControlService; + let prisma: { + agentSessionTree: { + findUnique: ReturnType; + updateMany: ReturnType; + }; + agentConversationMessage: { + create: ReturnType; + }; + operatorAuditLog: { + create: ReturnType; + }; + }; + + beforeEach(() => { + prisma = { + agentSessionTree: { + findUnique: vi.fn(), + updateMany: vi.fn().mockResolvedValue({ count: 1 }), + }, + agentConversationMessage: { + create: vi.fn().mockResolvedValue(undefined), + }, + operatorAuditLog: { + create: vi.fn().mockResolvedValue(undefined), + }, + }; + + service = new AgentControlService(prisma as unknown as PrismaService); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe("injectMessage", () => { + it("creates conversation message and audit log when tree entry exists", async () => { + prisma.agentSessionTree.findUnique.mockResolvedValue({ id: "tree-1" }); + + await service.injectMessage("agent-123", "operator-abc", "Please continue"); + + expect(prisma.agentSessionTree.findUnique).toHaveBeenCalledWith({ + where: { sessionId: "agent-123" }, + select: { id: true }, + }); + expect(prisma.agentConversationMessage.create).toHaveBeenCalledWith({ + data: { + sessionId: "agent-123", + role: "operator", + content: "Please continue", + provider: "internal", + metadata: {}, + }, + }); + expect(prisma.operatorAuditLog.create).toHaveBeenCalledWith({ + data: { + sessionId: "agent-123", + userId: "operator-abc", + provider: "internal", + action: "inject", + metadata: { + payload: { + message: "Please continue", + }, + }, + }, + }); + }); + + it("creates only audit log when no tree entry exists", async () => { + prisma.agentSessionTree.findUnique.mockResolvedValue(null); + + await service.injectMessage("agent-456", "operator-def", "Nudge message"); + + expect(prisma.agentConversationMessage.create).not.toHaveBeenCalled(); + expect(prisma.operatorAuditLog.create).toHaveBeenCalledWith({ + data: { + sessionId: "agent-456", + userId: "operator-def", + provider: "internal", + action: "inject", + metadata: { + payload: { + message: "Nudge message", + }, + }, + }, + }); + }); + }); + + describe("pauseAgent", () => { + it("updates tree status to paused and creates audit log", async () => { + await service.pauseAgent("agent-789", "operator-pause"); + + expect(prisma.agentSessionTree.updateMany).toHaveBeenCalledWith({ + where: { sessionId: "agent-789" }, + data: { status: "paused" }, + }); + expect(prisma.operatorAuditLog.create).toHaveBeenCalledWith({ + data: { + sessionId: "agent-789", + userId: "operator-pause", + provider: "internal", + action: "pause", + metadata: { + payload: {}, + }, + }, + }); + }); + }); + + describe("resumeAgent", () => { + it("updates tree status to running and creates audit log", async () => { + await service.resumeAgent("agent-321", "operator-resume"); + + expect(prisma.agentSessionTree.updateMany).toHaveBeenCalledWith({ + where: { sessionId: "agent-321" }, + data: { status: "running" }, + }); + expect(prisma.operatorAuditLog.create).toHaveBeenCalledWith({ + data: { + sessionId: "agent-321", + userId: "operator-resume", + provider: "internal", + action: "resume", + metadata: { + payload: {}, + }, + }, + }); + }); + }); +}); diff --git a/apps/orchestrator/src/api/agents/agent-messages.service.spec.ts b/apps/orchestrator/src/api/agents/agent-messages.service.spec.ts new file mode 100644 index 0000000..add7c7c --- /dev/null +++ b/apps/orchestrator/src/api/agents/agent-messages.service.spec.ts @@ -0,0 +1,103 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { AgentMessagesService } from "./agent-messages.service"; +import { PrismaService } from "../../prisma/prisma.service"; + +describe("AgentMessagesService", () => { + let service: AgentMessagesService; + let prisma: { + agentConversationMessage: { + findMany: ReturnType; + count: ReturnType; + }; + }; + + beforeEach(() => { + prisma = { + agentConversationMessage: { + findMany: vi.fn(), + count: vi.fn(), + }, + }; + + service = new AgentMessagesService(prisma as unknown as PrismaService); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe("getMessages", () => { + it("returns paginated messages from Prisma", async () => { + const sessionId = "agent-123"; + const messages = [ + { + id: "msg-1", + sessionId, + provider: "internal", + role: "assistant", + content: "First message", + timestamp: new Date("2026-03-07T16:00:00.000Z"), + metadata: {}, + }, + { + id: "msg-2", + sessionId, + provider: "internal", + role: "user", + content: "Second message", + timestamp: new Date("2026-03-07T15:59:00.000Z"), + metadata: {}, + }, + ]; + + prisma.agentConversationMessage.findMany.mockResolvedValue(messages); + prisma.agentConversationMessage.count.mockResolvedValue(2); + + const result = await service.getMessages(sessionId, 50, 0); + + expect(prisma.agentConversationMessage.findMany).toHaveBeenCalledWith({ + where: { sessionId }, + orderBy: { timestamp: "desc" }, + take: 50, + skip: 0, + }); + expect(prisma.agentConversationMessage.count).toHaveBeenCalledWith({ where: { sessionId } }); + expect(result).toEqual({ + messages, + total: 2, + }); + }); + + it("applies limit and cursor (skip) correctly", async () => { + const sessionId = "agent-456"; + const limit = 10; + const cursor = 20; + + prisma.agentConversationMessage.findMany.mockResolvedValue([]); + prisma.agentConversationMessage.count.mockResolvedValue(42); + + await service.getMessages(sessionId, limit, cursor); + + expect(prisma.agentConversationMessage.findMany).toHaveBeenCalledWith({ + where: { sessionId }, + orderBy: { timestamp: "desc" }, + take: limit, + skip: cursor, + }); + }); + + it("returns empty messages array when no messages exist", async () => { + const sessionId = "agent-empty"; + + prisma.agentConversationMessage.findMany.mockResolvedValue([]); + prisma.agentConversationMessage.count.mockResolvedValue(0); + + const result = await service.getMessages(sessionId, 25, 0); + + expect(result).toEqual({ + messages: [], + total: 0, + }); + }); + }); +}); diff --git a/apps/orchestrator/src/api/agents/agent-tree.service.spec.ts b/apps/orchestrator/src/api/agents/agent-tree.service.spec.ts new file mode 100644 index 0000000..be64b04 --- /dev/null +++ b/apps/orchestrator/src/api/agents/agent-tree.service.spec.ts @@ -0,0 +1,106 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { AgentTreeService } from "./agent-tree.service"; +import { PrismaService } from "../../prisma/prisma.service"; + +describe("AgentTreeService", () => { + let service: AgentTreeService; + let prisma: { + agentSessionTree: { + findMany: ReturnType; + }; + }; + + beforeEach(() => { + prisma = { + agentSessionTree: { + findMany: vi.fn(), + }, + }; + + service = new AgentTreeService(prisma as unknown as PrismaService); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe("getTree", () => { + it("returns mapped entries from Prisma", async () => { + prisma.agentSessionTree.findMany.mockResolvedValue([ + { + id: "tree-1", + sessionId: "agent-1", + parentSessionId: "agent-root", + provider: "internal", + missionId: "mission-1", + taskId: "task-1", + taskSource: "queue", + agentType: "worker", + status: "running", + spawnedAt: new Date("2026-03-07T10:00:00.000Z"), + completedAt: new Date("2026-03-07T11:00:00.000Z"), + metadata: {}, + }, + ]); + + const result = await service.getTree(); + + expect(prisma.agentSessionTree.findMany).toHaveBeenCalledWith({ + orderBy: { spawnedAt: "desc" }, + take: 200, + }); + expect(result).toEqual([ + { + sessionId: "agent-1", + parentSessionId: "agent-root", + status: "running", + agentType: "worker", + taskSource: "queue", + spawnedAt: "2026-03-07T10:00:00.000Z", + completedAt: "2026-03-07T11:00:00.000Z", + }, + ]); + }); + + it("returns empty array when no entries exist", async () => { + prisma.agentSessionTree.findMany.mockResolvedValue([]); + + const result = await service.getTree(); + + expect(result).toEqual([]); + }); + + it("maps null parentSessionId and completedAt correctly", async () => { + prisma.agentSessionTree.findMany.mockResolvedValue([ + { + id: "tree-2", + sessionId: "agent-root", + parentSessionId: null, + provider: "internal", + missionId: null, + taskId: null, + taskSource: null, + agentType: null, + status: "spawning", + spawnedAt: new Date("2026-03-07T09:00:00.000Z"), + completedAt: null, + metadata: {}, + }, + ]); + + const result = await service.getTree(); + + expect(result).toEqual([ + { + sessionId: "agent-root", + parentSessionId: null, + status: "spawning", + agentType: null, + taskSource: null, + spawnedAt: "2026-03-07T09:00:00.000Z", + completedAt: null, + }, + ]); + }); + }); +});