import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { DIProvider } from "@/app/di"; import { createMockGateways, MockPluginGateway, } from "@/adapters/mock"; import type { ProjectPluginManifest } from "@/domain"; import { SkillsPanel } from "@/features/skills"; import { McpPanel } from "@/features/permissions"; import { ProjectPluginsPanel } from "./ProjectPluginsPanel"; function manifest(): ProjectPluginManifest { return { ideaPluginManifestVersion: 1, id: "dev.acme.unity", displayName: "Unity Plugin", publisher: "Acme", version: "1.2.3", main: "dist/index.js", trustLevel: "full", capabilities: ["ui", "mcp"], activationScope: "app", contributes: { skills: [{ id: "unity.debug", name: "unity-debug", description: "Diagnostiquer les builds Unity.", kind: "workflow", path: "skills/unity-debug/SKILL.md", }], menus: [], menuItems: [], layouts: [], mcpServers: [ { id: "unity-tools", displayName: "Unity Tools", transport: "stdio", tools: [{ name: "unity_build", description: "Build the Unity project." }], }, { id: "legacy-tools", displayName: "Legacy Tools", transport: "stdio" }, ], }, }; } function gatewaysWithPlugin() { const plugin = new MockPluginGateway(); plugin._seedProjectPluginManifest(manifest()); return { ...createMockGateways(), plugin }; } describe("project plugin settings #282", () => { it("lists globally installed plugins, details their capabilities and toggles project activation", async () => { const gateways = gatewaysWithPlugin(); render( , ); expect(await screen.findByText("Unity Plugin")).toBeTruthy(); expect(screen.getByText("Installé globalement")).toBeTruthy(); fireEvent.click(screen.getByText("Voir les capacités")); expect(screen.getByText("unity-debug")).toBeTruthy(); expect(screen.getByText("Unity Tools")).toBeTruthy(); const toggle = screen.getByRole("checkbox", { name: "Activé pour ce projet" }); fireEvent.click(toggle); await waitFor(() => expect((toggle as HTMLInputElement).checked).toBe(true)); expect((await gateways.plugin.listProjectSettings("project-1")).settings.plugins).toEqual([ { pluginId: "dev.acme.unity", enabled: true }, ]); }); it("offers an obvious entry point to assign plugin capabilities to agents", async () => { const gateways = gatewaysWithPlugin(); const onAssignAgents = vi.fn(); render( , ); fireEvent.click(await screen.findByRole("button", { name: "Gérer les skills" })); expect(onAssignAgents).toHaveBeenCalledOnce(); }); it("assigns plugin skills from the unified Skills panel and keeps tombstones removable", async () => { const gateways = gatewaysWithPlugin(); const agent = await gateways.agent.createAgent("project-1", { name: "Dev", profileId: "profile-1", }); gateways.plugin._seedProjectPluginSettings("project-1", { version: 1, plugins: [{ pluginId: "dev.acme.unity", enabled: true }], agents: [{ agentId: agent.id, skills: [{ pluginId: "missing.plugin", skillId: "removed-skill" }], tools: [], }], }); render( , ); const skill = await screen.findByRole("checkbox", { name: /unity-debug/ }); fireEvent.click(skill); await waitFor(() => expect((skill as HTMLInputElement).checked).toBe(true)); expect(screen.getByText(/removed-skill/).closest("div")?.textContent).toContain( "Indisponible", ); const settings = (await gateways.plugin.listProjectSettings("project-1")).settings; const assigned = settings.agents.find((entry) => entry.agentId === agent.id); expect(assigned?.skills).toContainEqual({ pluginId: "dev.acme.unity", skillId: "unity.debug" }); }); it("lists catalogued plugin tools in MCP and removes free-form assignment", async () => { const gateways = gatewaysWithPlugin(); const agent = await gateways.agent.createAgent("project-1", { name: "Main", profileId: "profile-1", }); gateways.plugin._seedProjectPluginSettings("project-1", { version: 1, plugins: [{ pluginId: "dev.acme.unity", enabled: true }], agents: [{ agentId: agent.id, skills: [], tools: [] }], }); render( , ); fireEvent.click(await screen.findByRole("button", { name: /Main/ })); const staticTool = await screen.findByRole("checkbox", { name: /unity_build/ }); fireEvent.click(staticTool); await waitFor(() => expect((staticTool as HTMLInputElement).checked).toBe(true)); expect(screen.queryByPlaceholderText("Nom exact du tool")).toBeNull(); const settings = (await gateways.plugin.listProjectSettings("project-1")).settings; expect(settings.agents.find((entry) => entry.agentId === agent.id)?.tools).toContainEqual({ pluginId: "dev.acme.unity", serverId: "unity-tools", toolName: "unity_build", }); }); it("keeps disabled and missing plugin assignments removable", async () => { const gateways = gatewaysWithPlugin(); const agent = await gateways.agent.createAgent("project-1", { name: "QA", profileId: "profile-1", }); gateways.plugin._seedProjectPluginSettings("project-1", { version: 1, plugins: [{ pluginId: "dev.acme.unity", enabled: false }], agents: [{ agentId: agent.id, skills: [{ pluginId: "missing.plugin", skillId: "gone-skill" }], tools: [ { pluginId: "dev.acme.unity", serverId: "unity-tools", toolName: "unity_build" }, { pluginId: "dev.acme.unity", serverId: "legacy-tools", toolName: "legacy_ping" }, { pluginId: "missing.plugin", serverId: "gone-server", toolName: "gone_tool" }, ], }], }); render( , ); fireEvent.click(await screen.findByRole("button", { name: /QA/ })); const disabledAssigned = await screen.findByRole("checkbox", { name: /unity_build/ }); expect((disabledAssigned as HTMLInputElement).checked).toBe(true); expect((disabledAssigned as HTMLInputElement).disabled).toBe(false); fireEvent.click(disabledAssigned); await waitFor(() => expect((disabledAssigned as HTMLInputElement).checked).toBe(false)); const runtimeOnly = screen.getByText(/legacy_ping/).closest("div")!; expect(runtimeOnly.textContent).toContain("Indisponible"); expect(screen.queryByRole("checkbox", { name: /legacy_ping/ })).toBeNull(); fireEvent.click(runtimeOnly.querySelector("button")!); await waitFor(async () => { const settings = (await gateways.plugin.listProjectSettings("project-1")).settings; expect(settings.agents[0]?.tools).not.toContainEqual( expect.objectContaining({ toolName: "legacy_ping" }), ); }); const stale = screen.getByText(/gone_tool/).closest("div")!; fireEvent.click(stale.querySelector("button")!); await waitFor(async () => { const settings = (await gateways.plugin.listProjectSettings("project-1")).settings; expect(settings.agents[0]?.tools).not.toContainEqual( expect.objectContaining({ toolName: "gone_tool" }), ); }); }); });