| import { PluginsClient } from "@openhands/typescript-client/clients"; |
| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
| import { |
| __resetActiveStoreForTests, |
| setActiveSelection, |
| setRegisteredBackends, |
| } from "#/api/backend-registry/active-store"; |
| import type { Backend } from "#/api/backend-registry/types"; |
| import PluginsManagementService from "#/api/plugins-management-service"; |
|
|
| vi.mock("@openhands/typescript-client/clients", () => ({ |
| PluginsClient: vi.fn(), |
| })); |
|
|
| const listInstalledPlugins = vi.fn(); |
| const installPlugin = vi.fn(); |
| const setPluginEnabled = vi.fn(); |
| const uninstallPlugin = vi.fn(); |
| const refreshPlugin = vi.fn(); |
| const close = vi.fn(); |
|
|
| function useBackend(kind: "local" | "cloud"): void { |
| const backend: Backend = { |
| id: kind, |
| name: kind, |
| host: "http://127.0.0.1:8001", |
| apiKey: "session-key", |
| kind, |
| }; |
| setRegisteredBackends([backend]); |
| setActiveSelection({ backendId: kind, orgId: null }); |
| } |
|
|
| beforeEach(() => { |
| vi.clearAllMocks(); |
| __resetActiveStoreForTests(); |
| vi.mocked(PluginsClient).mockImplementation(function MockPluginsClient() { |
| return { |
| listInstalledPlugins, |
| installPlugin, |
| setPluginEnabled, |
| uninstallPlugin, |
| refreshPlugin, |
| close, |
| } as unknown as PluginsClient; |
| } as unknown as typeof PluginsClient); |
| }); |
|
|
| afterEach(() => { |
| __resetActiveStoreForTests(); |
| }); |
|
|
| const installedPlugin = { |
| name: "demo-plugin", |
| version: "1.0.0", |
| description: "A demo plugin", |
| enabled: true, |
| source: "github:OpenHands/extensions", |
| resolved_ref: null, |
| repo_path: "plugins/demo-plugin", |
| installed_at: "2026-06-01T00:00:00Z", |
| install_path: "/home/.openhands/plugins/installed/demo-plugin", |
| }; |
|
|
| describe("PluginsManagementService", () => { |
| it("lists installed plugins from the local agent-server", async () => { |
| useBackend("local"); |
| listInstalledPlugins.mockResolvedValue({ plugins: [installedPlugin] }); |
|
|
| const result = await PluginsManagementService.listInstalledPlugins(); |
|
|
| expect(result).toEqual([installedPlugin]); |
| expect(listInstalledPlugins).toHaveBeenCalledTimes(1); |
| }); |
|
|
| it("returns an empty installed list on a cloud backend without calling the client", async () => { |
| useBackend("cloud"); |
|
|
| const result = await PluginsManagementService.listInstalledPlugins(); |
|
|
| expect(result).toEqual([]); |
| expect(PluginsClient).not.toHaveBeenCalled(); |
| }); |
|
|
| it("returns an empty installed list when the local request fails", async () => { |
| useBackend("local"); |
| listInstalledPlugins.mockRejectedValue(new Error("unreachable")); |
|
|
| const result = await PluginsManagementService.listInstalledPlugins(); |
|
|
| expect(result).toEqual([]); |
| }); |
|
|
| it("forwards source, ref, and repo_path when installing a plugin", async () => { |
| useBackend("local"); |
| installPlugin.mockResolvedValue(installedPlugin); |
|
|
| await PluginsManagementService.installPlugin({ |
| source: "github:OpenHands/extensions", |
| ref: "main", |
| repo_path: "plugins/demo-plugin", |
| }); |
|
|
| expect(installPlugin).toHaveBeenCalledWith({ |
| source: "github:OpenHands/extensions", |
| ref: "main", |
| repo_path: "plugins/demo-plugin", |
| }); |
| }); |
|
|
| it("forwards the name and enabled flag when toggling a plugin", async () => { |
| useBackend("local"); |
| setPluginEnabled.mockResolvedValue({ name: "demo-plugin", enabled: false }); |
|
|
| await PluginsManagementService.setPluginEnabled("demo-plugin", false); |
|
|
| expect(setPluginEnabled).toHaveBeenCalledWith("demo-plugin", false); |
| }); |
|
|
| it("forwards the plugin name when uninstalling", async () => { |
| useBackend("local"); |
| uninstallPlugin.mockResolvedValue({ message: "ok" }); |
|
|
| await PluginsManagementService.uninstallPlugin("demo-plugin"); |
|
|
| expect(uninstallPlugin).toHaveBeenCalledWith("demo-plugin"); |
| }); |
|
|
| it("forwards the plugin name when refreshing", async () => { |
| useBackend("local"); |
| refreshPlugin.mockResolvedValue({ message: "ok", plugin: installedPlugin }); |
|
|
| await PluginsManagementService.refreshPlugin("demo-plugin"); |
|
|
| expect(refreshPlugin).toHaveBeenCalledWith("demo-plugin"); |
| }); |
| }); |
|
|