mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Add daemon E2E test for cancelAgent()
Test verifies: - Agent cancel request is processed correctly - Agent reaches idle/error state within 2 seconds - No zombie processes left after cancel 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
type DaemonTestContext,
|
||||
} from "./test-utils/index.js";
|
||||
import type { AgentTimelineItem } from "./agent/agent-sdk-types.js";
|
||||
import type { AgentSnapshotPayload } from "./messages.js";
|
||||
|
||||
function tmpCwd(): string {
|
||||
return mkdtempSync(path.join(tmpdir(), "daemon-e2e-"));
|
||||
@@ -570,6 +571,143 @@ describe("daemon E2E", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("cancelAgent", () => {
|
||||
test(
|
||||
"cancels a running agent mid-execution",
|
||||
async () => {
|
||||
const cwd = tmpCwd();
|
||||
|
||||
// Create Codex agent
|
||||
const agent = await ctx.client.createAgent({
|
||||
provider: "codex",
|
||||
cwd,
|
||||
title: "Cancel Test Agent",
|
||||
});
|
||||
|
||||
expect(agent.id).toBeTruthy();
|
||||
expect(agent.status).toBe("idle");
|
||||
|
||||
// Clear message queue before sending prompt
|
||||
ctx.client.clearMessageQueue();
|
||||
|
||||
// Send a prompt that triggers a long-running operation
|
||||
await ctx.client.sendMessage(agent.id, "Run: sleep 30");
|
||||
|
||||
// Wait for the agent to start running (tool call starts)
|
||||
let sawRunning = false;
|
||||
const startPosition = ctx.client.getMessageQueue().length;
|
||||
|
||||
// Wait for running state
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error("Timeout waiting for agent to start running"));
|
||||
}, 30000);
|
||||
|
||||
const checkForRunning = (): 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) {
|
||||
if (msg.payload.status === "running") {
|
||||
sawRunning = true;
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Check periodically
|
||||
const interval = setInterval(checkForRunning, 50);
|
||||
const cleanup = (): void => {
|
||||
clearInterval(interval);
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
|
||||
// Override reject to cleanup
|
||||
const originalReject = reject;
|
||||
reject = (err): void => {
|
||||
cleanup();
|
||||
originalReject(err);
|
||||
};
|
||||
});
|
||||
|
||||
expect(sawRunning).toBe(true);
|
||||
|
||||
// Record timestamp before cancel
|
||||
const cancelStart = Date.now();
|
||||
|
||||
// Cancel the agent
|
||||
await ctx.client.cancelAgent(agent.id);
|
||||
|
||||
// Wait for agent to reach idle or error state
|
||||
const finalState = await new Promise<AgentSnapshotPayload>(
|
||||
(resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(
|
||||
new Error(
|
||||
"Timeout waiting for agent to stop after cancel (>2 seconds)"
|
||||
)
|
||||
);
|
||||
}, 5000); // Give extra margin, but test should complete in 2s
|
||||
|
||||
const queueStart = ctx.client.getMessageQueue().length;
|
||||
const checkForStopped = (): void => {
|
||||
const queue = ctx.client.getMessageQueue();
|
||||
for (let i = queueStart; i < queue.length; i++) {
|
||||
const msg = queue[i];
|
||||
if (msg.type === "agent_state" && msg.payload.id === agent.id) {
|
||||
if (
|
||||
msg.payload.status === "idle" ||
|
||||
msg.payload.status === "error"
|
||||
) {
|
||||
clearTimeout(timeout);
|
||||
clearInterval(interval);
|
||||
resolve(msg.payload);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(checkForStopped, 50);
|
||||
}
|
||||
);
|
||||
|
||||
// Calculate how long the cancel took
|
||||
const cancelDuration = Date.now() - cancelStart;
|
||||
|
||||
// Verify agent stopped within reasonable time (2 seconds)
|
||||
expect(cancelDuration).toBeLessThan(2000);
|
||||
|
||||
// Verify agent is now idle or error
|
||||
expect(["idle", "error"]).toContain(finalState.status);
|
||||
|
||||
// Verify no zombie sleep processes left (check for sleep 30)
|
||||
const { execSync } = await import("child_process");
|
||||
try {
|
||||
const result = execSync("pgrep -f 'sleep 30'", {
|
||||
encoding: "utf8",
|
||||
timeout: 2000,
|
||||
});
|
||||
// If pgrep succeeds, there are zombie processes
|
||||
if (result.trim()) {
|
||||
// Kill them and fail the test
|
||||
execSync("pkill -f 'sleep 30'");
|
||||
expect.fail("Found zombie sleep processes after cancel");
|
||||
}
|
||||
} catch {
|
||||
// pgrep returns non-zero when no processes found - this is expected
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
},
|
||||
60000
|
||||
);
|
||||
});
|
||||
|
||||
// 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
|
||||
|
||||
3
plan.md
3
plan.md
@@ -1082,7 +1082,7 @@ Build a new Codex MCP provider side‑by‑side with the existing Codex SDK prov
|
||||
- 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()`.
|
||||
- [x] **Test**: Add daemon E2E test for `cancelAgent()`.
|
||||
|
||||
Cancel an agent mid-execution and verify it stops properly.
|
||||
|
||||
@@ -1098,6 +1098,7 @@ Build a new Codex MCP provider side‑by‑side with the existing Codex SDK prov
|
||||
- Test passes
|
||||
- Agent stops within reasonable time after cancel
|
||||
- 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()`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user