import { NotFoundException } from "@nestjs/common"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { AgentMessage, AgentSession, IAgentProvider, InjectResult } from "@mosaic/shared"; import type { PrismaService } from "../../prisma/prisma.service"; import { AgentProviderRegistry } from "../agents/agent-provider.registry"; import { MissionControlService } from "./mission-control.service"; type MockProvider = IAgentProvider & { listSessions: ReturnType; getSession: ReturnType; getMessages: ReturnType; injectMessage: ReturnType; pauseSession: ReturnType; resumeSession: ReturnType; killSession: ReturnType; streamMessages: ReturnType; }; const emptyMessageStream = async function* (): AsyncIterable { return; }; const createProvider = (providerId = "internal"): MockProvider => ({ providerId, providerType: providerId, displayName: providerId, listSessions: vi.fn().mockResolvedValue({ sessions: [], total: 0 }), getSession: vi.fn().mockResolvedValue(null), getMessages: vi.fn().mockResolvedValue([]), injectMessage: vi.fn().mockResolvedValue({ accepted: true } as InjectResult), pauseSession: vi.fn().mockResolvedValue(undefined), resumeSession: vi.fn().mockResolvedValue(undefined), killSession: vi.fn().mockResolvedValue(undefined), streamMessages: vi.fn().mockReturnValue(emptyMessageStream()), isAvailable: vi.fn().mockResolvedValue(true), }); describe("MissionControlService", () => { let service: MissionControlService; let registry: { listAllSessions: ReturnType; getProviderForSession: ReturnType; }; let prisma: { operatorAuditLog: { create: ReturnType; }; }; const session: AgentSession = { id: "session-1", providerId: "internal", providerType: "internal", status: "active", createdAt: new Date("2026-03-07T14:00:00.000Z"), updatedAt: new Date("2026-03-07T14:01:00.000Z"), }; beforeEach(() => { registry = { listAllSessions: vi.fn().mockResolvedValue([session]), getProviderForSession: vi.fn().mockResolvedValue(null), }; prisma = { operatorAuditLog: { create: vi.fn().mockResolvedValue(undefined), }, }; service = new MissionControlService( registry as unknown as AgentProviderRegistry, prisma as unknown as PrismaService ); }); it("lists sessions from the registry", async () => { await expect(service.listSessions()).resolves.toEqual([session]); expect(registry.listAllSessions).toHaveBeenCalledTimes(1); }); it("returns a session when it is found", async () => { const provider = createProvider("internal"); registry.getProviderForSession.mockResolvedValue({ provider, session }); await expect(service.getSession(session.id)).resolves.toEqual(session); }); it("throws NotFoundException when session lookup fails", async () => { await expect(service.getSession("missing-session")).rejects.toBeInstanceOf(NotFoundException); }); it("gets messages from the resolved provider", async () => { const provider = createProvider("openclaw"); const messages: AgentMessage[] = [ { id: "message-1", sessionId: session.id, role: "assistant", content: "hello", timestamp: new Date("2026-03-07T14:01:00.000Z"), }, ]; provider.getMessages.mockResolvedValue(messages); registry.getProviderForSession.mockResolvedValue({ provider, session }); await expect(service.getMessages(session.id, 25, "10")).resolves.toEqual(messages); expect(provider.getMessages).toHaveBeenCalledWith(session.id, 25, "10"); }); it("injects a message and writes an audit log", async () => { const provider = createProvider("internal"); const injectResult: InjectResult = { accepted: true, messageId: "msg-1" }; provider.injectMessage.mockResolvedValue(injectResult); registry.getProviderForSession.mockResolvedValue({ provider, session }); await expect(service.injectMessage(session.id, "ship it", "operator-1")).resolves.toEqual( injectResult ); expect(provider.injectMessage).toHaveBeenCalledWith(session.id, "ship it"); expect(prisma.operatorAuditLog.create).toHaveBeenCalledWith({ data: { sessionId: session.id, userId: "operator-1", provider: "internal", action: "inject", content: "ship it", metadata: { payload: { message: "ship it" }, }, }, }); }); it("pauses and resumes using default operator id", async () => { const provider = createProvider("openclaw"); registry.getProviderForSession.mockResolvedValue({ provider, session }); await service.pauseSession(session.id); await service.resumeSession(session.id); expect(provider.pauseSession).toHaveBeenCalledWith(session.id); expect(provider.resumeSession).toHaveBeenCalledWith(session.id); expect(prisma.operatorAuditLog.create).toHaveBeenNthCalledWith(1, { data: { sessionId: session.id, userId: "mission-control", provider: "openclaw", action: "pause", metadata: { payload: {}, }, }, }); expect(prisma.operatorAuditLog.create).toHaveBeenNthCalledWith(2, { data: { sessionId: session.id, userId: "mission-control", provider: "openclaw", action: "resume", metadata: { payload: {}, }, }, }); }); it("kills with provided force value and writes audit log", async () => { const provider = createProvider("openclaw"); registry.getProviderForSession.mockResolvedValue({ provider, session }); await service.killSession(session.id, false, "operator-2"); expect(provider.killSession).toHaveBeenCalledWith(session.id, false); expect(prisma.operatorAuditLog.create).toHaveBeenCalledWith({ data: { sessionId: session.id, userId: "operator-2", provider: "openclaw", action: "kill", metadata: { payload: { force: false }, }, }, }); }); it("resolves provider message stream", async () => { const provider = createProvider("internal"); const messageStream = (async function* (): AsyncIterable { yield { id: "message-1", sessionId: session.id, role: "assistant", content: "stream", timestamp: new Date("2026-03-07T14:03:00.000Z"), }; })(); provider.streamMessages.mockReturnValue(messageStream); registry.getProviderForSession.mockResolvedValue({ provider, session }); await expect(service.streamMessages(session.id)).resolves.toBe(messageStream); expect(provider.streamMessages).toHaveBeenCalledWith(session.id); }); it("does not write audit log when session cannot be resolved", async () => { await expect(service.pauseSession("missing-session")).rejects.toBeInstanceOf(NotFoundException); expect(prisma.operatorAuditLog.create).not.toHaveBeenCalled(); }); });