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" }),
);
});
});
});