From a85ed1d8007064b5f1758913b1fb66b4ba02506b Mon Sep 17 00:00:00 2001 From: 47th <161213233+flcrom@users.noreply.github.com> Date: Thu, 1 Oct 2026 20:35:16 +0530 Subject: [PATCH 1/2] fix: stop agent --wait polling after cancellation --- src/commands/agent.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/commands/agent.ts b/src/commands/agent.ts index 28832cdd41..24ccb9d4d5 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -352,6 +352,14 @@ export async function executeAgent( }; } + if (normalizedStatus === 'cancelled') { + spinner.succeed('Agent cancelled'); + return { + success: true, + data: toStatusData(jobId, agentStatus, normalizedStatus), + }; + } + // Check timeout if (timeoutMs && Date.now() - startTime > timeoutMs) { process.removeListener('SIGINT', handleInterrupt); From d497c5908fa5708b9da967c2b219cfc14f5b97ed Mon Sep 17 00:00:00 2001 From: 47th <161213233+flcrom@users.noreply.github.com> Date: Thu, 1 Oct 2026 20:36:07 +0530 Subject: [PATCH 2/2] test: cover agent --wait cancellation --- .../commands/agent-cancellation.test.ts | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 src/__tests__/commands/agent-cancellation.test.ts diff --git a/src/__tests__/commands/agent-cancellation.test.ts b/src/__tests__/commands/agent-cancellation.test.ts new file mode 100644 index 0000000000..76c23334cd --- /dev/null +++ b/src/__tests__/commands/agent-cancellation.test.ts @@ -0,0 +1,126 @@ +import { execFile } from 'node:child_process'; +import { createServer, type Server } from 'node:http'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { promisify } from 'node:util'; +import { afterAll, beforeAll, expect, it } from 'vitest'; + +const exec = promisify(execFile); +const JOB_ID = '019e5299-8235-7538-9f62-bc39d4b058f1'; +const home = mkdtempSync(join(tmpdir(), 'agent-cancellation-cli-')); +let server: Server; +let baseUrl: string; +let statuses: string[]; +let polls: number; + +beforeAll(async () => { + server = createServer((req, res) => { + res.writeHead(200, { 'content-type': 'application/json' }); + if (req.method === 'POST') { + res.end(JSON.stringify({ success: true, id: JOB_ID })); + return; + } + const status = statuses[Math.min(polls++, statuses.length - 1)]; + res.end( + JSON.stringify({ + success: true, + status, + data: { answer: 'result' }, + creditsUsed: 12, + threadId: 'thread-fixture', + error: status === 'failed' ? 'Agent reached max credits' : undefined, + }) + ); + }); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + baseUrl = `http://127.0.0.1:${(server.address() as { port: number }).port}`; +}); + +afterAll(async () => { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())) + ); + rmSync(home, { recursive: true, force: true }); +}); + +async function cli(sequence: string[], prompt = 'research example') { + statuses = sequence; + polls = 0; + try { + return { + code: 0, + ...(await exec( + process.execPath, + [ + 'dist/index.js', + 'agent', + prompt, + '--wait', + '--poll-interval', + '0.01', + '--timeout', + '1', + '--json', + ], + { + timeout: 10000, + env: { + ...process.env, + HOME: home, + USERPROFILE: home, + FIRECRAWL_API_KEY: 'fc-test', + FIRECRAWL_API_URL: baseUrl, + FIRECRAWL_NO_UPDATE_CHECK: '1', + }, + } + )), + }; + } catch (error) { + return error as { code: number; stdout: string; stderr: string }; + } +} + +it.each([['cancelled'], ['processing', 'cancelled']])( + 'stops prompt-start polling at cancellation (%j)', + async (...sequence) => { + const result = await cli(sequence); + expect(result.code).toBe(0); + expect(polls).toBe(sequence.length); + expect(JSON.parse(result.stdout)).toMatchObject({ + success: true, + id: JOB_ID, + status: 'cancelled', + creditsUsed: 12, + threadId: 'thread-fixture', + }); + expect(result.stderr).not.toContain('still processing'); + } +); + +it('keeps existing-job cancellation successful', async () => { + const result = await cli(['cancelled'], JOB_ID); + expect(result.code).toBe(0); + expect(polls).toBe(1); + expect(JSON.parse(result.stdout).status).toBe('cancelled'); +}); + +it('keeps processing-to-completed successful', async () => { + const result = await cli(['processing', 'completed']); + expect(result.code).toBe(0); + expect(polls).toBe(2); + expect(JSON.parse(result.stdout).data).toEqual({ answer: 'result' }); +}); + +it('keeps failed jobs unsuccessful', async () => { + const result = await cli(['failed']); + expect(result.code).toBe(1); + expect(polls).toBe(1); + expect(result.stderr).toContain('Agent reached max credits'); +}); + +it('still times out when the agent remains processing', async () => { + const result = await cli(['processing']); + expect(result.code).toBe(1); + expect(result.stderr).toContain('Agent still processing'); +});