Add timestamp behavior E2E tests and verify bug is fixed

WHAT:
- Added initializeAgent() and clearAgentAttention() methods to DaemonClient
  (packages/server/src/server/test-utils/daemon-client.ts:268-304)
- Added E2E tests for timestamp behavior
  (packages/server/src/server/daemon.e2e.test.ts:482-570):
  - "opening agent without interaction does not update timestamp"
  - "sending message DOES update timestamp"

RESULT:
Bug verified as already fixed in commit 32e111e (Dec 2, 2025) which removed
timestamp thrashing - previously lastActivityAt/updatedAt was updated on
every agent_stream event (15+ times/second during streaming).

Server only sets agent.updatedAt in:
- recordUserMessage (agent-manager.ts:436)
- handleStreamEvent (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 in 9.08s)
- Playwright test showed agent stayed in position 4 with unchanged timestamp
  after clicking

🤖 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 17:42:33 +07:00
co-authored by Claude Opus 4.5
parent 3459354e45
commit 6ba5c466a1
3 changed files with 198 additions and 1 deletions
@@ -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
@@ -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<AgentSnapshotPayload> {
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<void> {
this.send({
type: "clear_agent_attention",
agentId,
});
}
// ============================================================================
// Agent Interaction
// ============================================================================