From 5ce9be16883efddb9ca2e84772bcc6b3e5860199 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 24 Dec 2025 23:31:31 +0700 Subject: [PATCH] Fix Claude test credential setup --- REPORT-claude-credentials-failure.md | 20 ++++++++++++++ .../src/server/agent/agent-mcp.e2e.test.ts | 13 +++++++++- .../agent/providers/claude-agent.test.ts | 26 +++++++++---------- plan.md | 3 ++- 4 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 REPORT-claude-credentials-failure.md diff --git a/REPORT-claude-credentials-failure.md b/REPORT-claude-credentials-failure.md new file mode 100644 index 000000000..4acdb7e7e --- /dev/null +++ b/REPORT-claude-credentials-failure.md @@ -0,0 +1,20 @@ +# Claude test failures: real root cause + +## Finding +The Claude E2E tests were not failing due to missing credentials in general; they were failing because the tests overwrite `CLAUDE_CONFIG_DIR` with a fresh temp directory and do **not** carry forward Claude's credential store. This removes the SDK's `.credentials.json` file when the SDK is running in plaintext storage mode (common in CI), so the SDK cannot load OAuth/API credentials even though the user has valid credentials in the default config directory. + +## Evidence +- The Claude agent SDK stores credentials in a file named `.credentials.json` under its config directory: + - `node_modules/@anthropic-ai/claude-agent-sdk/cli.js` shows the plaintext credential store path as `join(storageDir, ".credentials.json")`. +- The tests set `process.env.CLAUDE_CONFIG_DIR` to a temp dir in: + - `packages/server/src/server/agent/providers/claude-agent.test.ts` (via `useTempClaudeConfigDir`) + - `packages/server/src/server/agent/agent-mcp.e2e.test.ts` +- The temp config dir is populated with settings only, so the SDK can't find `.credentials.json` after the override. + +## Fix summary +- Copy `.credentials.json` from the original config directory (explicit `CLAUDE_CONFIG_DIR` or `~/.claude`) into the temp config dir used by tests. +- Remove the hardcoded env-var-only credential precheck so tests fail with the *real* SDK error if credentials are genuinely absent. + +## Files updated +- `packages/server/src/server/agent/providers/claude-agent.test.ts` +- `packages/server/src/server/agent/agent-mcp.e2e.test.ts` diff --git a/packages/server/src/server/agent/agent-mcp.e2e.test.ts b/packages/server/src/server/agent/agent-mcp.e2e.test.ts index e3b9893aa..e0ea54ebb 100644 --- a/packages/server/src/server/agent/agent-mcp.e2e.test.ts +++ b/packages/server/src/server/agent/agent-mcp.e2e.test.ts @@ -2,7 +2,7 @@ import net from "node:net"; import os from "node:os"; import path from "node:path"; import { existsSync } from "node:fs"; -import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { copyFile, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { describe, expect, test } from "vitest"; import { experimental_createMCPClient } from "ai"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; @@ -36,6 +36,14 @@ const CLAUDE_SETTINGS = { }, }; +async function copyClaudeCredentials(sourceDir: string, targetDir: string): Promise { + const sourceCredentials = path.join(sourceDir, ".credentials.json"); + if (!existsSync(sourceCredentials)) { + return; + } + await copyFile(sourceCredentials, path.join(targetDir, ".credentials.json")); +} + async function getAvailablePort(): Promise { return new Promise((resolve, reject) => { const server = net.createServer(); @@ -145,10 +153,13 @@ describe("agent MCP end-to-end", () => { process.env.CODEX_SESSION_DIR = codexSessionDir; process.env.CODEX_HOME = codexHome; const previousClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR; + const sourceClaudeConfigDir = + previousClaudeConfigDir ?? path.join(os.homedir(), ".claude"); const claudeConfigDir = await mkdtemp(path.join(os.tmpdir(), "claude-config-")); const claudeSettingsText = `${JSON.stringify(CLAUDE_SETTINGS, null, 2)}\n`; await writeFile(path.join(claudeConfigDir, "settings.json"), claudeSettingsText, "utf8"); await writeFile(path.join(claudeConfigDir, "settings.local.json"), claudeSettingsText, "utf8"); + await copyClaudeCredentials(sourceClaudeConfigDir, claudeConfigDir); process.env.CLAUDE_CONFIG_DIR = claudeConfigDir; const daemon = await createPaseoDaemon(daemonConfig); diff --git a/packages/server/src/server/agent/providers/claude-agent.test.ts b/packages/server/src/server/agent/providers/claude-agent.test.ts index cedad8dd1..3346dae83 100644 --- a/packages/server/src/server/agent/providers/claude-agent.test.ts +++ b/packages/server/src/server/agent/providers/claude-agent.test.ts @@ -2,6 +2,7 @@ import { afterAll, beforeAll, describe, expect, test, vi } from "vitest"; import { createServer as createHTTPServer } from "http"; import { randomUUID } from "node:crypto"; import { + copyFileSync, existsSync, mkdtempSync, readFileSync, @@ -34,17 +35,6 @@ import { AgentManager } from "../agent-manager.js"; import { AgentRegistry } from "../agent-registry.js"; import { createAgentMcpServer } from "../mcp-server.js"; -const hasClaudeCredentials = Boolean( - process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY?.trim()?.length -); -const requireClaudeCredentials = () => { - if (!hasClaudeCredentials) { - throw new Error( - "Claude credentials missing. Set CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY to run ClaudeAgentClient integration tests." - ); - } -}; - function tmpCwd(): string { const dir = mkdtempSync(path.join(os.tmpdir(), "claude-agent-e2e-")); try { @@ -54,8 +44,18 @@ 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: { @@ -72,6 +72,7 @@ function useTempClaudeConfigDir(): () => void { 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) { @@ -274,9 +275,6 @@ describe("ClaudeAgentClient (SDK integration)", () => { }, }); - beforeAll(() => { - requireClaudeCredentials(); - }); beforeAll(() => { restoreClaudeConfigDir = useTempClaudeConfigDir(); }); diff --git a/plan.md b/plan.md index b69e3cabc..fb7d32fa8 100644 --- a/plan.md +++ b/plan.md @@ -325,7 +325,7 @@ Build a new Codex MCP provider side‑by‑side with the existing Codex SDK prov - All tests must run unconditionally and pass - **Done (2025-12-24 23:21)**: WHAT: tightened Claude permission parity setup/cleanup and sandbox config in `packages/server/src/server/agent/providers/claude-agent.test.ts:259` and `packages/server/src/server/agent/providers/claude-agent.test.ts:495`, enabled Claude user+project settings in `packages/server/src/server/agent/providers/claude-agent.ts:536`, set CLAUDE test sandbox config in `packages/server/src/server/agent/agent-mcp.e2e.test.ts:26`, restored wait_for_agent completion via stream events in `packages/server/src/server/agent/agent-manager.ts:696`, and sanitized permission payloads for MCP responses in `packages/server/src/server/agent/mcp-server.ts:81` + `packages/server/src/server/agent/mcp-server.ts:263`. RESULT: Claude permission allow/deny/abort flows resolve correctly and MCP agent run completes without timeouts; create_agent no longer fails schema validation. EVIDENCE: `npm run test --workspace=@paseo/server -- claude-agent.test.ts -t "permission flow parity"` (3/3 passed) and `npm run test --workspace=@paseo/server -- agent-mcp.e2e.test.ts` (1/1 passed). -- [ ] **Fix**: Claude credentials work - find REAL reason Claude tests fail. +- [x] **Fix**: Claude credentials work - find REAL reason Claude tests fail. - **Context**: You (the agent reading this) ARE Codex. The test runs Codex MCP which launches Claude. - "Missing Claude credentials" is FALSE - verified with `scripts/codex-claude-test.ts`: @@ -337,6 +337,7 @@ Build a new Codex MCP provider side‑by‑side with the existing Codex SDK prov - So why do the E2E tests claim credentials are missing? - Investigate what the Claude E2E tests are ACTUALLY doing differently - Run the failing test with debug output and capture exact error + - **Done (2025-12-24 23:30)**: WHAT: copied Claude SDK `.credentials.json` into temp config dirs in `packages/server/src/server/agent/providers/claude-agent.test.ts:47` and `packages/server/src/server/agent/agent-mcp.e2e.test.ts:39`, removed the env-only credential gate in `packages/server/src/server/agent/providers/claude-agent.test.ts:55`, and documented the root cause in `REPORT-claude-credentials-failure.md`. RESULT: Claude tests no longer falsely report missing credentials when auth is stored in the default config dir. EVIDENCE: `npm run test --workspace=@paseo/server -- agent-mcp.e2e.test.ts` (1/1 passed) and `npm run test --workspace=@paseo/server -- claude-agent.test.ts -t "responds with text"` (1/1 passed; remaining tests skipped by filter). - [x] **Test (E2E) CRITICAL**: Interruption/abort latency for Codex MCP provider.