import type { HttpService } from "@nestjs/axios"; import type { AgentMessage, AgentSession, AgentSessionList, IAgentProvider, InjectResult, } from "@mosaic/shared"; import type { AgentProviderConfig } from "@prisma/client"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { InternalAgentProvider } from "../../src/api/agents/internal-agent.provider"; import { AgentProviderRegistry } from "../../src/api/agents/agent-provider.registry"; import { MissionControlController } from "../../src/api/mission-control/mission-control.controller"; import { MissionControlService } from "../../src/api/mission-control/mission-control.service"; import { OpenClawProviderFactory } from "../../src/api/providers/openclaw/openclaw.provider-factory"; import { OpenClawSseBridge } from "../../src/api/providers/openclaw/openclaw-sse.bridge"; import { ProvidersModule } from "../../src/api/providers/providers.module"; import type { PrismaService } from "../../src/prisma/prisma.service"; import type { EncryptionService } from "../../src/security/encryption.service"; type MockProvider = IAgentProvider & { listSessions: ReturnType; getSession: ReturnType; injectMessage: ReturnType; pauseSession: ReturnType; killSession: ReturnType; }; type MockPrisma = { agentProviderConfig: { create: ReturnType; findMany: ReturnType; }; operatorAuditLog: { create: ReturnType; }; }; const emptyMessageStream = async function* (): AsyncIterable { return; }; describe("MS23-P3-004 API integration", () => { let controller: MissionControlController; let providersModule: ProvidersModule; let registry: AgentProviderRegistry; let prisma: MockPrisma; let httpService: { axiosRef: { get: ReturnType; post: ReturnType; }; }; const gatewayUrl = "https://openclaw-gateway.example.com"; const internalSession: AgentSession = { id: "session-internal-1", providerId: "internal", providerType: "internal", status: "active", createdAt: new Date("2026-03-07T16:00:00.000Z"), updatedAt: new Date("2026-03-07T16:02:00.000Z"), }; const openClawGatewaySession = { id: "session-openclaw-1", status: "running", createdAt: "2026-03-07T16:01:00.000Z", updatedAt: "2026-03-07T16:03:00.000Z", }; const createInternalProvider = (session: AgentSession): MockProvider => ({ providerId: "internal", providerType: "internal", displayName: "Internal", listSessions: vi.fn().mockResolvedValue({ sessions: [session], total: 1 } as AgentSessionList), getSession: vi.fn().mockImplementation(async (sessionId: string) => { return sessionId === session.id ? session : 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), }); beforeEach(() => { const providerConfigs: AgentProviderConfig[] = []; prisma = { agentProviderConfig: { create: vi.fn().mockImplementation(async (args: { data: Record }) => { const now = new Date("2026-03-07T15:00:00.000Z"); const record: AgentProviderConfig = { id: `cfg-${String(providerConfigs.length + 1)}`, workspaceId: String(args.data.workspaceId), name: String(args.data.name), provider: String(args.data.provider), gatewayUrl: String(args.data.gatewayUrl), credentials: (args.data.credentials ?? {}) as AgentProviderConfig["credentials"], isActive: args.data.isActive !== false, createdAt: now, updatedAt: now, }; providerConfigs.push(record); return record; }), findMany: vi .fn() .mockImplementation( async (args: { where?: { provider?: string; isActive?: boolean } }) => { const where = args.where ?? {}; return providerConfigs.filter((config) => { if (where.provider !== undefined && config.provider !== where.provider) { return false; } if (where.isActive !== undefined && config.isActive !== where.isActive) { return false; } return true; }); } ), }, operatorAuditLog: { create: vi.fn().mockResolvedValue(undefined), }, }; httpService = { axiosRef: { get: vi.fn().mockImplementation(async (url: string) => { if (url === `${gatewayUrl}/api/sessions`) { return { data: { sessions: [openClawGatewaySession], total: 1, }, }; } if (url === `${gatewayUrl}/api/sessions/${openClawGatewaySession.id}`) { return { data: { session: openClawGatewaySession, }, }; } throw new Error(`Unexpected GET ${url}`); }), post: vi.fn().mockImplementation(async (url: string) => { if (url.endsWith("/inject")) { return { data: { accepted: true, messageId: "msg-inject-1" } }; } if (url.endsWith("/pause") || url.endsWith("/kill")) { return { data: {} }; } throw new Error(`Unexpected POST ${url}`); }), }, }; const internalProvider = createInternalProvider(internalSession); registry = new AgentProviderRegistry(internalProvider as unknown as InternalAgentProvider); registry.onModuleInit(); const encryptionService = { decryptIfNeeded: vi.fn().mockReturnValue("plain-openclaw-token"), }; const sseBridge = new OpenClawSseBridge(httpService as unknown as HttpService); const openClawProviderFactory = new OpenClawProviderFactory( encryptionService as unknown as EncryptionService, httpService as unknown as HttpService, sseBridge ); providersModule = new ProvidersModule( prisma as unknown as PrismaService, registry, openClawProviderFactory ); const missionControlService = new MissionControlService( registry, prisma as unknown as PrismaService ); controller = new MissionControlController(missionControlService); }); it("Phase 3 gate: OpenClaw provider config registered in DB → provider loaded on boot → sessions returned from /api/mission-control/sessions → inject/pause/kill proxied to gateway", async () => { await prisma.agentProviderConfig.create({ data: { workspaceId: "workspace-ms23", name: "openclaw-home", provider: "openclaw", gatewayUrl, credentials: { apiToken: "enc:test-openclaw-token", }, isActive: true, }, }); await providersModule.onModuleInit(); // Equivalent to GET /api/mission-control/sessions const sessionsResponse = await controller.listSessions(); expect(sessionsResponse.sessions.map((session) => session.id)).toEqual([ "session-openclaw-1", "session-internal-1", ]); expect(sessionsResponse.sessions).toEqual( expect.arrayContaining([ expect.objectContaining({ id: "session-internal-1", providerId: "internal", }), expect.objectContaining({ id: "session-openclaw-1", providerId: "openclaw-home", providerType: "openclaw", }), ]) ); const operatorRequest = { user: { id: "operator-ms23", }, }; await expect( controller.injectMessage( "session-openclaw-1", { message: "Ship it", }, operatorRequest ) ).resolves.toEqual({ accepted: true, messageId: "msg-inject-1" }); await expect(controller.pauseSession("session-openclaw-1", operatorRequest)).resolves.toEqual({ message: "Session session-openclaw-1 paused", }); await expect( controller.killSession( "session-openclaw-1", { force: false, }, operatorRequest ) ).resolves.toEqual({ message: "Session session-openclaw-1 killed" }); expect(httpService.axiosRef.post).toHaveBeenNthCalledWith( 1, `${gatewayUrl}/api/sessions/session-openclaw-1/inject`, { content: "Ship it" }, { headers: { Authorization: "Bearer plain-openclaw-token", }, } ); expect(httpService.axiosRef.post).toHaveBeenNthCalledWith( 2, `${gatewayUrl}/api/sessions/session-openclaw-1/pause`, {}, { headers: { Authorization: "Bearer plain-openclaw-token", }, } ); expect(httpService.axiosRef.post).toHaveBeenNthCalledWith( 3, `${gatewayUrl}/api/sessions/session-openclaw-1/kill`, { force: false }, { headers: { Authorization: "Bearer plain-openclaw-token", }, } ); expect(prisma.operatorAuditLog.create).toHaveBeenNthCalledWith(1, { data: { sessionId: "session-openclaw-1", userId: "operator-ms23", provider: "openclaw-home", action: "inject", content: "Ship it", metadata: { payload: { message: "Ship it", }, }, }, }); }); });