From a2cfa7027ed8c972d14e1cc7f96ee7763985de83 Mon Sep 17 00:00:00 2001 From: Stefano Fiorini Date: Mon, 18 May 2026 18:11:45 -0500 Subject: [PATCH] feat(M3): Direct Execution --- tools/ai-cli-dispatch/src/execute.ts | 16 +++++++++++++--- tools/ai-cli-dispatch/tests/execute.test.ts | 20 ++++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/tools/ai-cli-dispatch/src/execute.ts b/tools/ai-cli-dispatch/src/execute.ts index b0f9359..a9c5b83 100644 --- a/tools/ai-cli-dispatch/src/execute.ts +++ b/tools/ai-cli-dispatch/src/execute.ts @@ -1,4 +1,6 @@ +import type { ChildProcess } from "node:child_process"; import { spawn as defaultSpawn } from "node:child_process"; +import type { PathLike } from "node:fs"; import { existsSync as defaultExistsSync } from "node:fs"; import type { ClientName, ExecResult } from "./types.js"; import { ClientNotFoundError, ExecError } from "./types.js"; @@ -12,8 +14,8 @@ const CLIENT_ARGS: Record string[]> = { export interface ExecuteOptions { clientPath?: string; timeoutMs?: number; - spawn?: typeof defaultSpawn; - existsSync?: typeof defaultExistsSync; + spawn?: (command: string, args: string[], options?: { shell?: boolean }) => ChildProcess; + existsSync?: (path: PathLike) => boolean; } export async function executePrompt( @@ -38,7 +40,15 @@ export async function executePrompt( throw new ClientNotFoundError(client); } - const args = CLIENT_ARGS[client](prompt); + const argBuilder = (CLIENT_ARGS as Record string[]>)[client]; + if (!argBuilder) { + throw new ExecError(`Unknown client: ${client}`, { + stdout: "", + stderr: "", + exitCode: -1, + }); + } + const args = argBuilder(prompt); return new Promise((resolve, reject) => { let settled = false; diff --git a/tools/ai-cli-dispatch/tests/execute.test.ts b/tools/ai-cli-dispatch/tests/execute.test.ts index 6f3a6d1..eca32d3 100644 --- a/tools/ai-cli-dispatch/tests/execute.test.ts +++ b/tools/ai-cli-dispatch/tests/execute.test.ts @@ -4,6 +4,7 @@ import { EventEmitter } from "node:events"; import { Readable } from "node:stream"; import { executePrompt } from "../src/execute.js"; import { ClientNotFoundError, ExecError } from "../src/types.js"; +import type { ClientName } from "../src/types.js"; interface MockScenario { stdout?: string; @@ -14,7 +15,7 @@ interface MockScenario { } function createMockChildProcess(scenario: MockScenario): any { - const child = new EventEmitter(); + const child = new EventEmitter() as any; child.stdout = Readable.from( scenario.stdout !== undefined ? [scenario.stdout] : [] ); @@ -61,8 +62,8 @@ function createMockChildProcess(scenario: MockScenario): any { function mockSpawn( scenarios: Map -): (cmd: string, args: string[], opts: any) => any { - return (cmd: string, args: string[], _opts: any) => { +): any { + return (cmd: string, args: string[], _opts: any): any => { const key = [cmd, ...args].join(" "); const scenario = scenarios.get(key); if (!scenario) { @@ -74,7 +75,7 @@ function mockSpawn( }; } -function mockExistsSync(allowedPaths: Set): (p: string) => boolean { +function mockExistsSync(allowedPaths: Set): any { return (p: string) => allowedPaths.has(p); } @@ -191,4 +192,15 @@ describe("executePrompt", () => { err instanceof ExecError && err.message.includes("empty") ); }); + + it("throws ExecError for invalid client at runtime", async () => { + await assert.rejects( + executePrompt("bogus" as unknown as ClientName, "hello", { + spawn: mockSpawn(new Map()), + existsSync: () => true, + }), + (err: unknown) => + err instanceof ExecError && err.message.includes("Unknown client") + ); + }); });