273 lines
8.9 KiB
TypeScript
273 lines
8.9 KiB
TypeScript
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);
|
||
});
|
||
});
|