From 18cc63fa3947d056fc6380b5bf9e49f318471522 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 25 Dec 2025 17:48:02 +0700 Subject: [PATCH] Add daemon E2E test for setAgentMode() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added E2E test that verifies mode switching (auto → read-only → full-access) - Test verifies mode persists across messages - Fixed bug: setMode() now updates cachedRuntimeInfo so getRuntimeInfo() returns correct modeId after mode change 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../server/agent/providers/codex-mcp-agent.ts | 7 ++ packages/server/src/server/daemon.e2e.test.ts | 112 ++++++++++++++++++ plan.md | 3 +- 3 files changed, 121 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/agent/providers/codex-mcp-agent.ts b/packages/server/src/server/agent/providers/codex-mcp-agent.ts index 96d443635..ea52ea79c 100644 --- a/packages/server/src/server/agent/providers/codex-mcp-agent.ts +++ b/packages/server/src/server/agent/providers/codex-mcp-agent.ts @@ -2763,6 +2763,13 @@ class CodexMcpAgentSession implements AgentSession { async setMode(modeId: string): Promise { this.currentMode = modeId; this.config.modeId = modeId; + // Update cached runtime info to reflect mode change + if (this.cachedRuntimeInfo) { + this.cachedRuntimeInfo = { + ...this.cachedRuntimeInfo, + modeId, + }; + } } getPendingPermissions(): AgentPermissionRequest[] { diff --git a/packages/server/src/server/daemon.e2e.test.ts b/packages/server/src/server/daemon.e2e.test.ts index 070263a1e..066dc3ee9 100644 --- a/packages/server/src/server/daemon.e2e.test.ts +++ b/packages/server/src/server/daemon.e2e.test.ts @@ -708,6 +708,118 @@ describe("daemon E2E", () => { ); }); + describe("setAgentMode", () => { + test( + "switches agent mode and persists across messages", + async () => { + const cwd = tmpCwd(); + + // Create a Codex agent with default mode ("auto") + const agent = await ctx.client.createAgent({ + provider: "codex", + cwd, + title: "Mode Switch Test Agent", + }); + + expect(agent.id).toBeTruthy(); + expect(agent.status).toBe("idle"); + + // Verify initial mode is "auto" (the default) + expect(agent.currentModeId).toBe("auto"); + + // Clear message queue before mode switch + ctx.client.clearMessageQueue(); + const startPosition = ctx.client.getMessageQueue().length; + + // Switch to "read-only" mode + await ctx.client.setAgentMode(agent.id, "read-only"); + + // Wait for agent_state update reflecting the new mode + const stateAfterModeSwitch = await new Promise( + (resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error("Timeout waiting for mode change in agent_state")); + }, 10000); + + const checkForModeChange = (): void => { + const queue = ctx.client.getMessageQueue(); + for (let i = startPosition; i < queue.length; i++) { + const msg = queue[i]; + if ( + msg.type === "agent_state" && + msg.payload.id === agent.id && + msg.payload.currentModeId === "read-only" + ) { + clearTimeout(timeout); + clearInterval(interval); + resolve(msg.payload); + return; + } + } + }; + + const interval = setInterval(checkForModeChange, 50); + } + ); + + // Verify mode changed to "read-only" + expect(stateAfterModeSwitch.currentModeId).toBe("read-only"); + + // Now verify the mode persists: send a message and check the mode is still "read-only" + ctx.client.clearMessageQueue(); + await ctx.client.sendMessage(agent.id, "Say 'hello' and nothing else"); + + const finalState = await ctx.client.waitForAgentIdle(agent.id, 120000); + + // Mode should still be "read-only" after the message + expect(finalState.currentModeId).toBe("read-only"); + + // Also verify runtimeInfo has the updated modeId + expect(finalState.runtimeInfo?.modeId).toBe("read-only"); + + // Switch to another mode: "full-access" + ctx.client.clearMessageQueue(); + const position2 = ctx.client.getMessageQueue().length; + + await ctx.client.setAgentMode(agent.id, "full-access"); + + // Wait for agent_state update + const stateAfterFullAccess = await new Promise( + (resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error("Timeout waiting for full-access mode change")); + }, 10000); + + const checkForModeChange = (): void => { + const queue = ctx.client.getMessageQueue(); + for (let i = position2; i < queue.length; i++) { + const msg = queue[i]; + if ( + msg.type === "agent_state" && + msg.payload.id === agent.id && + msg.payload.currentModeId === "full-access" + ) { + clearTimeout(timeout); + clearInterval(interval); + resolve(msg.payload); + return; + } + } + }; + + const interval = setInterval(checkForModeChange, 50); + } + ); + + expect(stateAfterFullAccess.currentModeId).toBe("full-access"); + + // Cleanup + rmSync(cwd, { recursive: true, force: true }); + }, + 180000 // 3 minute timeout + ); + }); + // 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 diff --git a/plan.md b/plan.md index 93e3fdd81..14a6f57ec 100644 --- a/plan.md +++ b/plan.md @@ -1100,7 +1100,7 @@ Build a new Codex MCP provider side‑by‑side with the existing Codex SDK prov - DaemonClient `cancelAgent()` method verified working - **Done (2025-12-25 17:44)**: WHAT: Added E2E test in `packages/server/src/server/daemon.e2e.test.ts:573-708` that creates a Codex agent, sends "Run: sleep 30" to trigger a long-running operation, waits for "running" status, calls `cancelAgent(agentId)`, verifies the agent stops within 2 seconds, and checks for no zombie "sleep 30" processes. RESULT: Test passes - agent cancel request is received, turn_failed is emitted, and agent becomes idle/error within milliseconds (test completed in 291ms). EVIDENCE: `npm run test --workspace=@paseo/server -- daemon.e2e.test.ts -t "cancelAgent"` (1 passed, 9 skipped in 846ms). -- [ ] **Test**: Add daemon E2E test for `setAgentMode()`. +- [x] **Test**: Add daemon E2E test for `setAgentMode()`. Switch agent mode and verify it takes effect. @@ -1115,6 +1115,7 @@ Build a new Codex MCP provider side‑by‑side with the existing Codex SDK prov - Test passes - Mode switch persists across messages - DaemonClient `setAgentMode()` method verified working + - **Done (2025-12-25 17:48)**: WHAT: Added E2E test in `packages/server/src/server/daemon.e2e.test.ts:711-821` that creates Codex agent, verifies initial mode is "auto", switches to "read-only", verifies mode persists after sending a message, and switches to "full-access". FIXED BUG: `packages/server/src/server/agent/providers/codex-mcp-agent.ts:2763-2773` - `setMode()` was not updating `cachedRuntimeInfo`, so `getRuntimeInfo()` returned stale `modeId`. Fix: update `cachedRuntimeInfo.modeId` when mode changes. RESULT: Test passes - mode switch reflects in both `currentModeId` and `runtimeInfo.modeId`, persists across messages. EVIDENCE: `npm run test --workspace=@paseo/server -- daemon.e2e.test.ts -t "setAgentMode"` (1 passed, 10 skipped in 4154ms). - [ ] **Test**: Add daemon E2E test for `listAgents()`.