Files
IdeA/frontend/src/features/permissions/permissions.test.tsx

273 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(
<DIProvider gateways={gateways}>
<PermissionsPanel projectId={PROJECT_ID} />
</DIProvider>,
);
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(
<DIProvider gateways={gateways}>
<PermissionsPanel projectId={PROJECT_ID} />
</DIProvider>,
);
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(
<DIProvider gateways={gateways}>
<PermissionsPanel projectId={PROJECT_ID} />
</DIProvider>,
);
await waitFor(() => {
expect(screen.getByText("Runtime verrouillé")).toBeTruthy();
});
expect(
(screen.getByLabelText("Réseau — defaults projet network") as HTMLSelectElement)
.disabled,
).toBe(false);
});
});