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>
214 lines
7.0 KiB
TypeScript
214 lines
7.0 KiB
TypeScript
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<typeof vi.fn>;
|
|
getSession: ReturnType<typeof vi.fn>;
|
|
getMessages: ReturnType<typeof vi.fn>;
|
|
injectMessage: ReturnType<typeof vi.fn>;
|
|
pauseSession: ReturnType<typeof vi.fn>;
|
|
resumeSession: ReturnType<typeof vi.fn>;
|
|
killSession: ReturnType<typeof vi.fn>;
|
|
streamMessages: ReturnType<typeof vi.fn>;
|
|
};
|
|
|
|
const emptyMessageStream = async function* (): AsyncIterable<AgentMessage> {
|
|
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<typeof vi.fn>;
|
|
getProviderForSession: ReturnType<typeof vi.fn>;
|
|
};
|
|
let prisma: {
|
|
operatorAuditLog: {
|
|
create: ReturnType<typeof vi.fn>;
|
|
};
|
|
};
|
|
|
|
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<AgentMessage> {
|
|
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();
|
|
});
|
|
});
|