diff --git a/packages/server/src/server/daemon.e2e.test.ts b/packages/server/src/server/daemon.e2e.test.ts index c19419df4..3e6daffb4 100644 --- a/packages/server/src/server/daemon.e2e.test.ts +++ b/packages/server/src/server/daemon.e2e.test.ts @@ -479,6 +479,97 @@ describe("daemon E2E", () => { ); }); + describe("timestamp behavior", () => { + test( + "opening agent without interaction does not update timestamp", + async () => { + const cwd = tmpCwd(); + + // Create a Codex agent + const agent = await ctx.client.createAgent({ + provider: "codex", + cwd, + title: "Timestamp Test Agent", + }); + + expect(agent.id).toBeTruthy(); + expect(agent.status).toBe("idle"); + + // Record the initial updatedAt timestamp + const initialUpdatedAt = agent.updatedAt; + expect(initialUpdatedAt).toBeTruthy(); + + // Wait a bit to ensure any timestamp update would be visible + await new Promise((resolve) => setTimeout(resolve, 1500)); + + // Clear message queue before the "click" action + ctx.client.clearMessageQueue(); + + // Simulate clicking on the agent (initialize_agent_request) + // This is what happens when the user opens an agent in the UI + const refreshedState = await ctx.client.initializeAgent(agent.id); + + // Verify agent is still idle + expect(refreshedState.status).toBe("idle"); + + // CRITICAL: The timestamp should NOT have changed + // Just opening/clicking an agent should not update its updatedAt + expect(refreshedState.updatedAt).toBe(initialUpdatedAt); + + // Also clear attention (what happens when opening an agent with notification) + await ctx.client.clearAgentAttention(agent.id); + + // Get the state again after clearing attention + const stateAfterClear = await ctx.client.initializeAgent(agent.id); + + // Timestamp should STILL not have changed + expect(stateAfterClear.updatedAt).toBe(initialUpdatedAt); + + // Cleanup + rmSync(cwd, { recursive: true, force: true }); + }, + 60000 + ); + + test( + "sending message DOES update timestamp", + async () => { + const cwd = tmpCwd(); + + // Create a Codex agent + const agent = await ctx.client.createAgent({ + provider: "codex", + cwd, + title: "Timestamp Update Test Agent", + }); + + expect(agent.id).toBeTruthy(); + expect(agent.status).toBe("idle"); + + // Record the initial updatedAt timestamp + const initialUpdatedAt = new Date(agent.updatedAt); + + // Wait a bit to ensure timestamp difference is visible + await new Promise((resolve) => setTimeout(resolve, 1500)); + + // Send a message (this SHOULD update the timestamp) + await ctx.client.sendMessage(agent.id, "Say 'test' and nothing else"); + + // Wait for agent to complete + const finalState = await ctx.client.waitForAgentIdle(agent.id, 120000); + expect(finalState.status).toBe("idle"); + + // The timestamp SHOULD have been updated (should be later than initial) + const finalUpdatedAt = new Date(finalState.updatedAt); + expect(finalUpdatedAt.getTime()).toBeGreaterThan(initialUpdatedAt.getTime()); + + // Cleanup + rmSync(cwd, { recursive: true, force: true }); + }, + 180000 + ); + }); + // 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/packages/server/src/server/test-utils/daemon-client.ts b/packages/server/src/server/test-utils/daemon-client.ts index 3890782c5..f3e7320b1 100644 --- a/packages/server/src/server/test-utils/daemon-client.ts +++ b/packages/server/src/server/test-utils/daemon-client.ts @@ -265,6 +265,44 @@ export class DaemonClient { ); // 60 second timeout for initialization } + /** + * Initialize an agent (fetch its current state without running a prompt). + * This mimics what happens when clicking on an agent in the UI. + */ + async initializeAgent(agentId: string): Promise { + const requestId = nanoid(); + const startPosition = this.messageQueue.length; + + this.send({ + type: "initialize_agent_request", + agentId, + requestId, + }); + + // Wait for agent_state with this agent's ID + return this.waitFor( + (msg) => { + if (msg.type === "agent_state" && msg.payload.id === agentId) { + return msg.payload; + } + return null; + }, + 10000, + { skipQueueBefore: startPosition } + ); + } + + /** + * Clear agent attention (mark as viewed). + * This is what happens when opening an agent that requires attention. + */ + async clearAgentAttention(agentId: string): Promise { + this.send({ + type: "clear_agent_attention", + agentId, + }); + } + // ============================================================================ // Agent Interaction // ============================================================================ diff --git a/plan.md b/plan.md index cb7cb7605..976c06eeb 100644 --- a/plan.md +++ b/plan.md @@ -1051,7 +1051,7 @@ Build a new Codex MCP provider side‑by‑side with the existing Codex SDK prov - Document what's tested vs not tested - **Done (2025-12-25 18:10)**: WHAT: Ran full E2E suite and created comprehensive audit report at `REPORT-daemon-e2e-audit.md:1-163`. RESULT: 5 tests pass, 2 skipped. Coverage analysis: (1) Core flows covered: create agent, send message, permissions (approve/deny), persistence/resume, multi-agent orchestration - all for Codex. (2) DaemonClient methods untested: `cancelAgent()`, `setAgentMode()`, `listAgents()`. (3) Providers: Codex fully covered, Claude has 0 passing tests (permission issue). (4) Message types: 6/15 inbound types tested, 5/17 outbound types tested. EVIDENCE: `npm run test --workspace=@paseo/server -- daemon.e2e.test.ts` (5 passed, 2 skipped in 40.8s). Report includes prioritized recommendations: P1 (cancelAgent, setAgentMode, listAgents tests), P2 (Claude provider parity), P3 (error/edge cases), P4 (git/file-explorer features). -- [ ] **BUG**: Agent timestamp updates when clicking/opening agent without interaction. +- [x] **BUG**: Agent timestamp updates when clicking/opening agent without interaction. **Symptom**: In the app: - Click on an agent to open it @@ -1080,3 +1080,71 @@ Build a new Codex MCP provider side‑by‑side with the existing Codex SDK prov - Opening agent without interaction does NOT update timestamp - Sending message DOES update timestamp - Agent list order reflects actual last interaction time + - **Done (2025-12-25 17:42)**: WHAT: (1) Bug was already fixed in commit `7b29978` (Dec 2, 2025) which removed timestamp thrashing - previously `lastActivityAt`/`updatedAt` was updated on every `agent_stream` event (15+ times/second during streaming); (2) Added `initializeAgent()` and `clearAgentAttention()` methods to `packages/server/src/server/test-utils/daemon-client.ts:268-304`; (3) Added E2E tests in `packages/server/src/server/daemon.e2e.test.ts:482-570` for timestamp behavior - "opening agent without interaction does not update timestamp" and "sending message DOES update timestamp". RESULT: Bug verified as fixed - clicking/opening agent does NOT update timestamp, only actual interactions (sending messages) update it. Server only sets `agent.updatedAt` in `recordUserMessage` (`packages/server/src/server/agent/agent-manager.ts:436`) and `handleStreamEvent` (`packages/server/src/server/agent/agent-manager.ts:864`), not in `clearAgentAttention` or `initializeAgent` flows. EVIDENCE: `npm run test --workspace=@paseo/server -- daemon.e2e.test.ts -t "timestamp"` (2 passed, 7 skipped in 9.08s), Playwright test showed agent stayed in position 4 with unchanged timestamp after clicking. + +- [ ] **Test**: Add daemon E2E test for `cancelAgent()`. + + Cancel an agent mid-execution and verify it stops properly. + + **Test case**: + 1. Create Codex agent + 2. Send prompt that triggers a long-running operation (e.g., "Run: sleep 30") + 3. Wait for tool call to start (status: "running") + 4. Call `cancelAgent(agentId)` + 5. Verify agent reaches idle or error state within 2 seconds + 6. Verify no zombie processes left + + **Acceptance criteria**: + - Test passes + - Agent stops within reasonable time after cancel + - DaemonClient `cancelAgent()` method verified working + +- [ ] **Test**: Add daemon E2E test for `setAgentMode()`. + + Switch agent mode and verify it takes effect. + + **Test case**: + 1. Create Codex agent (default mode) + 2. Verify initial mode in agent state + 3. Call `setAgentMode(agentId, "plan")` or another valid mode + 4. Verify mode change reflected in next `agent_state` + 5. Send a message and verify agent behaves according to new mode + + **Acceptance criteria**: + - Test passes + - Mode switch persists across messages + - DaemonClient `setAgentMode()` method verified working + +- [ ] **Test**: Add daemon E2E test for `listAgents()`. + + Verify session state returns current agents. + + **Test case**: + 1. Connect client + 2. Create 2 agents + 3. Call `listAgents()` + 4. Verify both agents returned with correct IDs and states + 5. Delete one agent + 6. Call `listAgents()` again + 7. Verify only remaining agent returned + + **Acceptance criteria**: + - Test passes + - Agent list accurate after create/delete operations + - DaemonClient `listAgents()` method verified working + +- [ ] **Investigate**: Claude provider permissions don't work in daemon E2E tests. + + **Problem**: Claude permission tests are skipped because Claude SDK doesn't request permissions in daemon context. Direct `claude-agent.test.ts` tests pass, but daemon E2E tests don't. + + **Investigation**: + 1. Compare how permissions work in `claude-agent.test.ts` vs `daemon.e2e.test.ts` + 2. Check if daemon context affects Claude SDK permission handling + 3. Check if the issue is in DaemonClient, session setup, or Claude provider + 4. Add debug logging to trace permission request flow + + **Goal**: Unskip Claude permission tests and achieve parity with Codex. + + **Acceptance criteria**: + - Root cause identified and documented + - Claude permission tests pass (or clear explanation of why they can't)