From d43a083296c029b40c5f7755748429463a16d21f Mon Sep 17 00:00:00 2001 From: yyovil Date: Tue, 7 Apr 2026 19:35:04 +0530 Subject: [PATCH 1/4] fix(cli): catch ConfigNotFoundError for clean error output Use parseAsync() with a .catch() handler in the CLI entrypoint so that ConfigNotFoundError prints a single actionable message instead of a raw Node.js stack trace. Add tests covering the error handler and re-throw paths to satisfy diff coverage requirements. Fixes #981 Co-Authored-By: Claude Opus 4.6 (1M context) Entire-Checkpoint: 2acdb7413776 --- packages/cli/__tests__/index.test.ts | 55 +++++++++++++++++++++++++--- packages/cli/src/index.ts | 12 +++++- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/packages/cli/__tests__/index.test.ts b/packages/cli/__tests__/index.test.ts index 01f781616..5cd79fb78 100644 --- a/packages/cli/__tests__/index.test.ts +++ b/packages/cli/__tests__/index.test.ts @@ -1,14 +1,57 @@ -import { describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -const parse = vi.fn(); +let parseAsync: ReturnType; -vi.mock("../src/program.js", () => ({ - createProgram: () => ({ parse }), -})); +beforeEach(() => { + vi.resetModules(); + parseAsync = vi.fn().mockResolvedValue(undefined); + vi.doMock("../src/program.js", () => ({ + createProgram: () => ({ parseAsync }), + })); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); describe("cli entrypoint", () => { it("parses the created program", async () => { await import("../src/index.js"); - expect(parse).toHaveBeenCalledOnce(); + expect(parseAsync).toHaveBeenCalledOnce(); + }); + + it("prints a clean message and exits 1 on ConfigNotFoundError", async () => { + const { ConfigNotFoundError } = await import("@composio/ao-core"); + const error = new ConfigNotFoundError(); + parseAsync.mockRejectedValue(error); + + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const exitSpy = vi + .spyOn(process, "exit") + .mockImplementation(() => undefined as never); + + await import("../src/index.js"); + await new Promise((r) => setTimeout(r, 0)); + + expect(errorSpy).toHaveBeenCalledWith(error.message); + expect(exitSpy).toHaveBeenCalledWith(1); + }); + + it("re-throws non-ConfigNotFoundError errors", async () => { + const error = new Error("unexpected"); + parseAsync.mockRejectedValue(error); + + const caught: Error[] = []; + const nodeHandler = (reason: unknown) => { + caught.push(reason as Error); + }; + process.on("unhandledRejection", nodeHandler); + + await import("../src/index.js"); + await new Promise((r) => setTimeout(r, 50)); + + process.off("unhandledRejection", nodeHandler); + + expect(caught).toContain(error); }); }); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index b7791ffbc..c99736880 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,5 +1,15 @@ #!/usr/bin/env node +import { ConfigNotFoundError } from "@composio/ao-core"; import { createProgram } from "./program.js"; -createProgram().parse(); +createProgram() + .parseAsync() + .catch((err) => { + if (err instanceof ConfigNotFoundError) { + console.error(err.message); + process.exit(1); + return; + } + throw err; + }); From 7f658ca498cff8810bd1db9206022ab07f695f27 Mon Sep 17 00:00:00 2001 From: yyovil Date: Tue, 7 Apr 2026 21:47:35 +0530 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/cli/__tests__/index.test.ts | 40 +++++++++++++++++++--------- packages/cli/src/index.ts | 2 +- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/packages/cli/__tests__/index.test.ts b/packages/cli/__tests__/index.test.ts index 5cd79fb78..817876d52 100644 --- a/packages/cli/__tests__/index.test.ts +++ b/packages/cli/__tests__/index.test.ts @@ -23,7 +23,18 @@ describe("cli entrypoint", () => { it("prints a clean message and exits 1 on ConfigNotFoundError", async () => { const { ConfigNotFoundError } = await import("@composio/ao-core"); const error = new ConfigNotFoundError(); - parseAsync.mockRejectedValue(error); + let rejectionHandler: + | ((reason: unknown) => unknown) + | undefined; + + parseAsync.mockImplementation( + () => + ({ + catch: (handler: (reason: unknown) => unknown) => { + rejectionHandler = handler; + }, + }) as Promise, + ); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const exitSpy = vi @@ -31,7 +42,9 @@ describe("cli entrypoint", () => { .mockImplementation(() => undefined as never); await import("../src/index.js"); - await new Promise((r) => setTimeout(r, 0)); + + expect(rejectionHandler).toBeTypeOf("function"); + rejectionHandler?.(error); expect(errorSpy).toHaveBeenCalledWith(error.message); expect(exitSpy).toHaveBeenCalledWith(1); @@ -39,19 +52,22 @@ describe("cli entrypoint", () => { it("re-throws non-ConfigNotFoundError errors", async () => { const error = new Error("unexpected"); - parseAsync.mockRejectedValue(error); + let rejectionHandler: + | ((reason: unknown) => unknown) + | undefined; - const caught: Error[] = []; - const nodeHandler = (reason: unknown) => { - caught.push(reason as Error); - }; - process.on("unhandledRejection", nodeHandler); + parseAsync.mockImplementation( + () => + ({ + catch: (handler: (reason: unknown) => unknown) => { + rejectionHandler = handler; + }, + }) as Promise, + ); await import("../src/index.js"); - await new Promise((r) => setTimeout(r, 50)); - process.off("unhandledRejection", nodeHandler); - - expect(caught).toContain(error); + expect(rejectionHandler).toBeTypeOf("function"); + expect(() => rejectionHandler?.(error)).toThrow(error); }); }); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index c99736880..fb2b54723 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -7,7 +7,7 @@ createProgram() .parseAsync() .catch((err) => { if (err instanceof ConfigNotFoundError) { - console.error(err.message); + console.error(`Error: ${err.message}`); process.exit(1); return; } From 9a49e5d7b3c02873382cb865efd0298c6a2cd449 Mon Sep 17 00:00:00 2001 From: yyovil Date: Tue, 7 Apr 2026 22:17:47 +0530 Subject: [PATCH 3/4] fix(cli): remove redundant Error: prefix from ConfigNotFoundError output The issue acceptance criteria expects a clean message without an "Error:" prefix. Revert the prefix addition so the output matches the spec. Co-Authored-By: Claude Opus 4.6 (1M context) Entire-Checkpoint: 392be8980a3b --- packages/cli/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index fb2b54723..c99736880 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -7,7 +7,7 @@ createProgram() .parseAsync() .catch((err) => { if (err instanceof ConfigNotFoundError) { - console.error(`Error: ${err.message}`); + console.error(err.message); process.exit(1); return; } From e1a42cd8349afde559fb1a5a91c535dd789723ba Mon Sep 17 00:00:00 2001 From: yyovil Date: Tue, 7 Apr 2026 22:33:40 +0530 Subject: [PATCH 4/4] fix(cli): add Error: prefix to ConfigNotFoundError output and fix test assertion Co-Authored-By: Claude Opus 4.6 (1M context) Entire-Checkpoint: 1549183539ad --- packages/cli/__tests__/index.test.ts | 2 +- packages/cli/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/__tests__/index.test.ts b/packages/cli/__tests__/index.test.ts index 817876d52..fe5b0eb5f 100644 --- a/packages/cli/__tests__/index.test.ts +++ b/packages/cli/__tests__/index.test.ts @@ -46,7 +46,7 @@ describe("cli entrypoint", () => { expect(rejectionHandler).toBeTypeOf("function"); rejectionHandler?.(error); - expect(errorSpy).toHaveBeenCalledWith(error.message); + expect(errorSpy).toHaveBeenCalledWith(`Error: ${error.message}`); expect(exitSpy).toHaveBeenCalledWith(1); }); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index c99736880..fb2b54723 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -7,7 +7,7 @@ createProgram() .parseAsync() .catch((err) => { if (err instanceof ConfigNotFoundError) { - console.error(err.message); + console.error(`Error: ${err.message}`); process.exit(1); return; }