mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Add daemon E2E test for listAgents()
- Add E2E test that creates two agents, verifies both are returned by listAgents(), deletes one, and verifies only the remaining agent is returned - Update listAgents() in DaemonClient to compute current agent list from session_state, agent_state, and agent_deleted messages in the queue - Fix createAgent() to use skipQueueBefore option so second agent creation doesn't match stale messages from first agent 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -820,6 +820,86 @@ describe("daemon E2E", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("listAgents", () => {
|
||||
test(
|
||||
"returns current agents and reflects create/delete operations",
|
||||
async () => {
|
||||
const cwd1 = tmpCwd();
|
||||
const cwd2 = tmpCwd();
|
||||
|
||||
// Initially, there should be no agents (fresh session)
|
||||
const initialAgents = ctx.client.listAgents();
|
||||
expect(initialAgents).toHaveLength(0);
|
||||
|
||||
// Create first agent
|
||||
const agent1 = await ctx.client.createAgent({
|
||||
provider: "codex",
|
||||
cwd: cwd1,
|
||||
title: "List Test Agent 1",
|
||||
});
|
||||
|
||||
expect(agent1.id).toBeTruthy();
|
||||
expect(agent1.status).toBe("idle");
|
||||
|
||||
// listAgents should now return 1 agent
|
||||
const afterFirst = ctx.client.listAgents();
|
||||
expect(afterFirst).toHaveLength(1);
|
||||
expect(afterFirst[0].id).toBe(agent1.id);
|
||||
// Title may or may not be set depending on timing
|
||||
expect(afterFirst[0].cwd).toBe(cwd1);
|
||||
|
||||
// Create second agent
|
||||
const agent2 = await ctx.client.createAgent({
|
||||
provider: "codex",
|
||||
cwd: cwd2,
|
||||
title: "List Test Agent 2",
|
||||
});
|
||||
|
||||
expect(agent2.id).toBeTruthy();
|
||||
expect(agent2.status).toBe("idle");
|
||||
|
||||
// listAgents should now return 2 agents
|
||||
const afterSecond = ctx.client.listAgents();
|
||||
expect(afterSecond).toHaveLength(2);
|
||||
|
||||
// Verify both agents are present with correct IDs and states
|
||||
const ids = afterSecond.map((a) => a.id);
|
||||
expect(ids).toContain(agent1.id);
|
||||
expect(ids).toContain(agent2.id);
|
||||
|
||||
const agent1State = afterSecond.find((a) => a.id === agent1.id);
|
||||
const agent2State = afterSecond.find((a) => a.id === agent2.id);
|
||||
|
||||
// Title may or may not be set depending on timing
|
||||
expect(agent1State?.cwd).toBe(cwd1);
|
||||
expect(agent1State?.status).toBe("idle");
|
||||
|
||||
// Title may or may not be set depending on timing
|
||||
expect(agent2State?.cwd).toBe(cwd2);
|
||||
expect(agent2State?.status).toBe("idle");
|
||||
|
||||
// Delete first agent
|
||||
await ctx.client.deleteAgent(agent1.id);
|
||||
|
||||
// listAgents should now return only 1 agent
|
||||
const afterDelete = ctx.client.listAgents();
|
||||
expect(afterDelete).toHaveLength(1);
|
||||
expect(afterDelete[0].id).toBe(agent2.id);
|
||||
expect(afterDelete[0].cwd).toBe(cwd2);
|
||||
|
||||
// Verify agent1 is no longer in the list
|
||||
const deletedAgent = afterDelete.find((a) => a.id === agent1.id);
|
||||
expect(deletedAgent).toBeUndefined();
|
||||
|
||||
// Cleanup
|
||||
await ctx.client.deleteAgent(agent2.id);
|
||||
rmSync(cwd1, { recursive: true, force: true });
|
||||
rmSync(cwd2, { recursive: true, force: true });
|
||||
},
|
||||
60000 // 1 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
|
||||
|
||||
@@ -142,6 +142,10 @@ export class DaemonClient {
|
||||
|
||||
async createAgent(options: CreateAgentOptions): Promise<AgentSnapshotPayload> {
|
||||
const requestId = nanoid();
|
||||
|
||||
// Record the current queue position so we only check NEW messages
|
||||
const startPosition = this.messageQueue.length;
|
||||
|
||||
this.send({
|
||||
type: "create_agent_request",
|
||||
requestId,
|
||||
@@ -157,31 +161,39 @@ export class DaemonClient {
|
||||
initialPrompt: options.initialPrompt,
|
||||
});
|
||||
|
||||
// First get the agent ID from the initial state
|
||||
// First get the agent ID from the initial state (only check new messages)
|
||||
let agentId: string | null = null;
|
||||
await this.waitFor((msg) => {
|
||||
if (msg.type === "agent_state") {
|
||||
agentId = msg.payload.id;
|
||||
return msg.payload;
|
||||
}
|
||||
return null;
|
||||
}, 10000);
|
||||
await this.waitFor(
|
||||
(msg) => {
|
||||
if (msg.type === "agent_state") {
|
||||
agentId = msg.payload.id;
|
||||
return msg.payload;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
10000,
|
||||
{ skipQueueBefore: startPosition }
|
||||
);
|
||||
|
||||
if (!agentId) {
|
||||
throw new Error("Failed to get agent ID from create response");
|
||||
}
|
||||
|
||||
// Wait for the agent to be idle
|
||||
return this.waitFor((msg) => {
|
||||
if (
|
||||
msg.type === "agent_state" &&
|
||||
msg.payload.id === agentId &&
|
||||
msg.payload.status === "idle"
|
||||
) {
|
||||
return msg.payload;
|
||||
}
|
||||
return null;
|
||||
}, 60000); // 60 second timeout for initialization
|
||||
// Wait for the agent to be idle (only check new messages from startPosition)
|
||||
return this.waitFor(
|
||||
(msg) => {
|
||||
if (
|
||||
msg.type === "agent_state" &&
|
||||
msg.payload.id === agentId &&
|
||||
msg.payload.status === "idle"
|
||||
) {
|
||||
return msg.payload;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
60000,
|
||||
{ skipQueueBefore: startPosition }
|
||||
); // 60 second timeout for initialization
|
||||
}
|
||||
|
||||
async deleteAgent(agentId: string): Promise<void> {
|
||||
@@ -194,14 +206,36 @@ export class DaemonClient {
|
||||
});
|
||||
}
|
||||
|
||||
async listAgents(): Promise<AgentSnapshotPayload[]> {
|
||||
// session_state is sent on connection, or we can wait for it
|
||||
return this.waitFor((msg) => {
|
||||
/**
|
||||
* Returns the current list of agents by analyzing the message queue.
|
||||
* This computes the latest state by:
|
||||
* 1. Starting with agents from session_state (if any)
|
||||
* 2. Updating with agent_state messages
|
||||
* 3. Removing agents that have agent_deleted events
|
||||
*/
|
||||
listAgents(): AgentSnapshotPayload[] {
|
||||
const agentMap = new Map<string, AgentSnapshotPayload>();
|
||||
const deletedAgents = new Set<string>();
|
||||
|
||||
for (const msg of this.messageQueue) {
|
||||
if (msg.type === "session_state") {
|
||||
return msg.payload.agents;
|
||||
// Initial agents from session state
|
||||
for (const agent of msg.payload.agents) {
|
||||
agentMap.set(agent.id, agent);
|
||||
}
|
||||
} else if (msg.type === "agent_state") {
|
||||
// Update or add agent from state event
|
||||
agentMap.set(msg.payload.id, msg.payload);
|
||||
} else if (msg.type === "agent_deleted") {
|
||||
// Mark agent as deleted
|
||||
deletedAgents.add(msg.payload.agentId);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
// Filter out deleted agents and return
|
||||
return Array.from(agentMap.values()).filter(
|
||||
(agent) => !deletedAgents.has(agent.id)
|
||||
);
|
||||
}
|
||||
|
||||
async listPersistedAgents(): Promise<PersistedAgentDescriptorPayload[]> {
|
||||
|
||||
3
plan.md
3
plan.md
@@ -1117,7 +1117,7 @@ Build a new Codex MCP provider side‑by‑side with the existing Codex SDK prov
|
||||
- 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()`.
|
||||
- [x] **Test**: Add daemon E2E test for `listAgents()`.
|
||||
|
||||
Verify session state returns current agents.
|
||||
|
||||
@@ -1134,6 +1134,7 @@ Build a new Codex MCP provider side‑by‑side with the existing Codex SDK prov
|
||||
- Test passes
|
||||
- Agent list accurate after create/delete operations
|
||||
- DaemonClient `listAgents()` method verified working
|
||||
- **Done (2025-12-25 17:53)**: WHAT: Added E2E test in `packages/server/src/server/daemon.e2e.test.ts:823-901` that creates two agents, calls `listAgents()` to verify both appear, deletes one, and verifies only the remaining agent is returned. Also updated `listAgents()` in `packages/server/src/server/test-utils/daemon-client.ts:197-227` to compute current agent list by processing `session_state`, `agent_state`, and `agent_deleted` messages. FIXED BUG: `packages/server/src/server/test-utils/daemon-client.ts:143-197` - `createAgent()` was not using `skipQueueBefore` option, causing second agent creation to match stale messages from the first agent. Fix: track queue position before sending request and only check new messages. RESULT: Test passes - listAgents correctly reflects agents after create/delete operations. EVIDENCE: `npm run test --workspace=@paseo/server -- daemon.e2e.test.ts -t "listAgents"` (1 passed, 11 skipped in 254ms).
|
||||
|
||||
- [ ] **Investigate**: Claude provider permissions don't work in daemon E2E tests.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user