feat(agents,plugins): surface unifiée Capacités de l'agent — skills et tools MCP natifs + plugins #296 (domain: PluginMcpToolMetadata dans PluginMcpServerContribution, validation hôte alignée SDK — name requis non vide, description optionnelle non vide, doublons rejetés; application: McpToolPermissionCatalogue.vue structurée additive tools {name, description, access read|writeAction} enrichie des descriptions canoniques infrastructure orchestrator mcp tools, use case SetAgentMcpToolAllowed — toggle atomique idempotente d'un tool natif dans l'allowlist explicite d'un agent, refus unknown tool sans mutation, overrides vides retirés; backend DTO: catalogue.tools additive + SetAgentMcpToolAllowedRequestDto, wire camelCase; app-tauri + web-server: commande set_agent_mcp_tool_allowed; frontend: AgentPluginCapabilities devient la surface unifiée — skills natifs assign/unassign, tools natifs via setAgentMcpToolAllowed (catalogue structuré avec fallback legacy), skills/tools plugins avec metadata statiques mcpServers[].tools et saisie manuelle fallback pour serveurs sans catalogue, tombstones indisponibles, bannière redémarrage requis si agent actif; AgentsPanel retire l'ancienne section Skills séparée au profit de la surface unifiée; ProjectPluginsPanel recentré enable/disable avec renvoi Capacités des agents; adapters Tauri/HTTP/mock + permission.test.ts; QA verte reconfirmée par Git: application mcp_tool_permissions 5/5 + plugin::tests 37/37, backend ciblés 3/3 + checks domain/application/backend, app-tauri dto 33/33 + dto_agents 30/30 + dto_plugins 2/2, check web-server, npm test 35/35 + 65/65, npm run build; suites complètes rouges classées non bloquantes par QA: web-server parallèle too many open files + 7 PTY série passant individuellement, frontend 32 échecs sur 6 fichiers web non modifiés reproduits isolément)
This commit is contained in:
@ -2,7 +2,11 @@ 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 {
|
||||
createMockGateways,
|
||||
MockPermissionGateway,
|
||||
MockPluginGateway,
|
||||
} from "@/adapters/mock";
|
||||
import type { ProjectPluginManifest } from "@/domain";
|
||||
import { AgentPluginCapabilities } from "./AgentPluginCapabilities";
|
||||
import { ProjectPluginsPanel } from "./ProjectPluginsPanel";
|
||||
@ -29,7 +33,15 @@ function manifest(): ProjectPluginManifest {
|
||||
menus: [],
|
||||
menuItems: [],
|
||||
layouts: [],
|
||||
mcpServers: [{ id: "unity-tools", displayName: "Unity Tools", transport: "stdio" }],
|
||||
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" },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -75,48 +87,152 @@ describe("project plugin settings #282", () => {
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Attribuer aux agents" }));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Capacités des agents" }));
|
||||
expect(onAssignAgents).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("assigns plugin skills and MCP tools to an agent and keeps stale references 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-1",
|
||||
agentId: agent.id,
|
||||
skills: [{ pluginId: "missing.plugin", skillId: "removed-skill" }],
|
||||
tools: [],
|
||||
}],
|
||||
});
|
||||
render(
|
||||
<DIProvider gateways={gateways}>
|
||||
<AgentPluginCapabilities projectId="project-1" agentId="agent-1" agentName="Dev" />
|
||||
<AgentPluginCapabilities projectId="project-1" agent={agent} active />
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText("Capabilities de plugins — Dev")).toBeTruthy();
|
||||
expect(await screen.findByText("Capacités de l'agent — Dev")).toBeTruthy();
|
||||
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/).textContent).toContain("Obsolète ou manquant");
|
||||
expect(screen.getByText(/removed-skill/).closest("div")?.textContent).toContain(
|
||||
"Indisponible",
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByLabelText("serveur MCP de plugin"));
|
||||
fireEvent.click(screen.getByRole("option", { name: "Unity Plugin — Unity Tools" }));
|
||||
fireEvent.change(screen.getByLabelText("nom du tool MCP"), {
|
||||
target: { value: "unity_build" },
|
||||
const staticTool = screen.getByRole("checkbox", { name: /unity_build/ });
|
||||
fireEvent.click(staticTool);
|
||||
await waitFor(() => expect((staticTool as HTMLInputElement).checked).toBe(true));
|
||||
|
||||
fireEvent.click(screen.getByText("Ajout avancé d’un tool MCP"));
|
||||
fireEvent.click(screen.getByLabelText("serveur MCP sans catalogue statique"));
|
||||
fireEvent.click(screen.getByRole("option", { name: "Unity Plugin — Legacy Tools" }));
|
||||
fireEvent.change(screen.getByLabelText("nom avancé du tool MCP"), {
|
||||
target: { value: "legacy_ping" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Attribuer" }));
|
||||
expect(await screen.findByText("unity_build")).toBeTruthy();
|
||||
expect(await screen.findByText(/legacy_ping/)).toBeTruthy();
|
||||
expect(screen.getByRole("status").textContent).toContain("Relance requise");
|
||||
|
||||
const settings = (await gateways.plugin.listProjectSettings("project-1")).settings;
|
||||
const agent = settings.agents.find((entry) => entry.agentId === "agent-1");
|
||||
expect(agent?.skills).toContainEqual({ pluginId: "dev.acme.unity", skillId: "unity.debug" });
|
||||
expect(agent?.tools).toContainEqual({
|
||||
const assigned = settings.agents.find((entry) => entry.agentId === agent.id);
|
||||
expect(assigned?.skills).toContainEqual({ pluginId: "dev.acme.unity", skillId: "unity.debug" });
|
||||
expect(assigned?.tools).toContainEqual({
|
||||
pluginId: "dev.acme.unity",
|
||||
serverId: "unity-tools",
|
||||
toolName: "unity_build",
|
||||
});
|
||||
expect(assigned?.tools).toContainEqual({
|
||||
pluginId: "dev.acme.unity",
|
||||
serverId: "legacy-tools",
|
||||
toolName: "legacy_ping",
|
||||
});
|
||||
});
|
||||
|
||||
it("aggregates native capabilities and uses the granular MCP tool toggle", async () => {
|
||||
const gateways = gatewaysWithPlugin();
|
||||
const agent = await gateways.agent.createAgent("project-1", {
|
||||
name: "Main",
|
||||
profileId: "profile-1",
|
||||
});
|
||||
await gateways.skill.createSkill({
|
||||
projectId: "project-1",
|
||||
scope: "project",
|
||||
name: "release-check",
|
||||
description: "Vérifier une release.",
|
||||
content: "# Release",
|
||||
});
|
||||
(gateways.permission as MockPermissionGateway).setMcpToolCatalogue({
|
||||
readOnlyTools: ["idea_ticket_read"],
|
||||
writeActionTools: ["idea_ticket_update"],
|
||||
tools: [
|
||||
{ name: "idea_ticket_read", description: "Lire un ticket.", access: "read" },
|
||||
{ name: "idea_ticket_update", description: "Modifier un ticket.", access: "writeAction" },
|
||||
],
|
||||
});
|
||||
const toggleSpy = vi.spyOn(gateways.permission, "setAgentMcpToolAllowed");
|
||||
|
||||
render(
|
||||
<DIProvider gateways={gateways}>
|
||||
<AgentPluginCapabilities projectId="project-1" agent={agent} active />
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
const nativeSkill = await screen.findByRole("checkbox", { name: /release-check/ });
|
||||
const nativeTool = screen.getByRole("checkbox", { name: /idea_ticket_update/ });
|
||||
expect(screen.getAllByText("IdeA").length).toBeGreaterThanOrEqual(2);
|
||||
fireEvent.click(nativeSkill);
|
||||
fireEvent.click(nativeTool);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(toggleSpy).toHaveBeenCalledWith(
|
||||
"project-1",
|
||||
agent.id,
|
||||
"idea_ticket_update",
|
||||
true,
|
||||
),
|
||||
);
|
||||
expect(screen.getByRole("status").textContent).toContain("Relance requise");
|
||||
});
|
||||
|
||||
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: "missing.plugin", serverId: "gone-server", toolName: "gone_tool" },
|
||||
],
|
||||
}],
|
||||
});
|
||||
|
||||
render(
|
||||
<DIProvider gateways={gateways}>
|
||||
<AgentPluginCapabilities projectId="project-1" agent={agent} active={false} />
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
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 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" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user