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:
@ -17,6 +17,8 @@ import { render, screen, waitFor, fireEvent } from "@testing-library/react";
|
||||
|
||||
import {
|
||||
MockAgentGateway,
|
||||
MockPermissionGateway,
|
||||
MockPluginGateway,
|
||||
MockProfileGateway,
|
||||
MockSkillGateway,
|
||||
} from "@/adapters/mock";
|
||||
@ -44,7 +46,13 @@ function renderSkillsPanel(agent?: MockAgentGateway, skill?: MockSkillGateway) {
|
||||
|
||||
function renderAgentsPanel(agent: MockAgentGateway, skill: MockSkillGateway) {
|
||||
const profile = new MockProfileGateway();
|
||||
const gateways = { agent, profile, skill } as unknown as Gateways;
|
||||
const gateways = {
|
||||
agent,
|
||||
profile,
|
||||
skill,
|
||||
permission: new MockPermissionGateway(),
|
||||
plugin: new MockPluginGateway(),
|
||||
} as unknown as Gateways;
|
||||
return render(
|
||||
<DIProvider gateways={gateways}>
|
||||
<AgentsPanel projectId={PROJECT_ID} projectRoot="/tmp/proj" />
|
||||
@ -58,11 +66,6 @@ async function waitForSkillsIdle() {
|
||||
});
|
||||
}
|
||||
|
||||
function chooseDropdownOption(label: string | RegExp, optionName: string | RegExp) {
|
||||
fireEvent.click(screen.getByLabelText(label));
|
||||
fireEvent.click(screen.getByRole("option", { name: optionName }));
|
||||
}
|
||||
|
||||
/** Opens the SkillEditor, fills it, and saves. */
|
||||
async function createSkill(
|
||||
name: string,
|
||||
@ -172,7 +175,7 @@ describe("SkillsPanel (with MockSkillGateway)", () => {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("Skill assignment (AgentsPanel + MockSkillGateway)", () => {
|
||||
it("assigns a skill to the selected agent and shows it as a chip", async () => {
|
||||
it("assigns a skill to the selected agent from the unified capabilities", async () => {
|
||||
const agent = new MockAgentGateway();
|
||||
const skill = new MockSkillGateway(agent);
|
||||
const created = await agent.createAgent(PROJECT_ID, {
|
||||
@ -191,14 +194,9 @@ describe("Skill assignment (AgentsPanel + MockSkillGateway)", () => {
|
||||
|
||||
// Select the agent
|
||||
fireEvent.click(screen.getByText("Worker"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByLabelText("skill to assign")).toBeTruthy(),
|
||||
);
|
||||
|
||||
// Choose the skill and assign
|
||||
const toggle = await screen.findByRole("checkbox", { name: /Deploy/ });
|
||||
const sk = (await skill.listSkills(PROJECT_ID, "project"))[0];
|
||||
chooseDropdownOption("skill to assign", "Deploy (project)");
|
||||
fireEvent.click(screen.getByRole("button", { name: "assign skill" }));
|
||||
fireEvent.click(toggle);
|
||||
|
||||
// The agent record now carries the skill ref
|
||||
await waitFor(async () => {
|
||||
@ -206,8 +204,7 @@ describe("Skill assignment (AgentsPanel + MockSkillGateway)", () => {
|
||||
const a = agents.find((x) => x.id === created.id)!;
|
||||
expect(a.skills.map((s) => s.skillId)).toContain(sk.id);
|
||||
});
|
||||
// Chip with an unassign control appears
|
||||
expect(screen.getByRole("button", { name: "unassign Deploy" })).toBeTruthy();
|
||||
expect((toggle as HTMLInputElement).checked).toBe(true);
|
||||
});
|
||||
|
||||
it("unassigns a skill from the agent", async () => {
|
||||
@ -229,12 +226,9 @@ describe("Skill assignment (AgentsPanel + MockSkillGateway)", () => {
|
||||
await waitFor(() => expect(screen.getByLabelText("agent name")).toBeTruthy());
|
||||
fireEvent.click(screen.getByText("Worker2"));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole("button", { name: "unassign Cleanup" }),
|
||||
).toBeTruthy(),
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "unassign Cleanup" }));
|
||||
const toggle = await screen.findByRole("checkbox", { name: /Cleanup/ });
|
||||
expect((toggle as HTMLInputElement).checked).toBe(true);
|
||||
fireEvent.click(toggle);
|
||||
|
||||
await waitFor(async () => {
|
||||
const agents = await agent.listAgents(PROJECT_ID);
|
||||
|
||||
Reference in New Issue
Block a user