import { describe, expect, it, vi } from "vitest"; import { fireEvent, render, screen, waitFor, within } from "@testing-library/react"; import { MockAgentGateway, MockPermissionGateway, MockProfileGateway, } from "@/adapters/mock"; import { DIProvider } from "@/app/di"; import type { Gateways } from "@/ports"; import { PermissionsPanel } from "./PermissionsPanel"; const PROJECT_ID = "proj-permissions-test"; async function renderPanel() { const agent = new MockAgentGateway(); const permission = new MockPermissionGateway(); await agent.createAgent(PROJECT_ID, { name: "Builder", profileId: "p1" }); const gateways = { agent, permission, profile: new MockProfileGateway(), } as unknown as Gateways; render( , ); await waitFor(() => { expect(screen.getByText("Builder")).toBeTruthy(); }); return { agent, permission }; } function clickChoice(groupName: string, choice: string) { const group = screen.getByRole("group", { name: groupName }); fireEvent.click(within(group).getByRole("button", { name: choice })); } describe("PermissionsPanel", () => { it("saves project defaults from the compact policy editor", async () => { const { permission } = await renderPanel(); fireEvent.change(screen.getByLabelText("Project defaults fallback"), { target: { value: "allow" }, }); clickChoice("Read permission", "Allow"); clickChoice("Bash permission", "Deny"); fireEvent.click(screen.getByRole("button", { name: "Save" })); await waitFor(async () => { const doc = await permission.getProjectPermissions(PROJECT_ID); expect(doc.projectDefaults?.fallback).toBe("allow"); expect(doc.projectDefaults?.rules).toEqual([ { capability: "read", effect: "allow", paths: ["**"] }, { capability: "executeBash", effect: "deny", commands: [] }, ]); }); }); it("saves and clears an agent override", async () => { const { permission } = await renderPanel(); fireEvent.click(screen.getByText("Builder")); fireEvent.change(screen.getByLabelText("Override — Builder fallback"), { target: { value: "deny" }, }); clickChoice("Write permission", "Allow"); fireEvent.click(screen.getByRole("button", { name: "Save" })); await waitFor(async () => { const doc = await permission.getProjectPermissions(PROJECT_ID); expect(doc.agents?.[0]?.permissions.fallback).toBe("deny"); expect(doc.agents?.[0]?.permissions.rules).toEqual([ { capability: "write", effect: "allow", paths: ["**"] }, ]); }); fireEvent.click(screen.getByRole("button", { name: "Clear" })); await waitFor(async () => { const doc = await permission.getProjectPermissions(PROJECT_ID); expect(doc.agents).toEqual([]); }); }); it("saves project network defaults in the separate system permissions document", async () => { const { permission } = await renderPanel(); fireEvent.change(screen.getByLabelText("Réseau — defaults projet network"), { target: { value: "allow" }, }); fireEvent.click(screen.getByRole("button", { name: "Save network" })); await waitFor(async () => { const doc = await permission.getProjectSystemPermissions(PROJECT_ID); expect(doc.projectDefault?.network).toBe("allow"); }); expect(screen.getAllByText("Réseau autorisé").length).toBeGreaterThan(0); }); it("defaults cross-project contactability off and persists the agent checkbox", async () => { const { agent, permission } = await renderPanel(); const contactableSpy = vi.spyOn(agent, "updateAgentContactable"); const systemSpy = vi.spyOn(permission, "updateAgentSystemPermissions"); fireEvent.click(screen.getByText("Builder")); const checkbox = screen.getByLabelText( "Contactable depuis d’autres projets", ) as HTMLInputElement; expect(checkbox.checked).toBe(false); expect( screen.getByText( "Autorise les agents d’autres projets IdeA à solliciter cet agent via l’orchestration inter-projets. L’agent reste soumis à ses permissions et à son contexte.", ), ).toBeTruthy(); await waitFor(() => expect(checkbox.disabled).toBe(false)); fireEvent.click(checkbox); const save = screen.getByRole("button", { name: "Save network" }); await waitFor(() => expect(save).toHaveProperty("disabled", false)); fireEvent.click(save); await waitFor(() => { expect(contactableSpy).toHaveBeenCalledWith( PROJECT_ID, expect.any(String), true, ); }); await waitFor(async () => { const agents = await agent.listAgents(PROJECT_ID); expect(agents[0].contactable).toBe(true); }); expect(systemSpy).not.toHaveBeenCalled(); }); it("renders the network control read-only when persisted control says so", async () => { const agent = new MockAgentGateway(); const permission = new MockPermissionGateway(); const created = await agent.createAgent(PROJECT_ID, { name: "Builder", profileId: "p1", }); permission.setResolvedAgentSystemPermissions(created.id, { wanted: "allow", effective: "deny", runtimeLock: { state: "locked", source: "external-runtime", reason: "Runtime network is locked.", }, control: { mode: "readOnly", reason: "Runtime network is locked." }, runtimeControl: { mode: "readOnly", reason: "Runtime network is locked." }, }); const gateways = { agent, permission, profile: new MockProfileGateway(), } as unknown as Gateways; render( , ); await waitFor(() => { expect(screen.getByText("Runtime verrouillé")).toBeTruthy(); }); expect(screen.getByText("Runtime network is locked.")).toBeTruthy(); expect( (screen.getByLabelText("Réseau — defaults projet network") as HTMLSelectElement) .disabled, ).toBe(true); }); it("keeps wanted network editable when no runtime is active", async () => { const { permission } = await renderPanel(); await waitFor(() => { expect(screen.getByText("Aucun runtime actif")).toBeTruthy(); }); expect( screen.getByText( "Aucun runtime actif. La configuration sera appliquée au prochain lancement.", ), ).toBeTruthy(); expect( (screen.getByLabelText("Réseau — defaults projet network") as HTMLSelectElement) .disabled, ).toBe(false); fireEvent.change(screen.getByLabelText("Réseau — defaults projet network"), { target: { value: "deny" }, }); fireEvent.click(screen.getByRole("button", { name: "Save network" })); await waitFor(async () => { const doc = await permission.getProjectSystemPermissions(PROJECT_ID); expect(doc.projectDefault?.network).toBe("deny"); }); }); it("keeps an idle agent editable by default and only enables Save after a change", async () => { await renderPanel(); fireEvent.click(screen.getByText("Builder")); await waitFor(() => { expect(screen.getByText("Aucun runtime actif")).toBeTruthy(); }); expect(screen.queryByText("Lecture seule")).toBeNull(); expect(screen.queryByText("Runtime verrouillé")).toBeNull(); const select = screen.getByLabelText("Réseau — Builder network") as HTMLSelectElement; const save = screen.getByRole("button", { name: "Save network" }); expect(select.disabled).toBe(false); expect(save).toHaveProperty("disabled", true); fireEvent.change(select, { target: { value: "allow" } }); expect(save).toHaveProperty("disabled", false); }); it("keeps wanted network editable when only runtime control is read-only", async () => { const agent = new MockAgentGateway(); const permission = new MockPermissionGateway(); const created = await agent.createAgent(PROJECT_ID, { name: "Builder", profileId: "p1", }); permission.setResolvedAgentSystemPermissions(created.id, { wanted: "allow", effective: "deny", runtimeLock: { state: "locked", source: "external-runtime", reason: "IdeA cannot change this active runtime.", }, control: { mode: "editable" }, runtimeControl: { mode: "readOnly", reason: "IdeA cannot change this active runtime.", }, }); const gateways = { agent, permission, profile: new MockProfileGateway(), } as unknown as Gateways; render( , ); await waitFor(() => { expect(screen.getByText("Runtime verrouillé")).toBeTruthy(); }); expect( (screen.getByLabelText("Réseau — defaults projet network") as HTMLSelectElement) .disabled, ).toBe(false); }); });