From 2188ef7273f04f84fa0ce9ae1598374ac4841de2 Mon Sep 17 00:00:00 2001 From: 47th <161213233+flcrom@users.noreply.github.com> Date: Thu, 1 Oct 2026 20:22:56 +0530 Subject: [PATCH 1/4] fix: exit with error when agent job status is failed The plain `firecrawl agent ` check printed {"success":true,"status":"failed","data":null} and exited 0 for an already-failed job, dropping the server error. Match the --wait path: exit 1 with the server's error (or "Agent failed" when none is sent). Completed, processing and cancelled statuses are unchanged. Refs #115. --- src/commands/agent.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/commands/agent.ts b/src/commands/agent.ts index 28832cdd41..d2b9971d44 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -113,6 +113,14 @@ async function checkAgentStatus( const normalizedStatus = normalizeAgentStatus( status.status as AgentStatusFromApi ); + if (normalizedStatus === 'failed') { + return { + success: false, + data: toStatusData(jobId, status, normalizedStatus), + error: status.error ?? 'Agent failed', + }; + } + const isCancelled = normalizedStatus === 'cancelled'; return { From a68f2761fadecab28889e0598e32cf7cbe1e040b Mon Sep 17 00:00:00 2001 From: 47th <161213233+flcrom@users.noreply.github.com> Date: Thu, 1 Oct 2026 20:23:15 +0530 Subject: [PATCH 2/4] test: cover agent status check exit codes --- src/__tests__/commands/agent-status.test.ts | 111 ++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/__tests__/commands/agent-status.test.ts diff --git a/src/__tests__/commands/agent-status.test.ts b/src/__tests__/commands/agent-status.test.ts new file mode 100644 index 0000000000..17c07ebe58 --- /dev/null +++ b/src/__tests__/commands/agent-status.test.ts @@ -0,0 +1,111 @@ +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); +let server: Server; +let baseUrl: string; +let payload: Record = {}; +const home = mkdtempSync(join(tmpdir(), 'agent-status-cli-')); + +beforeAll(async () => { + server = createServer((req, res) => { + res.writeHead(200, { 'content-type': 'application/json' }); + res.end(JSON.stringify(payload)); + }); + 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(args: string[]) { + try { + return { + code: 0, + ...(await exec(process.execPath, ['dist/index.js', ...args], { + 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 }; + } +} + +const JOB_ID = '019e5299-8235-7538-9f62-bc39d4b058f1'; + +it('exits nonzero and surfaces the server error when checking a failed agent without --wait', async () => { + payload = { + success: true, + status: 'failed', + error: 'Refusal: Error: Agent reached max credits', + data: null, + }; + const result = await cli(['agent', JOB_ID, '--json']); + expect(result.code).toBe(1); + expect(result.stderr).toContain('Agent reached max credits'); +}); + +it('keeps success semantics for a completed agent status', async () => { + payload = { + success: true, + status: 'completed', + data: { answer: 'done' }, + creditsUsed: 12, + }; + const result = await cli(['agent', JOB_ID, '--json']); + expect(result.code).toBe(0); + const parsed = JSON.parse(result.stdout); + expect(parsed.success).toBe(true); + expect(parsed.status).toBe('completed'); + expect(parsed.data).toEqual({ answer: 'done' }); +}); + +it('keeps success semantics for a processing agent status', async () => { + payload = { success: true, status: 'processing' }; + const result = await cli(['agent', JOB_ID, '--json']); + expect(result.code).toBe(0); + const parsed = JSON.parse(result.stdout); + expect(parsed.success).toBe(true); + expect(parsed.status).toBe('processing'); +}); + +it('keeps success semantics for a cancelled agent status', async () => { + payload = { success: true, status: 'cancelled' }; + const result = await cli(['agent', JOB_ID, '--json']); + expect(result.code).toBe(0); + const parsed = JSON.parse(result.stdout); + expect(parsed.success).toBe(true); + expect(parsed.status).toBe('cancelled'); +}); + +it('keeps failure semantics when the server rejects the status request', async () => { + payload = { success: false, error: 'Unauthorized: Invalid API key' }; + const result = await cli(['agent', JOB_ID, '--json']); + expect(result.code).toBe(1); + expect(result.stderr).toContain('Error'); +}); + +it('exits nonzero even when a failed agent carries no error string', async () => { + payload = { success: true, status: 'failed', data: null }; + const result = await cli(['agent', JOB_ID, '--json']); + expect(result.code).toBe(1); + expect(result.stderr).toContain('Agent failed'); +}); From 2a3fdd6ae9e44d328b414834440abe0fbe8306d6 Mon Sep 17 00:00:00 2001 From: 47th <161213233+flcrom@users.noreply.github.com> Date: Thu, 1 Oct 2026 23:54:16 +0530 Subject: [PATCH 3/4] fix: show fallback for empty agent errors --- src/commands/agent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/agent.ts b/src/commands/agent.ts index d2b9971d44..a4fd1ed7a0 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -117,7 +117,7 @@ async function checkAgentStatus( return { success: false, data: toStatusData(jobId, status, normalizedStatus), - error: status.error ?? 'Agent failed', + error: status.error || 'Agent failed', }; } From 4746693f777497a3844f7285ee2a6524e78e3197 Mon Sep 17 00:00:00 2001 From: 47th <161213233+flcrom@users.noreply.github.com> Date: Thu, 1 Oct 2026 23:54:31 +0530 Subject: [PATCH 4/4] test: cover empty errors and HTTP status rejection --- src/__tests__/commands/agent-status.test.ts | 22 +++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/__tests__/commands/agent-status.test.ts b/src/__tests__/commands/agent-status.test.ts index 17c07ebe58..4d94fc5077 100644 --- a/src/__tests__/commands/agent-status.test.ts +++ b/src/__tests__/commands/agent-status.test.ts @@ -10,11 +10,12 @@ const exec = promisify(execFile); let server: Server; let baseUrl: string; let payload: Record = {}; +let responseStatus = 200; const home = mkdtempSync(join(tmpdir(), 'agent-status-cli-')); beforeAll(async () => { server = createServer((req, res) => { - res.writeHead(200, { 'content-type': 'application/json' }); + res.writeHead(responseStatus, { 'content-type': 'application/json' }); res.end(JSON.stringify(payload)); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); @@ -98,14 +99,19 @@ it('keeps success semantics for a cancelled agent status', async () => { it('keeps failure semantics when the server rejects the status request', async () => { payload = { success: false, error: 'Unauthorized: Invalid API key' }; + responseStatus = 401; const result = await cli(['agent', JOB_ID, '--json']); + responseStatus = 200; expect(result.code).toBe(1); - expect(result.stderr).toContain('Error'); + expect(result.stderr).toContain('Unauthorized: Invalid API key'); }); -it('exits nonzero even when a failed agent carries no error string', async () => { - payload = { success: true, status: 'failed', data: null }; - const result = await cli(['agent', JOB_ID, '--json']); - expect(result.code).toBe(1); - expect(result.stderr).toContain('Agent failed'); -}); +it.each([undefined, null, ''])( + 'uses the failure fallback for a missing or empty error (%j)', + async (error) => { + payload = { success: true, status: 'failed', data: null, error }; + const result = await cli(['agent', JOB_ID, '--json']); + expect(result.code).toBe(1); + expect(result.stderr).toContain('Agent failed'); + } +);