UI for the shared Claude token: a Settings section showing token state with Authenticate and Revoke, an acquisition modal built on the shared Modal (sign-in link handed to the host browser via the opener plugin, plus the code input that answers `setup-token`'s stdin prompt — the flow cannot complete without it), and a per-project opt-out toggle shown only for the Anthropic backend. Cancellation: acquire_claude_token previously had only two exits, completion and a 15-minute timeout, and held the single-flight guard for the whole time. Closing the dialog therefore locked the user out of retrying for up to 15 minutes. Adds cancel_claude_token, backed by a oneshot claimed and released in lockstep with the input guard, selected on in the run loop so it wins the race and tears the exec down. The dialog's Cancel now calls it and closes either way. Also refreshes CLAUDE.md, which had drifted: it documented the deleted ProjectCard, and asserted that new IPC commands need permission grants in capabilities/default.json — they do not, that file covers plugin commands only. Adds the conventions that would otherwise bite: container_needs_recreation() is purely label-based and never diffs env, so container-affecting state needs its own label; and #[serde(default)] on a bool yields false regardless of intent. Corrects the claim that Reset preserves credentials. Reset calls remove_project_volumes, which deletes both the home and claude-config volumes, so it wipes ~/.claude, the OAuth token, installed skills and session transcripts. 84 frontend tests, 34 Rust tests, both builds clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
143 lines
4.4 KiB
TypeScript
143 lines
4.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { render, screen, fireEvent } from "@testing-library/react";
|
|
import PermissionModeControl, {
|
|
effectivePermissionMode,
|
|
permissionModePatch,
|
|
} from "./PermissionModeControl";
|
|
import type { Project } from "../../lib/types";
|
|
|
|
const baseProject: Project = {
|
|
id: "p1",
|
|
name: "api-server",
|
|
paths: [{ host_path: "/src/api", mount_name: "api" }],
|
|
container_id: null,
|
|
status: "running",
|
|
backend: "anthropic",
|
|
bedrock_config: null,
|
|
ollama_config: null,
|
|
openai_compatible_config: null,
|
|
allow_docker_access: false,
|
|
sandbox_mode_enabled: true,
|
|
mission_control_enabled: false,
|
|
auth_bridge_enabled: false,
|
|
use_shared_auth_token: true,
|
|
full_permissions: false,
|
|
permission_mode: null,
|
|
ssh_key_path: null,
|
|
git_token: null,
|
|
git_user_name: null,
|
|
git_user_email: null,
|
|
custom_env_vars: [],
|
|
port_mappings: [],
|
|
claude_instructions: null,
|
|
claude_code_settings: null,
|
|
renamed_session_names: {},
|
|
created_at: "2026-01-01T00:00:00Z",
|
|
updated_at: "2026-01-01T00:00:00Z",
|
|
};
|
|
|
|
describe("effectivePermissionMode", () => {
|
|
it("falls back to the legacy boolean when permission_mode is null", () => {
|
|
expect(effectivePermissionMode(baseProject)).toBe("default");
|
|
expect(
|
|
effectivePermissionMode({ ...baseProject, full_permissions: true }),
|
|
).toBe("bypass");
|
|
});
|
|
|
|
it("prefers permission_mode when it is set", () => {
|
|
expect(
|
|
effectivePermissionMode({
|
|
...baseProject,
|
|
permission_mode: "plan",
|
|
full_permissions: true,
|
|
}),
|
|
).toBe("plan");
|
|
});
|
|
});
|
|
|
|
describe("permissionModePatch", () => {
|
|
it("keeps the legacy full_permissions flag in sync", () => {
|
|
expect(permissionModePatch("bypass")).toEqual({
|
|
permission_mode: "bypass",
|
|
full_permissions: true,
|
|
});
|
|
expect(permissionModePatch("acceptEdits")).toEqual({
|
|
permission_mode: "acceptEdits",
|
|
full_permissions: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("PermissionModeControl", () => {
|
|
const onChange = vi.fn();
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("renders all four modes as a radio group with the effective one checked", () => {
|
|
render(<PermissionModeControl project={baseProject} onChange={onChange} />);
|
|
const group = screen.getByRole("radiogroup", { name: "Permission mode" });
|
|
expect(group).toBeInTheDocument();
|
|
expect(screen.getAllByRole("radio")).toHaveLength(4);
|
|
expect(screen.getByRole("radio", { name: "Default" })).toHaveAttribute(
|
|
"aria-checked",
|
|
"true",
|
|
);
|
|
});
|
|
|
|
it("reports the picked mode", () => {
|
|
render(<PermissionModeControl project={baseProject} onChange={onChange} />);
|
|
fireEvent.click(screen.getByRole("radio", { name: "Accept Edits" }));
|
|
expect(onChange).toHaveBeenCalledWith("acceptEdits");
|
|
});
|
|
|
|
it("moves selection with the arrow keys", () => {
|
|
render(<PermissionModeControl project={baseProject} onChange={onChange} />);
|
|
fireEvent.keyDown(screen.getByRole("radiogroup", { name: "Permission mode" }), {
|
|
key: "ArrowRight",
|
|
});
|
|
expect(onChange).toHaveBeenCalledWith("acceptEdits");
|
|
});
|
|
|
|
it("shows sandbox state beside the control", () => {
|
|
render(<PermissionModeControl project={baseProject} onChange={onChange} />);
|
|
expect(screen.getByTestId("sandbox-state")).toHaveTextContent(
|
|
/Sandbox\s*ON/,
|
|
);
|
|
});
|
|
|
|
it("does not paint Bypass as dangerous while the sandbox contains it", () => {
|
|
render(
|
|
<PermissionModeControl
|
|
project={{ ...baseProject, permission_mode: "bypass" }}
|
|
onChange={onChange}
|
|
/>,
|
|
);
|
|
const bypass = screen.getByRole("radio", { name: "Bypass" });
|
|
expect(bypass.className).toContain("--accent-emphasis");
|
|
expect(bypass.className).not.toContain("--warning-emphasis");
|
|
expect(screen.getByTestId("permission-mode-hint")).toHaveTextContent(
|
|
/contained by the sandbox/i,
|
|
);
|
|
});
|
|
|
|
it("uses caution colour only when Bypass runs with the sandbox off", () => {
|
|
render(
|
|
<PermissionModeControl
|
|
project={{
|
|
...baseProject,
|
|
permission_mode: "bypass",
|
|
sandbox_mode_enabled: false,
|
|
}}
|
|
onChange={onChange}
|
|
/>,
|
|
);
|
|
const bypass = screen.getByRole("radio", { name: "Bypass" });
|
|
expect(bypass.className).toContain("--warning-emphasis");
|
|
expect(screen.getByTestId("permission-mode-hint")).toHaveTextContent(
|
|
/Caution/i,
|
|
);
|
|
});
|
|
});
|