feat(permissions): voie projection CLI (LP0→LP3) + checkpoint Codex/input
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>
This commit is contained in:
89
frontend/src/features/permissions/permissions.test.tsx
Normal file
89
frontend/src/features/permissions/permissions.test.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user