fix(permissions): tools MCP inter-projets exposés (idea_project_*) + contactable agent/template persisté — update_agent_contactable (application/web-server/app-tauri), defaultContactable TemplateEditor save/reopen, groupes MCP 'IdeA inter-projets' avec fallback, permissions agent Contactable — #267 (QA verte)

This commit is contained in:
2026-09-07 18:13:27 +02:00
parent 9266d121cd
commit f6101a8249
30 changed files with 632 additions and 89 deletions

View File

@ -17,7 +17,12 @@
import { describe, it, expect } from "vitest";
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
import { MockAgentGateway, MockProfileGateway, MockTemplateGateway } from "@/adapters/mock";
import {
MockAgentGateway,
MockPermissionGateway,
MockProfileGateway,
MockTemplateGateway,
} from "@/adapters/mock";
import type { Gateways } from "@/ports";
import { DIProvider } from "@/app/di";
import { TemplatesPanel } from "./TemplatesPanel";
@ -33,6 +38,7 @@ interface RenderOpts {
agent?: MockAgentGateway;
template?: MockTemplateGateway;
profile?: MockProfileGateway;
permission?: MockPermissionGateway;
}
/**
@ -43,11 +49,13 @@ function renderTemplatesPanel(opts: RenderOpts = {}) {
const agent = opts.agent ?? new MockAgentGateway();
const tmpl = opts.template ?? new MockTemplateGateway(agent);
const profile = opts.profile ?? new MockProfileGateway();
const gateways = { agent, profile, template: tmpl } as unknown as Gateways;
const permission = opts.permission ?? new MockPermissionGateway();
const gateways = { agent, permission, profile, template: tmpl } as unknown as Gateways;
return {
agent,
tmpl,
profile,
permission,
...render(
<DIProvider gateways={gateways}>
<TemplatesPanel projectId={PROJECT_ID} />
@ -187,11 +195,40 @@ describe("TemplatesPanel (with MockTemplateGateway)", () => {
expect(templates[0]).toMatchObject({
defaultAgentName: "Main",
defaultProfileId: "profile-main",
systemPermissions: { network: "allow", crossProjectContactable: true },
defaultContactable: true,
systemPermissions: { network: "allow" },
mcpToolPermissions: { allowedTools: ["idea_ticket_read"] },
});
});
it("preserves defaultContactable after save and reopen", async () => {
const { tmpl } = renderTemplatesPanel();
await waitForTemplatesIdle();
fireEvent.click(screen.getByRole("button", { name: "create template" }));
await screen.findByLabelText("template name");
fireEvent.change(screen.getByLabelText("template name"), {
target: { value: "Contactable" },
});
fireEvent.click(screen.getByLabelText("Contactable depuis d’autres projets"));
fireEvent.click(screen.getByRole("button", { name: "Save template" }));
await screen.findByText("Contactable");
expect((await tmpl.listTemplates())[0]).toMatchObject({
defaultContactable: true,
systemPermissions: null,
});
fireEvent.click(screen.getByRole("button", { name: "edit Contactable" }));
await waitFor(() => {
expect(
(screen.getByLabelText(
"Contactable depuis d’autres projets",
) as HTMLInputElement).checked,
).toBe(true);
});
});
it("defaults template cross-project contactability off when the field is absent", async () => {
renderTemplatesPanel();
await waitForTemplatesIdle();
@ -211,6 +248,28 @@ describe("TemplatesPanel (with MockTemplateGateway)", () => {
).toBeTruthy();
});
it("shows cross-project and ungrouped catalogued MCP tools in the template editor", async () => {
const permission = new MockPermissionGateway();
permission.setMcpToolCatalogue({
readOnlyTools: ["idea_project_list", "idea_future_read"],
writeActionTools: ["idea_project_cancel_agent"],
});
renderTemplatesPanel({ permission });
await waitForTemplatesIdle();
fireEvent.click(screen.getByRole("button", { name: "create template" }));
expect(await screen.findByText("IdeA inter-projets")).toBeTruthy();
expect(
screen.getByLabelText("template MCP tool idea_project_cancel_agent"),
).toBeTruthy();
expect(screen.getByText("Autres")).toBeTruthy();
const fallback = screen.getByLabelText(
"template MCP tool idea_future_read",
) as HTMLInputElement;
expect(fallback.disabled).toBe(false);
});
it("the Create button is disabled when the name is empty", async () => {
renderTemplatesPanel();
await waitForTemplatesIdle();