Add temp Claude config isolation to daemon E2E tests

- Create shared useTempClaudeConfigDir() helper in test-utils/claude-config.ts
- Export from test-utils/index.ts for reuse across test files
- Update daemon.e2e.test.ts to use beforeAll/afterAll hooks with temp config
- Remove .skip from "permission flow: Claude" describe block
- Refactor claude-agent.test.ts to use shared helper instead of local copy

This fixes the daemon E2E Claude permission tests that were skipped due to
the user's real ~/.claude/settings.json having "Bash(rm:*)" in the allow
list, which caused rm commands to auto-execute without permission prompts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-12-25 18:06:44 +07:00
co-authored by Claude Opus 4.5
parent 78dec91bdf
commit 46ddb5a20c
5 changed files with 154 additions and 50 deletions
@@ -2,7 +2,6 @@ import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";
import { createServer } from "http";
import { randomUUID } from "node:crypto";
import {
copyFileSync,
existsSync,
mkdtempSync,
readFileSync,
@@ -17,6 +16,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import { ClaudeAgentClient, convertClaudeHistoryEntry } from "./claude-agent.js";
import { useTempClaudeConfigDir } from "../../test-utils/claude-config.js";
import {
hydrateStreamState,
type StreamItem,
@@ -46,46 +46,6 @@ function tmpCwd(): string {
}
}
function copyClaudeCredentials(sourceDir: string, targetDir: string): void {
const sourceCredentials = path.join(sourceDir, ".credentials.json");
if (!existsSync(sourceCredentials)) {
return;
}
copyFileSync(sourceCredentials, path.join(targetDir, ".credentials.json"));
}
function useTempClaudeConfigDir(): () => void {
const previousConfigDir = process.env.CLAUDE_CONFIG_DIR;
const sourceConfigDir =
previousConfigDir ?? path.join(os.homedir(), ".claude");
const configDir = mkdtempSync(path.join(os.tmpdir(), "claude-config-"));
const settings = {
permissions: {
allow: [],
deny: [],
ask: ["Bash(rm:*)"],
additionalDirectories: [],
},
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: false,
},
};
const settingsText = `${JSON.stringify(settings, null, 2)}\n`;
writeFileSync(path.join(configDir, "settings.json"), settingsText, "utf8");
writeFileSync(path.join(configDir, "settings.local.json"), settingsText, "utf8");
copyClaudeCredentials(sourceConfigDir, configDir);
process.env.CLAUDE_CONFIG_DIR = configDir;
return () => {
if (previousConfigDir === undefined) {
delete process.env.CLAUDE_CONFIG_DIR;
} else {
process.env.CLAUDE_CONFIG_DIR = previousConfigDir;
}
rmSync(configDir, { recursive: true, force: true });
};
}
async function autoApprove(session: Awaited<ReturnType<ClaudeAgentClient["createSession"]>>, event: AgentStreamEvent) {
if (event.type === "permission_requested") {
await session.respondToPermission(event.request.id, { behavior: "allow" });
+15 -9
View File
@@ -1,10 +1,11 @@
import { describe, test, expect, beforeEach, afterEach } from "vitest";
import { describe, test, expect, beforeEach, afterEach, beforeAll, afterAll } from "vitest";
import { mkdtempSync, writeFileSync, existsSync, rmSync } from "fs";
import { tmpdir } from "os";
import path from "path";
import {
createDaemonTestContext,
type DaemonTestContext,
useTempClaudeConfigDir,
} from "./test-utils/index.js";
import type { AgentTimelineItem } from "./agent/agent-sdk-types.js";
import type { AgentSnapshotPayload } from "./messages.js";
@@ -900,14 +901,19 @@ describe("daemon E2E", () => {
);
});
// Claude permission tests are skipped due to SDK behavior:
// - The sandbox config IS passed correctly to Claude SDK
// - Claude executes tool calls without requesting permission
// - This appears to be related to user/project settings or SDK behavior
// - The direct claude-agent.test.ts permission tests pass
// - Codex permission tests through the daemon work correctly
// TODO: Investigate Claude SDK permission behavior in daemon context
describe.skip("permission flow: Claude", () => {
describe("permission flow: Claude", () => {
// Use isolated Claude config to ensure permission prompts are triggered
// (user's real config may have allow rules that auto-approve commands)
let restoreClaudeConfig: () => void;
beforeAll(() => {
restoreClaudeConfig = useTempClaudeConfigDir();
});
afterAll(() => {
restoreClaudeConfig();
});
test(
"approves permission and executes command",
async () => {
@@ -0,0 +1,53 @@
import { mkdtempSync, writeFileSync, copyFileSync, existsSync, rmSync } from "fs";
import { tmpdir, homedir } from "os";
import path from "path";
function copyClaudeCredentials(sourceDir: string, targetDir: string): void {
const sourceCredentials = path.join(sourceDir, ".credentials.json");
if (!existsSync(sourceCredentials)) {
return;
}
copyFileSync(sourceCredentials, path.join(targetDir, ".credentials.json"));
}
/**
* Sets up an isolated Claude config directory for testing.
* Creates a temp directory with:
* - settings.json with ask: ["Bash(rm:*)"] to trigger permission prompts
* - settings.local.json with the same settings
* - .credentials.json copied from user's real config
*
* Sets CLAUDE_CONFIG_DIR env var to point to the temp directory.
* Returns a cleanup function that restores the original env and removes the temp dir.
*/
export function useTempClaudeConfigDir(): () => void {
const previousConfigDir = process.env.CLAUDE_CONFIG_DIR;
const sourceConfigDir =
previousConfigDir ?? path.join(homedir(), ".claude");
const configDir = mkdtempSync(path.join(tmpdir(), "claude-config-"));
const settings = {
permissions: {
allow: [],
deny: [],
ask: ["Bash(rm:*)"],
additionalDirectories: [],
},
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: false,
},
};
const settingsText = `${JSON.stringify(settings, null, 2)}\n`;
writeFileSync(path.join(configDir, "settings.json"), settingsText, "utf8");
writeFileSync(path.join(configDir, "settings.local.json"), settingsText, "utf8");
copyClaudeCredentials(sourceConfigDir, configDir);
process.env.CLAUDE_CONFIG_DIR = configDir;
return () => {
if (previousConfigDir === undefined) {
delete process.env.CLAUDE_CONFIG_DIR;
} else {
process.env.CLAUDE_CONFIG_DIR = previousConfigDir;
}
rmSync(configDir, { recursive: true, force: true });
};
}
@@ -11,3 +11,4 @@ export {
createDaemonTestContext,
type DaemonTestContext,
} from "./daemon-test-context.js";
export { useTempClaudeConfigDir } from "./claude-config.js";