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:
co-authored by
Claude Opus 4.5
parent
6ba5c466a1
commit
5ce0a742e7
@@ -7,6 +7,7 @@ import {
|
|||||||
type DaemonTestContext,
|
type DaemonTestContext,
|
||||||
} from "./test-utils/index.js";
|
} from "./test-utils/index.js";
|
||||||
import type { AgentTimelineItem } from "./agent/agent-sdk-types.js";
|
import type { AgentTimelineItem } from "./agent/agent-sdk-types.js";
|
||||||
|
import type { AgentSnapshotPayload } from "./messages.js";
|
||||||
|
|
||||||
function tmpCwd(): string {
|
function tmpCwd(): string {
|
||||||
return mkdtempSync(path.join(tmpdir(), "daemon-e2e-"));
|
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:
|
// Claude permission tests are skipped due to SDK behavior:
|
||||||
// - The sandbox config IS passed correctly to Claude SDK
|
// - The sandbox config IS passed correctly to Claude SDK
|
||||||
// - Claude executes tool calls without requesting permission
|
// - Claude executes tool calls without requesting permission
|
||||||
|
|||||||
@@ -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
|
- 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.
|
- **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.
|
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
|
- Test passes
|
||||||
- Agent stops within reasonable time after cancel
|
- Agent stops within reasonable time after cancel
|
||||||
- DaemonClient `cancelAgent()` method verified working
|
- 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()`.
|
- [ ] **Test**: Add daemon E2E test for `setAgentMode()`.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user