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>
203 lines
6.7 KiB
TypeScript
203 lines
6.7 KiB
TypeScript
import { Logger } from "@nestjs/common";
|
|
import type {
|
|
AgentMessage,
|
|
AgentSession,
|
|
AgentSessionList,
|
|
IAgentProvider,
|
|
InjectResult,
|
|
} from "@mosaic/shared";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { AgentProviderRegistry } from "./agent-provider.registry";
|
|
import { InternalAgentProvider } from "./internal-agent.provider";
|
|
|
|
type MockProvider = IAgentProvider & {
|
|
listSessions: ReturnType<typeof vi.fn>;
|
|
getSession: ReturnType<typeof vi.fn>;
|
|
};
|
|
|
|
const emptyMessageStream = async function* (): AsyncIterable<AgentMessage> {
|
|
return;
|
|
};
|
|
|
|
const createProvider = (providerId: string, sessions: AgentSession[] = []): MockProvider => {
|
|
return {
|
|
providerId,
|
|
providerType: providerId,
|
|
displayName: providerId,
|
|
listSessions: vi.fn().mockResolvedValue({
|
|
sessions,
|
|
total: sessions.length,
|
|
} as AgentSessionList),
|
|
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("AgentProviderRegistry", () => {
|
|
let registry: AgentProviderRegistry;
|
|
let internalProvider: MockProvider;
|
|
|
|
beforeEach(() => {
|
|
internalProvider = createProvider("internal");
|
|
registry = new AgentProviderRegistry(internalProvider as unknown as InternalAgentProvider);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("registers InternalAgentProvider on module init", () => {
|
|
registry.onModuleInit();
|
|
|
|
expect(registry.getProvider("internal")).toBe(internalProvider);
|
|
});
|
|
|
|
it("registers providers and returns null for unknown provider ids", () => {
|
|
const externalProvider = createProvider("openclaw");
|
|
|
|
registry.registerProvider(externalProvider);
|
|
|
|
expect(registry.getProvider("openclaw")).toBe(externalProvider);
|
|
expect(registry.getProvider("missing")).toBeNull();
|
|
});
|
|
|
|
it("aggregates and sorts sessions from all providers", async () => {
|
|
const internalSessions: AgentSession[] = [
|
|
{
|
|
id: "session-older",
|
|
providerId: "internal",
|
|
providerType: "internal",
|
|
status: "active",
|
|
createdAt: new Date("2026-03-07T10:00:00.000Z"),
|
|
updatedAt: new Date("2026-03-07T10:10:00.000Z"),
|
|
},
|
|
];
|
|
|
|
const externalSessions: AgentSession[] = [
|
|
{
|
|
id: "session-newer",
|
|
providerId: "openclaw",
|
|
providerType: "external",
|
|
status: "paused",
|
|
createdAt: new Date("2026-03-07T09:00:00.000Z"),
|
|
updatedAt: new Date("2026-03-07T10:20:00.000Z"),
|
|
},
|
|
];
|
|
|
|
internalProvider.listSessions.mockResolvedValue({
|
|
sessions: internalSessions,
|
|
total: internalSessions.length,
|
|
} as AgentSessionList);
|
|
|
|
const externalProvider = createProvider("openclaw", externalSessions);
|
|
registry.onModuleInit();
|
|
registry.registerProvider(externalProvider);
|
|
|
|
const result = await registry.listAllSessions();
|
|
|
|
expect(result.map((session) => session.id)).toEqual(["session-newer", "session-older"]);
|
|
expect(internalProvider.listSessions).toHaveBeenCalledTimes(1);
|
|
expect(externalProvider.listSessions).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("skips provider failures and logs warning", async () => {
|
|
const warnSpy = vi.spyOn(Logger.prototype, "warn").mockImplementation(() => undefined);
|
|
|
|
const healthyProvider = createProvider("healthy", [
|
|
{
|
|
id: "session-1",
|
|
providerId: "healthy",
|
|
providerType: "external",
|
|
status: "active",
|
|
createdAt: new Date("2026-03-07T11:00:00.000Z"),
|
|
updatedAt: new Date("2026-03-07T11:00:00.000Z"),
|
|
},
|
|
]);
|
|
|
|
const failingProvider = createProvider("failing");
|
|
failingProvider.listSessions.mockRejectedValue(new Error("provider offline"));
|
|
|
|
registry.onModuleInit();
|
|
registry.registerProvider(healthyProvider);
|
|
registry.registerProvider(failingProvider);
|
|
|
|
const result = await registry.listAllSessions();
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]?.id).toBe("session-1");
|
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining("Failed to list sessions for provider failing")
|
|
);
|
|
});
|
|
|
|
it("finds a provider for an existing session", async () => {
|
|
const targetSession: AgentSession = {
|
|
id: "session-found",
|
|
providerId: "openclaw",
|
|
providerType: "external",
|
|
status: "active",
|
|
createdAt: new Date("2026-03-07T12:00:00.000Z"),
|
|
updatedAt: new Date("2026-03-07T12:10:00.000Z"),
|
|
};
|
|
|
|
const openclawProvider = createProvider("openclaw");
|
|
openclawProvider.getSession.mockResolvedValue(targetSession);
|
|
|
|
registry.onModuleInit();
|
|
registry.registerProvider(openclawProvider);
|
|
|
|
const result = await registry.getProviderForSession(targetSession.id);
|
|
|
|
expect(result).toEqual({
|
|
provider: openclawProvider,
|
|
session: targetSession,
|
|
});
|
|
expect(internalProvider.getSession).toHaveBeenCalledWith(targetSession.id);
|
|
expect(openclawProvider.getSession).toHaveBeenCalledWith(targetSession.id);
|
|
});
|
|
|
|
it("returns null when no provider has the requested session", async () => {
|
|
const openclawProvider = createProvider("openclaw");
|
|
|
|
registry.onModuleInit();
|
|
registry.registerProvider(openclawProvider);
|
|
|
|
await expect(registry.getProviderForSession("missing-session")).resolves.toBeNull();
|
|
});
|
|
|
|
it("continues searching providers when getSession throws", async () => {
|
|
const warnSpy = vi.spyOn(Logger.prototype, "warn").mockImplementation(() => undefined);
|
|
const failingProvider = createProvider("failing");
|
|
failingProvider.getSession.mockRejectedValue(new Error("provider timeout"));
|
|
|
|
const healthySession: AgentSession = {
|
|
id: "session-healthy",
|
|
providerId: "healthy",
|
|
providerType: "external",
|
|
status: "active",
|
|
createdAt: new Date("2026-03-07T12:15:00.000Z"),
|
|
updatedAt: new Date("2026-03-07T12:16:00.000Z"),
|
|
};
|
|
|
|
const healthyProvider = createProvider("healthy");
|
|
healthyProvider.getSession.mockResolvedValue(healthySession);
|
|
|
|
registry.onModuleInit();
|
|
registry.registerProvider(failingProvider);
|
|
registry.registerProvider(healthyProvider);
|
|
|
|
const result = await registry.getProviderForSession(healthySession.id);
|
|
|
|
expect(result).toEqual({ provider: healthyProvider, session: healthySession });
|
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining("Failed to get session session-healthy for provider failing")
|
|
);
|
|
});
|
|
});
|