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 type { WorkspaceLayoutPort } from "../logic/types"; 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 { 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 }; } /** A layout port (plan 04 §4.4) scripted per test. */ function fakeLayoutPort( providerActive = true, reset: WorkspaceLayoutPort["reset"] = vi.fn(async () => null), refresh: WorkspaceLayoutPort["refresh"] = vi.fn(), ): WorkspaceLayoutPort { return { providerActive, reset, refresh }; } 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("a successful toggle calls the layout port's refresh (the per-workspace activation may change the served tree)", async () => { const user = userEvent.setup(); const refresh = vi.fn(); const store = createPluginsStore(fakeHttp()); await store.openWorkspace("ws"); render(WorkspacePluginModal, { props: { workspaceId: "ws", workspaceTitle: "My Workspace", plugins: store, layout: fakeLayoutPort( true, vi.fn(async () => null), refresh, ), onClose: vi.fn(), }, }); await user.click(screen.getByRole("checkbox", { name: /Enable mcp/i })); expect(refresh).toHaveBeenCalledWith("ws"); }); it("a REFUSED toggle (state write fails) still calls refresh — the probe is the idempotent catch-up", async () => { const user = userEvent.setup(); const refresh = vi.fn(); const fake = fakeHttp({ setWorkspaceState: async () => ({ ok: false, error: "Plugin is not loaded — load it globally first", }), }); const store = createPluginsStore(fake); await store.openWorkspace("ws"); render(WorkspacePluginModal, { props: { workspaceId: "ws", workspaceTitle: "My Workspace", plugins: store, layout: fakeLayoutPort( true, vi.fn(async () => null), refresh, ), onClose: vi.fn(), }, }); await user.click(screen.getByRole("checkbox", { name: /Enable mcp/i })); // The attempted state change re-probes either way (a refused write changed // nothing, so the probe re-syncs the same tree — harmless and consistent). expect(refresh).toHaveBeenCalledWith("ws"); }); 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(); }); }); describe("WorkspacePluginModal — the reset-layout affordance (plan 04 §4.4)", () => { async function renderWithLayout(layout?: ReturnType) { const store = createPluginsStore(fakeHttp()); await store.openWorkspace("ws"); render(WorkspacePluginModal, { props: { workspaceId: "ws", workspaceTitle: "My Workspace", plugins: store, ...(layout ? { layout } : {}), onClose: vi.fn(), }, }); return layout; } it("renders an ENABLED reset button when a layout provider is active", async () => { await renderWithLayout(fakeLayoutPort(true)); const button = screen.getByRole("button", { name: /reset layout to default/i }); expect(button).toBeEnabled(); }); it("the button is DISABLED with the 'no layout plugin enabled' reason when no provider is active", async () => { await renderWithLayout(fakeLayoutPort(false)); const button = screen.getByRole("button", { name: /reset layout to default/i }); expect(button).toBeDisabled(); expect(button.getAttribute("title")).toMatch(/no layout plugin enabled/i); expect(screen.getByText(/no layout plugin enabled/i)).toBeInTheDocument(); }); it("no layout port → the layout section doesn't render at all (tests / no wiring)", async () => { await renderWithLayout(undefined); expect(screen.queryByRole("button", { name: /reset layout to default/i })).toBeNull(); }); it("confirming the dialog resets the layout (and the dialog copy names the consequence)", async () => { const user = userEvent.setup(); const reset = vi.fn(async () => null); await renderWithLayout(fakeLayoutPort(true, reset)); await user.click(screen.getByRole("button", { name: /reset layout to default/i })); // The confirm dialog names the consequence exactly (§4.4 copy). expect( screen.getByText( /layout returns to the default; plugin and dropdown customizations are lost/i, ), ).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: /^reset$/i })); expect(reset).toHaveBeenCalledWith("ws"); }); it("cancelling the dialog never resets", async () => { const user = userEvent.setup(); const reset = vi.fn(async () => null); await renderWithLayout(fakeLayoutPort(true, reset)); await user.click(screen.getByRole("button", { name: /reset layout to default/i })); await user.click(screen.getByRole("button", { name: /cancel/i })); expect(reset).not.toHaveBeenCalled(); }); it("a failed reset surfaces the reason as an alert", async () => { const user = userEvent.setup(); const reset = vi.fn(async () => "no layout plugin enabled"); await renderWithLayout(fakeLayoutPort(true, reset)); await user.click(screen.getByRole("button", { name: /reset layout to default/i })); await user.click(screen.getByRole("button", { name: /^reset$/i })); expect(await screen.findByRole("alert")).toHaveTextContent(/resetting the layout failed/i); }); });