diff options
| author | Adam Malczewski <[email protected]> | 2026-09-03 18:05:29 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-09-03 18:05:29 +0900 |
| commit | 3a4922a05930b0dc22740bf5621910803e8f275e (patch) | |
| tree | d5f831c3d3838cfff346e378f5370a9c58bc2ed3 /src/features/plugins/ui/WorkspacePluginModal.test.ts | |
| parent | 33c7245f18346553f660434bc3ac589cff3e80c2 (diff) | |
| download | dispatch-web-3a4922a05930b0dc22740bf5621910803e8f275e.tar.gz dispatch-web-3a4922a05930b0dc22740bf5621910803e8f275e.zip | |
feat(plugin-system): plan 07 frontend plugin manager ui
Diffstat (limited to 'src/features/plugins/ui/WorkspacePluginModal.test.ts')
| -rw-r--r-- | src/features/plugins/ui/WorkspacePluginModal.test.ts | 124 |
1 files changed, 124 insertions, 0 deletions
diff --git a/src/features/plugins/ui/WorkspacePluginModal.test.ts b/src/features/plugins/ui/WorkspacePluginModal.test.ts new file mode 100644 index 0000000..32e88d7 --- /dev/null +++ b/src/features/plugins/ui/WorkspacePluginModal.test.ts @@ -0,0 +1,124 @@ +import type { WorkspacePluginsResponse } from "@dispatch/transport-contract"; +import { render, screen } from "@testing-library/svelte"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; +import type { PluginsHttp } from "../adapter/http"; +import { createPluginsStore } from "../store.svelte"; +import WorkspacePluginModal from "./WorkspacePluginModal.svelte"; + +const wsResponse: WorkspacePluginsResponse = { + workspaceId: "ws", + plugins: [ + // Core: enabled + locked (the server materializes core to enabled and + // refuses its PUT with a 409 core-locked — plan 06 §6.6). + { pluginId: "kernel", tier: "core", state: "enabled", loaded: true, locked: true }, + // Loaded standard: interactive. + { pluginId: "mcp", tier: "standard", state: "undefined", loaded: true, locked: false }, + // UNLOADED: greyed out + unselectable (§7 strict layering). + { pluginId: "notloaded", tier: "standard", state: "disabled", loaded: false, locked: false }, + ], + enabledProviderCount: 1, +}; + +function fakeHttp(overrides: Partial<PluginsHttp> = {}): PluginsHttp { + return { + list: vi.fn(async () => null), + scan: vi.fn(), + load: vi.fn(), + unload: vi.fn(), + reload: vi.fn(), + workspacePlugins: vi.fn(async () => wsResponse), + setWorkspaceState: vi.fn(async () => ({ + ok: true, + response: { workspaceId: "ws", pluginId: "mcp", state: "enabled" }, + })), + providersStatus: vi.fn(async () => ({ workspaceId: "ws", enabledProviderCount: 1 })), + ...overrides, + } as unknown as PluginsHttp; +} + +async function renderModal(http: PluginsHttp = fakeHttp()) { + const store = createPluginsStore(http); + await store.openWorkspace("ws"); + render(WorkspacePluginModal, { + props: { workspaceId: "ws", workspaceTitle: "My Workspace", plugins: store, onClose: vi.fn() }, + }); + return { store, http }; +} + +describe("WorkspacePluginModal", () => { + it("renders the workspace's plugin rows", async () => { + await renderModal(); + expect(screen.getByText("Plugins — My Workspace")).toBeInTheDocument(); + expect(screen.getByText("kernel")).toBeInTheDocument(); + expect(screen.getByText("mcp")).toBeInTheDocument(); + }); + + it("checkboxes reflect the workspace state (enabled=checked, undefined=unchecked)", async () => { + await renderModal(); + expect(screen.getByRole("checkbox", { name: /kernel/i })).toBeChecked(); + expect(screen.getByRole("checkbox", { name: /Enable mcp/i })).not.toBeChecked(); + }); + + it("unloaded plugins are greyed out AND unselectable (§7)", async () => { + await renderModal(); + const checkbox = screen.getByRole("checkbox", { name: /notloaded/i }); + expect(checkbox).toBeDisabled(); + // The row is visually greyed (the opacity marker class). + expect(checkbox.closest("tr")?.className).toContain("opacity-50"); + // The tooltip points at the global manager. + expect(checkbox.getAttribute("title")).toMatch(/plugin manager/i); + }); + + it("core rows render locked (checked but unselectable — the server refuses the PUT)", async () => { + await renderModal(); + const checkbox = screen.getByRole("checkbox", { name: /kernel/i }); + expect(checkbox).toBeDisabled(); + expect(checkbox.getAttribute("title")).toMatch(/Core plugins are enabled by default/i); + }); + + it("checking a selectable row writes enabled via the store", async () => { + const user = userEvent.setup(); + const { http } = await renderModal(); + await user.click(screen.getByRole("checkbox", { name: /Enable mcp/i })); + expect(http.setWorkspaceState).toHaveBeenCalledWith("ws", "mcp", "enabled"); + }); + + it("unchecking a checked row writes disabled", async () => { + const user = userEvent.setup(); + const enabled: WorkspacePluginsResponse = { + workspaceId: "ws", + plugins: [ + { pluginId: "mcp", tier: "standard", state: "enabled", loaded: true, locked: false }, + ], + enabledProviderCount: 1, + }; + const fake = fakeHttp({ workspacePlugins: async () => enabled }); + const store = createPluginsStore(fake); + await store.openWorkspace("ws"); + render(WorkspacePluginModal, { + props: { + workspaceId: "ws", + workspaceTitle: "My Workspace", + plugins: store, + onClose: vi.fn(), + }, + }); + await user.click(screen.getByRole("checkbox", { name: /Disable mcp/i })); + expect(fake.setWorkspaceState).toHaveBeenCalledWith("ws", "mcp", "disabled"); + }); + + it("shows the template hint for the default workspace", async () => { + const store = createPluginsStore(fakeHttp()); + await store.openWorkspace("default"); + render(WorkspacePluginModal, { + props: { + workspaceId: "default", + workspaceTitle: "Default", + plugins: store, + onClose: vi.fn(), + }, + }); + expect(screen.getByText(/other workspaces inherit these states/i)).toBeInTheDocument(); + }); +}); |
