summaryrefslogtreecommitdiffhomepage
path: root/src/features/plugins/ui/WorkspacePluginModal.test.ts
blob: 32e88d7b0075ec7b6cd22bb5a487b68042099ca3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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();
  });
});