import type { HttpService } from "@nestjs/axios"; import { ServiceUnavailableException } from "@nestjs/common"; import type { AgentProviderConfig } from "@prisma/client"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { EncryptionService } from "../../../security/encryption.service"; import { OpenClawSseBridge } from "./openclaw-sse.bridge"; import { OpenClawProvider } from "./openclaw.provider"; describe("OpenClawProvider", () => { let provider: OpenClawProvider; let httpService: { axiosRef: { get: ReturnType; post: ReturnType; }; }; let encryptionService: { decryptIfNeeded: ReturnType; }; let sseBridge: { streamSession: ReturnType; }; const config: AgentProviderConfig = { id: "cfg-openclaw-1", workspaceId: "workspace-1", name: "openclaw-home", provider: "openclaw", gatewayUrl: "https://gateway.example.com/", credentials: { apiToken: "enc:token-value", displayName: "Home OpenClaw", }, isActive: true, createdAt: new Date("2026-03-07T15:00:00.000Z"), updatedAt: new Date("2026-03-07T15:00:00.000Z"), }; beforeEach(() => { httpService = { axiosRef: { get: vi.fn(), post: vi.fn(), }, }; encryptionService = { decryptIfNeeded: vi.fn().mockReturnValue("plain-token"), }; sseBridge = { streamSession: vi.fn(), }; provider = new OpenClawProvider( config, encryptionService as unknown as EncryptionService, httpService as unknown as HttpService, sseBridge as unknown as OpenClawSseBridge ); }); it("maps listSessions from OpenClaw API", async () => { httpService.axiosRef.get.mockResolvedValue({ data: { sessions: [ { id: "session-1", status: "running", createdAt: "2026-03-07T15:01:00.000Z", updatedAt: "2026-03-07T15:02:00.000Z", }, ], total: 1, cursor: "next-cursor", }, }); const result = await provider.listSessions("cursor-1", 25); expect(httpService.axiosRef.get).toHaveBeenCalledWith( "https://gateway.example.com/api/sessions", { headers: { Authorization: "Bearer plain-token", }, params: { cursor: "cursor-1", limit: 25, }, } ); expect(result).toEqual({ sessions: [ { id: "session-1", providerId: "openclaw-home", providerType: "openclaw", status: "active", createdAt: new Date("2026-03-07T15:01:00.000Z"), updatedAt: new Date("2026-03-07T15:02:00.000Z"), }, ], total: 1, cursor: "next-cursor", }); expect(encryptionService.decryptIfNeeded).toHaveBeenCalledWith("enc:token-value"); }); it("returns null from getSession when OpenClaw returns 404", async () => { httpService.axiosRef.get.mockRejectedValue({ response: { status: 404, }, }); await expect(provider.getSession("missing-session")).resolves.toBeNull(); }); it("maps getMessages response", async () => { httpService.axiosRef.get.mockResolvedValue({ data: { messages: [ { id: "message-1", sessionId: "session-1", role: "agent", content: "hello", timestamp: "2026-03-07T15:03:00.000Z", metadata: { tokens: 128, }, }, ], }, }); const result = await provider.getMessages("session-1", 20, "before-cursor"); expect(httpService.axiosRef.get).toHaveBeenCalledWith( "https://gateway.example.com/api/messages", { headers: { Authorization: "Bearer plain-token", }, params: { sessionId: "session-1", limit: 20, before: "before-cursor", }, } ); expect(result).toEqual([ { id: "message-1", sessionId: "session-1", role: "assistant", content: "hello", timestamp: new Date("2026-03-07T15:03:00.000Z"), metadata: { tokens: 128, }, }, ]); }); it("maps inject and control endpoints", async () => { httpService.axiosRef.post .mockResolvedValueOnce({ data: { accepted: true, messageId: "message-2", }, }) .mockResolvedValueOnce({ data: {} }) .mockResolvedValueOnce({ data: {} }) .mockResolvedValueOnce({ data: {} }); await expect(provider.injectMessage("session-1", "barge in")).resolves.toEqual({ accepted: true, messageId: "message-2", }); await provider.pauseSession("session-1"); await provider.resumeSession("session-1"); await provider.killSession("session-1", false); expect(httpService.axiosRef.post).toHaveBeenNthCalledWith( 1, "https://gateway.example.com/api/sessions/session-1/inject", { content: "barge in" }, { headers: { Authorization: "Bearer plain-token", }, } ); expect(httpService.axiosRef.post).toHaveBeenNthCalledWith( 2, "https://gateway.example.com/api/sessions/session-1/pause", {}, { headers: { Authorization: "Bearer plain-token", }, } ); expect(httpService.axiosRef.post).toHaveBeenNthCalledWith( 3, "https://gateway.example.com/api/sessions/session-1/resume", {}, { headers: { Authorization: "Bearer plain-token", }, } ); expect(httpService.axiosRef.post).toHaveBeenNthCalledWith( 4, "https://gateway.example.com/api/sessions/session-1/kill", { force: false }, { headers: { Authorization: "Bearer plain-token", }, } ); }); it("delegates streaming to OpenClawSseBridge", async () => { const streamedMessage = { id: "message-stream", sessionId: "session-stream", role: "assistant", content: "stream hello", timestamp: new Date("2026-03-07T16:00:00.000Z"), }; sseBridge.streamSession.mockReturnValue( (async function* () { yield streamedMessage; })() ); const messages: Array = []; for await (const message of provider.streamMessages("session-stream")) { messages.push(message); } expect(sseBridge.streamSession).toHaveBeenCalledWith( "https://gateway.example.com", "session-stream", { Authorization: "Bearer plain-token", } ); expect(messages).toEqual([streamedMessage]); }); it("throws ServiceUnavailableException for request failures", async () => { httpService.axiosRef.get.mockRejectedValue(new Error("gateway unreachable")); await expect(provider.listSessions()).rejects.toBeInstanceOf(ServiceUnavailableException); }); it("returns false from isAvailable when gateway check fails", async () => { httpService.axiosRef.get.mockRejectedValue(new Error("gateway unreachable")); await expect(provider.isAvailable()).resolves.toBe(false); }); });