merge M3 into implement/2026-05-19-ai-cli-dispatch-fixes

This commit is contained in:
2026-05-19 22:22:54 -05:00
5 changed files with 1663 additions and 100 deletions
+6 -5
View File
@@ -28,12 +28,13 @@ npm install
```bash ```bash
scripts/ai-cli-dispatch list --json scripts/ai-cli-dispatch list --json
scripts/ai-cli-dispatch exec --client codex --prompt "refactor this function" scripts/ai-cli-dispatch run --client codex --prompt "refactor this function"
scripts/ai-cli-dispatch exec --client claude --prompt "add tests for auth middleware" scripts/ai-cli-dispatch run --client claude --prompt "add tests for auth middleware"
scripts/ai-cli-dispatch exec --client opencode --prompt "migrate to ESM" scripts/ai-cli-dispatch run --client opencode --prompt "migrate to ESM"
scripts/ai-cli-dispatch dispatch "use codex to write tests"
``` ```
Use `--json` for machine-readable command output. Use `--json` for machine-readable command output. Use `--sync` with `run` or `dispatch` to block until the client returns; the default is async (starts a background job and returns the job ID immediately).
## Client Discovery ## Client Discovery
@@ -47,7 +48,7 @@ Run `list` to see which clients are installed and their resolved versions.
## Background Jobs ## Background Jobs
For long-running or fire-and-forget tasks, use the programmatic job API instead of `exec`: For long-running or fire-and-forget tasks, use the programmatic job API or invoke `run` / `dispatch` without `--sync`:
```typescript ```typescript
import { startJob, getJob, cancelJob, listJobs, cleanupJobs } from "./src/jobs.js"; import { startJob, getJob, cancelJob, listJobs, cleanupJobs } from "./src/jobs.js";
+85
View File
@@ -0,0 +1,85 @@
import type { ClientName, ExecResult, DebugInfo, Job } from "./types.js";
export interface RunContext {
jsonMode: boolean;
stdoutWrite: (chunk: string) => void;
stderrWrite: (chunk: string) => void;
}
export function reportError(err: unknown, jsonMode: boolean): number {
const message = err instanceof Error ? err.message : String(err);
if (jsonMode) {
console.error(JSON.stringify({ error: message }, null, 2));
} else {
console.error(message);
}
return 1;
}
export function reportCliError(message: string, jsonMode: boolean): number {
if (jsonMode) {
console.error(JSON.stringify({ error: message }, null, 2));
} else {
console.error(`Error: ${message}`);
}
return 1;
}
export async function handleSyncRun(
executePrompt: (
client: ClientName,
prompt: string,
options?: { timeoutMs?: number; debug?: boolean; onDebug?: (info: DebugInfo) => void }
) => Promise<ExecResult>,
client: ClientName,
prompt: string,
timeoutMs: number | undefined,
debug: boolean,
ctx: RunContext
): Promise<number> {
try {
const result = await executePrompt(client, prompt, {
timeoutMs,
debug,
onDebug: debug ? (info) => ctx.stderrWrite(JSON.stringify(info) + "\n") : undefined,
});
if (ctx.jsonMode) {
console.log(JSON.stringify(result, null, 2));
} else {
if (result.stdout) ctx.stdoutWrite(result.stdout);
if (result.stderr) ctx.stderrWrite(result.stderr);
}
return 0;
} catch (err) {
return reportError(err, ctx.jsonMode);
}
}
export async function handleAsyncRun(
startJob: (
client: ClientName,
prompt: string,
options?: { timeoutMs?: number; debug?: boolean; onDebug?: (info: DebugInfo) => void }
) => Promise<Job>,
client: ClientName,
prompt: string,
timeoutMs: number | undefined,
debug: boolean,
ctx: RunContext
): Promise<number> {
try {
const job = await startJob(client, prompt, {
timeoutMs,
debug,
onDebug: debug ? (info) => ctx.stderrWrite(JSON.stringify(info) + "\n") : undefined,
});
if (ctx.jsonMode) {
console.log(JSON.stringify({ jobId: job.id, client: job.client, status: job.status }, null, 2));
} else {
console.log(`Job ${job.id} started (${job.client}): ${job.status}`);
}
return 0;
} catch (err) {
return reportError(err, ctx.jsonMode);
}
}
+221 -69
View File
@@ -1,6 +1,14 @@
import minimist from "minimist"; import minimist from "minimist";
import { detectClients as realDetectClients } from "./detect.js"; import { detectClients as realDetectClients } from "./detect.js";
import { executePrompt as realExecutePrompt } from "./execute.js"; import { executePrompt as realExecutePrompt } from "./execute.js";
import {
startJob as realStartJob,
getJob as realGetJob,
getJobResult as realGetJobResult,
cancelJob as realCancelJob,
listJobs as realListJobs,
cleanupJobs as realCleanupJobs,
} from "./jobs.js";
import { resolveClient as realResolveClient } from "./dispatch.js"; import { resolveClient as realResolveClient } from "./dispatch.js";
import { resolveConfig as realResolveConfig } from "./config.js"; import { resolveConfig as realResolveConfig } from "./config.js";
import { CLIENT_NAMES } from "./constants.js"; import { CLIENT_NAMES } from "./constants.js";
@@ -9,8 +17,16 @@ import {
type ClientInfo, type ClientInfo,
type ExecResult, type ExecResult,
type DebugInfo, type DebugInfo,
type Job,
type JobStatus,
ClientNotFoundError, ClientNotFoundError,
} from "./types.js"; } from "./types.js";
import {
reportError,
reportCliError,
handleSyncRun,
handleAsyncRun,
} from "./cli-helpers.js";
export interface CliDeps { export interface CliDeps {
detectClients?: () => ClientInfo[]; detectClients?: () => ClientInfo[];
@@ -19,6 +35,11 @@ export interface CliDeps {
prompt: string, prompt: string,
options?: { timeoutMs?: number; debug?: boolean; onDebug?: (info: DebugInfo) => void } options?: { timeoutMs?: number; debug?: boolean; onDebug?: (info: DebugInfo) => void }
) => Promise<ExecResult>; ) => Promise<ExecResult>;
startJob?: (
client: ClientName,
prompt: string,
options?: { timeoutMs?: number; debug?: boolean; onDebug?: (info: DebugInfo) => void }
) => Promise<Job>;
resolveClient?: ( resolveClient?: (
prompt: string, prompt: string,
config?: { client?: ClientName; defaultClient?: ClientName } config?: { client?: ClientName; defaultClient?: ClientName }
@@ -26,6 +47,29 @@ export interface CliDeps {
resolveConfig?: () => { paths: Partial<Record<ClientName, string>>; defaultClient?: ClientName; timeout?: number }; resolveConfig?: () => { paths: Partial<Record<ClientName, string>>; defaultClient?: ClientName; timeout?: number };
stdoutWrite?: (chunk: string) => void; stdoutWrite?: (chunk: string) => void;
stderrWrite?: (chunk: string) => void; stderrWrite?: (chunk: string) => void;
getJob?: (jobId: string) => Job;
getJobResult?: (jobId: string) => ExecResult;
cancelJob?: (jobId: string) => void;
listJobs?: (options?: { filter?: JobStatus }) => Job[];
cleanupJobs?: (options?: { maxAgeMs?: number }) => void;
}
function parseMaxAge(value: string): number | null {
const match = value.match(/^(\d+(?:\.\d+)?)\s*([hmsd]?)$/i);
if (!match) return null;
const num = parseFloat(match[1]);
if (!Number.isFinite(num) || num < 0) return null;
const unit = match[2].toLowerCase();
const multipliers: Record<string, number> = {
s: 1000,
m: 60 * 1000,
h: 60 * 60 * 1000,
d: 24 * 60 * 60 * 1000,
"": 60 * 60 * 1000,
};
const multiplier = multipliers[unit];
if (multiplier === undefined) return null;
return num * multiplier;
} }
function printHelp(): void { function printHelp(): void {
@@ -33,10 +77,25 @@ function printHelp(): void {
Usage: Usage:
ai-cli-dispatch list [--json|--text] ai-cli-dispatch list [--json|--text]
ai-cli-dispatch run --client <client> --prompt <prompt> [--json|--text] ai-cli-dispatch run --client <client> --prompt <prompt> [--sync] [--timeout <ms>] [--debug] [--json|--text]
ai-cli-dispatch dispatch <prompt> [--client <client>] [--json|--text] ai-cli-dispatch dispatch <prompt> [--client <client>] [--sync] [--timeout <ms>] [--debug] [--json|--text]
ai-cli-dispatch start --client <client> --prompt <prompt> [--timeout <ms>] [--debug] [--json|--text]
ai-cli-dispatch status <job-id> [--json|--text]
ai-cli-dispatch results <job-id> [--json|--text]
ai-cli-dispatch cancel <job-id> [--json|--text]
ai-cli-dispatch list-jobs [--status running|completed|failed] [--json|--text]
ai-cli-dispatch cleanup-jobs [--max-age <number>[h|m|s|d]] [--json|--text]
ai-cli-dispatch --help ai-cli-dispatch --help
Flags:
--sync Run synchronously and block until the client returns (default is async)
--timeout Timeout in milliseconds (or override via config)
--debug Emit diagnostic JSON to stderr
--max-age Maximum age for cleanup (default unit: hours, e.g. 24h or 30m)
--status Filter jobs by status (running, completed, failed)
--json Output JSON (default)
--text Output plain text
Clients: codex, claude, opencode`); Clients: codex, claude, opencode`);
} }
@@ -46,18 +105,24 @@ export async function main(
): Promise<number> { ): Promise<number> {
const detectClients = deps.detectClients ?? realDetectClients; const detectClients = deps.detectClients ?? realDetectClients;
const executePrompt = deps.executePrompt ?? realExecutePrompt; const executePrompt = deps.executePrompt ?? realExecutePrompt;
const startJob = deps.startJob ?? realStartJob;
const resolveClient = deps.resolveClient ?? realResolveClient; const resolveClient = deps.resolveClient ?? realResolveClient;
const resolveConfig = deps.resolveConfig ?? realResolveConfig; const resolveConfig = deps.resolveConfig ?? realResolveConfig;
const stdoutWrite = deps.stdoutWrite ?? ((c: string) => process.stdout.write(c)); const stdoutWrite = deps.stdoutWrite ?? ((c: string) => process.stdout.write(c));
const stderrWrite = deps.stderrWrite ?? ((c: string) => process.stderr.write(c)); const stderrWrite = deps.stderrWrite ?? ((c: string) => process.stderr.write(c));
const getJob = deps.getJob ?? realGetJob;
const getJobResult = deps.getJobResult ?? realGetJobResult;
const cancelJob = deps.cancelJob ?? realCancelJob;
const listJobs = deps.listJobs ?? realListJobs;
const cleanupJobs = deps.cleanupJobs ?? realCleanupJobs;
const rawArgs = argv.slice(2); const rawArgs = argv.slice(2);
const parseArgs = const parseArgs =
rawArgs[0]?.includes("cli.ts") ? rawArgs.slice(1) : rawArgs; rawArgs[0]?.includes("cli.ts") ? rawArgs.slice(1) : rawArgs;
const args = minimist(parseArgs, { const args = minimist(parseArgs, {
string: ["client", "prompt", "timeout"], string: ["client", "prompt", "timeout", "max-age"],
boolean: ["json", "text", "help", "debug"], boolean: ["json", "text", "help", "debug", "sync"],
alias: { h: "help" }, alias: { h: "help" },
}); });
@@ -96,25 +161,14 @@ export async function main(
const prompt = args.prompt as string | undefined; const prompt = args.prompt as string | undefined;
if (!client || !CLIENT_NAMES.includes(client)) { if (!client || !CLIENT_NAMES.includes(client)) {
const message = !client return reportCliError(
? "--client is required" !client ? "--client is required" : `Unknown client: ${client}`,
: `Unknown client: ${client}`; jsonMode
if (jsonMode) { );
console.error(JSON.stringify({ error: message }, null, 2));
} else {
console.error(`Error: ${message}`);
}
return 1;
} }
if (!prompt) { if (!prompt) {
const message = "--prompt is required"; return reportCliError("--prompt is required", jsonMode);
if (jsonMode) {
console.error(JSON.stringify({ error: message }, null, 2));
} else {
console.error(`Error: ${message}`);
}
return 1;
} }
const config = resolveConfig(); const config = resolveConfig();
@@ -123,28 +177,11 @@ export async function main(
const timeoutMs = const timeoutMs =
Number.isFinite(parsedTimeout) ? parsedTimeout : config.timeout; Number.isFinite(parsedTimeout) ? parsedTimeout : config.timeout;
try { const ctx = { jsonMode, stdoutWrite, stderrWrite };
const result = await executePrompt(client, prompt, { if (args.sync) {
timeoutMs, return await handleSyncRun(executePrompt, client, prompt, timeoutMs, debug, ctx);
debug,
onDebug: debug ? (info) => stderrWrite(JSON.stringify(info) + "\n") : undefined,
});
if (jsonMode) {
console.log(JSON.stringify(result, null, 2));
} else {
if (result.stdout) stdoutWrite(result.stdout);
if (result.stderr) stderrWrite(result.stderr);
}
return 0;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (jsonMode) {
console.error(JSON.stringify({ error: message }, null, 2));
} else {
console.error(message);
}
return 1;
} }
return await handleAsyncRun(startJob, client, prompt, timeoutMs, debug, ctx);
} }
if (command === "dispatch") { if (command === "dispatch") {
@@ -154,13 +191,7 @@ export async function main(
} }
if (!prompt) { if (!prompt) {
const message = "prompt is required"; return reportCliError("prompt is required", jsonMode);
if (jsonMode) {
console.error(JSON.stringify({ error: message }, null, 2));
} else {
console.error(`Error: ${message}`);
}
return 1;
} }
const config = resolveConfig(); const config = resolveConfig();
@@ -171,26 +202,86 @@ export async function main(
}); });
if (!client) { if (!client) {
const message = "Could not resolve client from prompt"; return reportCliError("Could not resolve client from prompt", jsonMode);
if (jsonMode) {
console.error(JSON.stringify({ error: message }, null, 2));
} else {
console.error(`Error: ${message}`);
}
return 1;
} }
const parsedTimeout =
typeof args.timeout === "string" ? Number(args.timeout) : undefined;
const timeoutMs =
Number.isFinite(parsedTimeout) ? parsedTimeout : config.timeout;
const ctx = { jsonMode, stdoutWrite, stderrWrite };
if (args.sync) {
return await handleSyncRun(executePrompt, client, prompt, timeoutMs, debug, ctx);
}
return await handleAsyncRun(startJob, client, prompt, timeoutMs, debug, ctx);
}
if (command === "start") {
const client = args.client as ClientName | undefined;
const prompt = args.prompt as string | undefined;
if (!client || !CLIENT_NAMES.includes(client)) {
return reportCliError(
!client ? "--client is required" : `Unknown client: ${client}`,
jsonMode
);
}
if (!prompt) {
return reportCliError("--prompt is required", jsonMode);
}
const config = resolveConfig();
const parsedTimeout = const parsedTimeout =
typeof args.timeout === "string" ? Number(args.timeout) : undefined; typeof args.timeout === "string" ? Number(args.timeout) : undefined;
const timeoutMs = const timeoutMs =
Number.isFinite(parsedTimeout) ? parsedTimeout : config.timeout; Number.isFinite(parsedTimeout) ? parsedTimeout : config.timeout;
try { try {
const result = await executePrompt(client, prompt, { const job = await startJob(client, prompt, {
timeoutMs, timeoutMs,
debug, debug,
onDebug: debug ? (info) => stderrWrite(JSON.stringify(info) + "\n") : undefined, onDebug: debug ? (info) => stderrWrite(JSON.stringify(info) + "\n") : undefined,
}); });
if (jsonMode) {
console.log(JSON.stringify({ jobId: job.id, client: job.client, status: job.status }, null, 2));
} else {
console.log(`Job ${job.id} started (${job.client}): ${job.status}`);
}
return 0;
} catch (err) {
return reportError(err, jsonMode);
}
}
if (command === "status") {
const jobId = args._[1];
if (!jobId) {
return reportCliError("job-id is required", jsonMode);
}
try {
const job = getJob(jobId);
if (jsonMode) {
console.log(JSON.stringify(job, null, 2));
} else {
console.log(`Job ${jobId}: ${job.status}`);
}
return 0;
} catch (err) {
return reportError(err, jsonMode);
}
}
if (command === "results") {
const jobId = args._[1];
if (!jobId) {
return reportCliError("job-id is required", jsonMode);
}
try {
const result = getJobResult(jobId);
if (jsonMode) { if (jsonMode) {
console.log(JSON.stringify(result, null, 2)); console.log(JSON.stringify(result, null, 2));
} else { } else {
@@ -199,23 +290,84 @@ export async function main(
} }
return 0; return 0;
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : String(err); return reportError(err, jsonMode);
if (jsonMode) {
console.error(JSON.stringify({ error: message }, null, 2));
} else {
console.error(message);
}
return 1;
} }
} }
const message = `Unknown command: ${command}`; if (command === "cancel") {
if (jsonMode) { const jobId = args._[1];
console.error(JSON.stringify({ error: message }, null, 2)); if (!jobId) {
} else { return reportCliError("job-id is required", jsonMode);
console.error(message); }
try {
const job = getJob(jobId);
if (job.status !== "running") {
return reportCliError(
`Job is not running (status: ${job.status})`,
jsonMode
);
}
cancelJob(jobId);
if (jsonMode) {
console.log(JSON.stringify({ jobId, cancelled: true }, null, 2));
} else {
console.log(`Job ${jobId} cancelled`);
}
return 0;
} catch (err) {
return reportError(err, jsonMode);
}
} }
return 1;
if (command === "list-jobs") {
try {
const filter = args.status as JobStatus | undefined;
const jobs = listJobs({ filter });
if (jsonMode) {
console.log(JSON.stringify(jobs, null, 2));
} else {
for (const job of jobs) {
console.log(`${job.id} (${job.client}): ${job.status}`);
}
}
return 0;
} catch (err) {
return reportError(err, jsonMode);
}
}
if (command === "cleanup-jobs") {
const maxAgeRaw = args["max-age"] as string | undefined;
let maxAgeMs: number | undefined;
if (maxAgeRaw !== undefined) {
const parsed = parseMaxAge(maxAgeRaw);
if (parsed === null) {
return reportCliError(
"Invalid --max-age format. Use: <number>[h|m|s|d], e.g. 24h",
jsonMode
);
}
maxAgeMs = parsed;
}
try {
const jobsBefore = listJobs();
cleanupJobs({ maxAgeMs });
const jobsAfter = listJobs();
const count = jobsBefore.length - jobsAfter.length;
if (jsonMode) {
console.log(JSON.stringify({ count }, null, 2));
} else {
console.log(`Cleaned ${count} jobs`);
}
return 0;
} catch (err) {
return reportError(err, jsonMode);
}
}
return reportError(`Unknown command: ${command}`, jsonMode);
} }
const isMain = const isMain =
@@ -0,0 +1,278 @@
import { describe, it } from "node:test";
import assert from "node:assert";
import {
reportError,
reportCliError,
handleSyncRun,
handleAsyncRun,
} from "../src/cli-helpers.js";
import type { ExecResult, Job } from "../src/types.js";
function captureConsole() {
const logs: string[] = [];
const errors: string[] = [];
const origLog = console.log;
const origError = console.error;
console.log = (...args: unknown[]) => logs.push(args.map(String).join(" "));
console.error = (...args: unknown[]) => errors.push(args.map(String).join(" "));
return {
logs,
errors,
restore() {
console.log = origLog;
console.error = origError;
},
};
}
describe("reportError", () => {
it("prints JSON error for Error instance when jsonMode=true", () => {
const out = captureConsole();
try {
const code = reportError(new Error("boom"), true);
assert.strictEqual(code, 1);
assert.strictEqual(out.errors.length, 1);
const parsed = JSON.parse(out.errors[0]);
assert.strictEqual(parsed.error, "boom");
} finally {
out.restore();
}
});
it("prints plain text error for Error instance when jsonMode=false", () => {
const out = captureConsole();
try {
const code = reportError(new Error("boom"), false);
assert.strictEqual(code, 1);
assert.strictEqual(out.errors.length, 1);
assert.strictEqual(out.errors[0], "boom");
} finally {
out.restore();
}
});
it("prints JSON error for non-Error value when jsonMode=true", () => {
const out = captureConsole();
try {
const code = reportError("plain string", true);
assert.strictEqual(code, 1);
const parsed = JSON.parse(out.errors[0]);
assert.strictEqual(parsed.error, "plain string");
} finally {
out.restore();
}
});
});
describe("reportCliError", () => {
it("prints JSON error with jsonMode=true", () => {
const out = captureConsole();
try {
const code = reportCliError("missing arg", true);
assert.strictEqual(code, 1);
const parsed = JSON.parse(out.errors[0]);
assert.strictEqual(parsed.error, "missing arg");
} finally {
out.restore();
}
});
it("prints prefixed text error with jsonMode=false", () => {
const out = captureConsole();
try {
const code = reportCliError("missing arg", false);
assert.strictEqual(code, 1);
assert.strictEqual(out.errors[0], "Error: missing arg");
} finally {
out.restore();
}
});
});
describe("handleSyncRun", () => {
it("returns 0 and prints JSON result in jsonMode", async () => {
const out = captureConsole();
try {
const result: ExecResult = {
stdout: "out",
stderr: "err",
exitCode: 0,
client: "codex",
durationMs: 10,
};
const code = await handleSyncRun(
async () => result,
"codex",
"hello",
5000,
false,
{ jsonMode: true, stdoutWrite: () => {}, stderrWrite: () => {} }
);
assert.strictEqual(code, 0);
assert.strictEqual(out.logs.length, 1);
const parsed = JSON.parse(out.logs[0]);
assert.strictEqual(parsed.stdout, "out");
} finally {
out.restore();
}
});
it("returns 0 and writes stdout/stderr in text mode", async () => {
const out = captureConsole();
const stdout: string[] = [];
const stderr: string[] = [];
try {
const result: ExecResult = {
stdout: "out",
stderr: "err",
exitCode: 0,
client: "codex",
durationMs: 10,
};
const code = await handleSyncRun(
async () => result,
"codex",
"hello",
5000,
false,
{
jsonMode: false,
stdoutWrite: (c) => stdout.push(c),
stderrWrite: (c) => stderr.push(c),
}
);
assert.strictEqual(code, 0);
assert.strictEqual(stdout.join(""), "out");
assert.strictEqual(stderr.join(""), "err");
assert.strictEqual(out.logs.length, 0);
} finally {
out.restore();
}
});
it("passes timeoutMs and debug to executePrompt", async () => {
const out = captureConsole();
try {
let received: any;
const code = await handleSyncRun(
async (_c, _p, opts) => {
received = opts;
return {
stdout: "",
stderr: "",
exitCode: 0,
client: "codex",
durationMs: 1,
};
},
"codex",
"hello",
12345,
true,
{ jsonMode: true, stdoutWrite: () => {}, stderrWrite: () => {} }
);
assert.strictEqual(code, 0);
assert.strictEqual(received?.timeoutMs, 12345);
assert.strictEqual(received?.debug, true);
} finally {
out.restore();
}
});
it("returns 1 and prints error JSON when executePrompt throws", async () => {
const out = captureConsole();
try {
const code = await handleSyncRun(
async () => {
throw new Error("fail");
},
"codex",
"hello",
undefined,
false,
{ jsonMode: true, stdoutWrite: () => {}, stderrWrite: () => {} }
);
assert.strictEqual(code, 1);
const parsed = JSON.parse(out.errors[0]);
assert.strictEqual(parsed.error, "fail");
} finally {
out.restore();
}
});
});
describe("handleAsyncRun", () => {
it("returns 0 and prints JSON job in jsonMode", async () => {
const out = captureConsole();
try {
const job: Job = {
id: "j1",
client: "codex",
prompt: "hi",
status: "running",
startedAt: "2024-01-01T00:00:00Z",
};
const code = await handleAsyncRun(
async () => job,
"codex",
"hi",
undefined,
false,
{ jsonMode: true, stdoutWrite: () => {}, stderrWrite: () => {} }
);
assert.strictEqual(code, 0);
const parsed = JSON.parse(out.logs[0]);
assert.strictEqual(parsed.jobId, "j1");
assert.strictEqual(parsed.status, "running");
} finally {
out.restore();
}
});
it("returns 0 and prints text job info", async () => {
const out = captureConsole();
try {
const job: Job = {
id: "j1",
client: "codex",
prompt: "hi",
status: "running",
startedAt: "2024-01-01T00:00:00Z",
};
const code = await handleAsyncRun(
async () => job,
"codex",
"hi",
undefined,
false,
{ jsonMode: false, stdoutWrite: () => {}, stderrWrite: () => {} }
);
assert.strictEqual(code, 0);
assert.ok(out.logs[0].includes("j1"));
assert.ok(out.logs[0].includes("running"));
} finally {
out.restore();
}
});
it("returns 1 and prints error JSON when startJob throws", async () => {
const out = captureConsole();
try {
const code = await handleAsyncRun(
async () => {
throw new Error("fail");
},
"codex",
"hi",
undefined,
false,
{ jsonMode: true, stdoutWrite: () => {}, stderrWrite: () => {} }
);
assert.strictEqual(code, 1);
const parsed = JSON.parse(out.errors[0]);
assert.strictEqual(parsed.error, "fail");
} finally {
out.restore();
}
});
});
File diff suppressed because it is too large Load Diff