generated from nhcarrigan/template
fix: include auto_granted_tools when reconnecting after permission approval
Closes #198 — default permissions were lost after the first permission modal reconnect because PermissionModal only passed session-granted tools to start_claude. Now merges auto_granted_tools from config, matching the behaviour of every other start_claude call site.
This commit is contained in:
@@ -86,7 +86,7 @@
|
|||||||
api_key: config.api_key || null,
|
api_key: config.api_key || null,
|
||||||
custom_instructions: config.custom_instructions || null,
|
custom_instructions: config.custom_instructions || null,
|
||||||
mcp_servers_json: config.mcp_servers_json || null,
|
mcp_servers_json: config.mcp_servers_json || null,
|
||||||
allowed_tools: newGrantedTools,
|
allowed_tools: [...new Set([...newGrantedTools, ...config.auto_granted_tools])],
|
||||||
use_worktree: config.use_worktree ?? false,
|
use_worktree: config.use_worktree ?? false,
|
||||||
disable_1m_context: config.disable_1m_context ?? false,
|
disable_1m_context: config.disable_1m_context ?? false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
/**
|
||||||
|
* PermissionModal Component Tests
|
||||||
|
*
|
||||||
|
* Tests the pure helper functions used by the PermissionModal component.
|
||||||
|
*
|
||||||
|
* What this component does:
|
||||||
|
* - Displays pending permission requests from Claude Code
|
||||||
|
* - Allows the user to approve or dismiss permission requests
|
||||||
|
* - On approval, reconnects Claude with the newly granted tools merged with
|
||||||
|
* `auto_granted_tools` from config (bug fix: issue #198)
|
||||||
|
* - Restores conversation context after reconnecting
|
||||||
|
*
|
||||||
|
* Manual testing checklist:
|
||||||
|
* - [ ] Permission modal appears when Claude requests a tool not in allowed_tools
|
||||||
|
* - [ ] All permissions are pre-selected by default when modal opens
|
||||||
|
* - [ ] "Select All" and "Select None" buttons work correctly
|
||||||
|
* - [ ] "Already Granted" badge appears for tools already in the session grant list
|
||||||
|
* - [ ] Approving permissions reconnects Claude and restores conversation context
|
||||||
|
* - [ ] After reconnecting, auto_granted_tools are still respected (no re-prompting)
|
||||||
|
* - [ ] Dismissing the modal clears pending permissions without reconnecting
|
||||||
|
* - [ ] Enter key approves selected permissions
|
||||||
|
* - [ ] Escape key dismisses the modal
|
||||||
|
* - [ ] Character enters "permission" state when modal appears
|
||||||
|
* - [ ] Input details are shown in a collapsible "View details" section
|
||||||
|
*
|
||||||
|
* Note: The `handleApproveAndReconnect` function cannot be unit tested here
|
||||||
|
* because it depends on Tauri IPC calls (`invoke("stop_claude")`,
|
||||||
|
* `invoke("start_claude")`, `invoke("send_prompt")`). The critical bug fix
|
||||||
|
* (including `auto_granted_tools` in the reconnect's `allowed_tools`) is
|
||||||
|
* covered by the `buildAllowedToolsList` tests below, which replicate the
|
||||||
|
* exact merging logic from the component.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replicates the allowed-tools merging logic from PermissionModal's
|
||||||
|
* handleApproveAndReconnect. This is the fix for issue #198: previously,
|
||||||
|
* `auto_granted_tools` were not included when reconnecting, causing them to
|
||||||
|
* be silently dropped and prompting the user again on subsequent requests.
|
||||||
|
*/
|
||||||
|
function buildAllowedToolsList(
|
||||||
|
sessionGrantedTools: string[],
|
||||||
|
newlyGrantedTools: string[],
|
||||||
|
autoGrantedTools: string[]
|
||||||
|
): string[] {
|
||||||
|
return [...new Set([...sessionGrantedTools, ...newlyGrantedTools, ...autoGrantedTools])];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replicates the formatInput helper from PermissionModal, used to display
|
||||||
|
* the tool input JSON in the permission details section.
|
||||||
|
*/
|
||||||
|
function formatInput(input: Record<string, unknown>): string {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(input, null, 2);
|
||||||
|
} catch {
|
||||||
|
return String(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replicates the isToolAlreadyGranted helper from PermissionModal.
|
||||||
|
*/
|
||||||
|
function isToolAlreadyGranted(toolName: string, grantedToolsList: string[]): boolean {
|
||||||
|
return grantedToolsList.includes(toolName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---
|
||||||
|
|
||||||
|
describe("buildAllowedToolsList", () => {
|
||||||
|
it("merges session-granted, newly-granted, and auto-granted tools", () => {
|
||||||
|
const result = buildAllowedToolsList(["Bash"], ["Glob"], ["Read"]);
|
||||||
|
expect(result).toContain("Bash");
|
||||||
|
expect(result).toContain("Glob");
|
||||||
|
expect(result).toContain("Read");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deduplicates tools that appear in multiple lists", () => {
|
||||||
|
const result = buildAllowedToolsList(["Read", "Bash"], ["Read"], ["Read", "Write"]);
|
||||||
|
const readCount = result.filter((t) => t === "Read").length;
|
||||||
|
expect(readCount).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves auto_granted_tools even when session list is empty", () => {
|
||||||
|
const result = buildAllowedToolsList([], ["Bash"], ["Read", "Glob"]);
|
||||||
|
expect(result).toContain("Read");
|
||||||
|
expect(result).toContain("Glob");
|
||||||
|
expect(result).toContain("Bash");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns only auto_granted_tools when no other grants exist", () => {
|
||||||
|
const result = buildAllowedToolsList([], [], ["Read"]);
|
||||||
|
expect(result).toEqual(["Read"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty array when all lists are empty", () => {
|
||||||
|
const result = buildAllowedToolsList([], [], []);
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reproduces the bug scenario from issue #198", () => {
|
||||||
|
// Scenario: user has Read in auto_granted_tools.
|
||||||
|
// Session starts correctly with Read allowed.
|
||||||
|
// User approves Bash via permission modal.
|
||||||
|
// Before fix: reconnect only passed [Bash], dropping Read.
|
||||||
|
// After fix: reconnect passes [Bash, Read].
|
||||||
|
const sessionGrantedTools: string[] = []; // no prior session grants
|
||||||
|
const newlyGrantedTools = ["Bash"]; // just approved via modal
|
||||||
|
const autoGrantedTools = ["Read"]; // configured default
|
||||||
|
|
||||||
|
const result = buildAllowedToolsList(sessionGrantedTools, newlyGrantedTools, autoGrantedTools);
|
||||||
|
|
||||||
|
expect(result).toContain("Bash");
|
||||||
|
expect(result).toContain("Read"); // Must be present — this was the bug!
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("formatInput", () => {
|
||||||
|
it("formats a simple object as pretty-printed JSON", () => {
|
||||||
|
const result = formatInput({ file_path: "/home/naomi/test.ts" });
|
||||||
|
expect(result).toBe(JSON.stringify({ file_path: "/home/naomi/test.ts" }, null, 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats a nested object correctly", () => {
|
||||||
|
const input = { command: "ls", args: ["-la", "/home"] };
|
||||||
|
const result = formatInput(input);
|
||||||
|
expect(result).toContain('"command": "ls"');
|
||||||
|
expect(result).toContain('"args"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats an empty object as '{}'", () => {
|
||||||
|
const result = formatInput({});
|
||||||
|
expect(result).toBe("{}");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isToolAlreadyGranted", () => {
|
||||||
|
it("returns true when the tool is in the granted list", () => {
|
||||||
|
expect(isToolAlreadyGranted("Read", ["Read", "Bash"])).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when the tool is not in the granted list", () => {
|
||||||
|
expect(isToolAlreadyGranted("Write", ["Read", "Bash"])).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for an empty granted list", () => {
|
||||||
|
expect(isToolAlreadyGranted("Read", [])).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is case-sensitive", () => {
|
||||||
|
expect(isToolAlreadyGranted("read", ["Read"])).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user