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