import { beforeEach, describe, expect, it, vi } from "vitest"; import { FileClient, PluginsClient, } from "@openhands/typescript-client/clients"; import { setActiveSelection, setRegisteredBackends, } from "./backend-registry/active-store"; import PluginsService from "./plugins-service"; vi.mock("@openhands/typescript-client/clients", () => ({ PluginsClient: vi.fn(), FileClient: vi.fn(), })); const getPluginsMarketplace = vi.fn(); const getPlugins = vi.fn(); const downloadFile = vi.fn(); const close = vi.fn(); function useBackend(kind: "local" | "cloud"): void { setRegisteredBackends([ { id: kind, name: kind, host: "http://127.0.0.1:8001", apiKey: "session-key", kind, }, ]); setActiveSelection({ backendId: kind, orgId: null }); } describe("PluginsService.getPluginsMarketplace", () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(PluginsClient).mockImplementation(function MockPluginsClient() { return { getPluginsMarketplace, close } as unknown as PluginsClient; } as unknown as typeof PluginsClient); }); it("returns the catalog from the local agent-server", async () => { useBackend("local"); const plugin = { name: "city-weather", description: "Weather plugin", source: "github:OpenHands/extensions", ref: null, repo_path: "plugins/city-weather", installed: false, }; getPluginsMarketplace.mockResolvedValue({ plugins: [plugin] }); const result = await PluginsService.getPluginsMarketplace(); expect(result).toEqual([plugin]); expect(getPluginsMarketplace).toHaveBeenCalledTimes(1); }); it("returns an empty catalog on a cloud backend without calling the client", async () => { useBackend("cloud"); const result = await PluginsService.getPluginsMarketplace(); expect(result).toEqual([]); expect(PluginsClient).not.toHaveBeenCalled(); }); it("returns an empty catalog when the local request fails", async () => { useBackend("local"); getPluginsMarketplace.mockRejectedValue(new Error("unreachable")); const result = await PluginsService.getPluginsMarketplace(); expect(result).toEqual([]); }); }); describe("PluginsService.getLocalPlugins", () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(PluginsClient).mockImplementation(function MockPluginsClient() { return { getPlugins, close } as unknown as PluginsClient; } as unknown as typeof PluginsClient); }); it("requests user-level local plugins from the local agent-server", async () => { useBackend("local"); const plugin = { name: "hello-local", version: "1.0.0", description: "A local plugin", }; getPlugins.mockResolvedValue({ plugins: [plugin] }); const result = await PluginsService.getLocalPlugins(); expect(result).toEqual([plugin]); expect(getPlugins).toHaveBeenCalledWith({ load_user: true, load_project: false, }); }); it("returns an empty list on a cloud backend without calling the client", async () => { useBackend("cloud"); const result = await PluginsService.getLocalPlugins(); expect(result).toEqual([]); expect(PluginsClient).not.toHaveBeenCalled(); }); }); describe("PluginsService.getPluginFileContent", () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(FileClient).mockImplementation(function MockFileClient() { return { downloadFile, close } as unknown as FileClient; } as unknown as typeof FileClient); }); it("downloads the file under the plugin directory and decodes it as text", async () => { useBackend("local"); downloadFile.mockResolvedValue(new TextEncoder().encode("# Hello").buffer); const result = await PluginsService.getPluginFileContent( "/plugins/demo", "docs/README.md", ); expect(result).toEqual({ kind: "text", text: "# Hello" }); expect(downloadFile).toHaveBeenCalledWith("/plugins/demo/docs/README.md"); }); it("flags content containing NUL bytes as binary", async () => { useBackend("local"); downloadFile.mockResolvedValue( new Uint8Array([0x89, 0x50, 0x00, 0x47]).buffer, ); const result = await PluginsService.getPluginFileContent( "/plugins/demo", "logo.png", ); expect(result).toEqual({ kind: "binary", text: null }); }); it("rejects on a cloud backend without calling the client", async () => { useBackend("cloud"); await expect( PluginsService.getPluginFileContent("/plugins/demo", "README.md"), ).rejects.toThrow(); expect(FileClient).not.toHaveBeenCalled(); }); });