Jalon vert regroupant deux chantiers entrelacés dans le working tree, indissociables au niveau fichier mais tous deux verts (cargo test --workspace + tests frontend permissions au vert). Permissions — voie « projection CLI » (advisory), complète : - LP0 domaine pur : modèle PermissionSet/EffectivePermissions, resolve deny-wins + postures Allow<Ask<Deny (crates/domain/src/permission.rs). - LP1 store : FsPermissionStore (.ideai/permissions.json). - LP2 use cases : Get/Update project, Update agent override, Resolve. - LP3 projecteurs Claude/Codex (settings.local.json / config.toml), câblage launch-path + PermissionProjectorRegistry, nettoyage des fichiers Replace orphelins au swap de profil (LP3-4), composition root + commandes Tauri, UI PermissionsPanel (projet + override agent). - ports.rs : PermissionStore + FileSystem::remove_file (cleanup au swap). Reste ouvert (hors scope, marqué dans le code) : LP4 enforcement OS airtight (Landlock fichiers) + résumé de permissions injecté. Inclut aussi le chantier Codex/input/sessions structurées en cours (McpConfigStrategy, StructuredAdapter, gestion d'input) partageant les mêmes fichiers (lifecycle.rs, commands.rs, dto.rs, state.rs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
90 lines
2.8 KiB
TypeScript
90 lines
2.8 KiB
TypeScript
import { describe, expect, it } 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([]);
|
|
});
|
|
});
|
|
});
|