Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,17 @@ describe('CLI argument parsing', () => {
});
});

describe('CLI process lifecycle', () => {
it('does not force-exit after emitting output so piped JSON can flush', async () => {
const source = await Bun.file(new URL('./cli.ts', import.meta.url)).text();

expect(source).toContain('process.exitCode = exitCode;');
expect(source).toContain('process.exitCode = 1;');
expect(source).not.toContain('process.exit(exitCode);');
expect(source).not.toContain('process.exit(1);');
});
});

describe('Conversation ID generation', () => {
// Test the generateConversationId pattern
const generateConversationId = (): string => {
Expand Down
7 changes: 4 additions & 3 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1026,13 +1026,14 @@ async function main(): Promise<number> {
}
}

// Run main and exit with the returned code
// Set the result, then allow Bun to complete pending stdout work naturally.
// Calling process.exit() can terminate an in-flight pipe write.
main()
.then(exitCode => {
process.exit(exitCode);
process.exitCode = exitCode;
})
.catch((error: unknown) => {
const err = error as Error;
console.error('Fatal error:', err.message);
process.exit(1);
process.exitCode = 1;
});
50 changes: 47 additions & 3 deletions packages/cli/src/commands/workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2098,13 +2098,20 @@ describe('workflowRunCommand', () => {

describe('workflowStatusCommand', () => {
let consoleSpy: ReturnType<typeof spyOn>;
let stdoutSpy: ReturnType<typeof spyOn>;

beforeEach(() => {
consoleSpy = spyOn(console, 'log').mockImplementation(() => {});
stdoutSpy = spyOn(process.stdout, 'write').mockImplementation((...args: unknown[]) => {
const callback = args.find(arg => typeof arg === 'function');
if (typeof callback === 'function') callback();
return true;
});
});

afterEach(() => {
consoleSpy.mockRestore();
stdoutSpy.mockRestore();
});

it('should print message when no active runs', async () => {
Expand Down Expand Up @@ -2143,7 +2150,37 @@ describe('workflowStatusCommand', () => {

await workflowStatusCommand(true);

expect(consoleSpy).toHaveBeenCalledWith(JSON.stringify({ runs: [] }, null, 2));
expect(stdoutSpy).toHaveBeenCalledWith(
`${JSON.stringify({ runs: [] }, null, 2)}\n`,
expect.any(Function)
);
});

it('waits for the stdout write callback before completing JSON output', async () => {
const workflowDb = await import('@archon/core/db/workflows');
(workflowDb.listWorkflowRuns as ReturnType<typeof mock>).mockResolvedValueOnce([]);
let completeWrite: (() => void) | undefined;
let settled = false;

stdoutSpy.mockImplementation((...args: unknown[]) => {
const callback = args.find(arg => typeof arg === 'function');
if (typeof callback === 'function') completeWrite = callback;
return false;
});

const command = workflowStatusCommand(true).then(() => {
settled = true;
});
for (let attempt = 0; attempt < 10 && !completeWrite; attempt += 1) {
await Promise.resolve();
}

expect(settled).toBe(false);
expect(completeWrite).toBeDefined();

completeWrite?.();
await command;
expect(settled).toBe(true);
});

it('should show node summaries in verbose mode', async () => {
Expand Down Expand Up @@ -2284,7 +2321,7 @@ describe('workflowStatusCommand', () => {

await workflowStatusCommand(true, true);

const jsonOutput = consoleSpy.mock.calls[0]?.[0] as string;
const jsonOutput = stdoutSpy.mock.calls[0]?.[0] as string;
const parsed = JSON.parse(jsonOutput) as { runs: Array<{ events: unknown[] }> };
expect(parsed.runs[0].events).toHaveLength(1);
});
Expand All @@ -2302,13 +2339,20 @@ const EMPTY_COUNTS = {

describe('workflowGetCommand', () => {
let consoleSpy: ReturnType<typeof spyOn>;
let stdoutSpy: ReturnType<typeof spyOn>;

beforeEach(() => {
consoleSpy = spyOn(console, 'log').mockImplementation(() => {});
stdoutSpy = spyOn(process.stdout, 'write').mockImplementation((...args: unknown[]) => {
const callback = args.find(arg => typeof arg === 'function');
if (typeof callback === 'function') callback();
return true;
});
});

afterEach(() => {
consoleSpy.mockRestore();
stdoutSpy.mockRestore();
});

it('prints not-found (human) and exits non-zero for a missing run', async () => {
Expand Down Expand Up @@ -2477,7 +2521,7 @@ describe('workflowGetCommand', () => {

await workflowGetCommand('run-v', true, true);

const parsed = JSON.parse(consoleSpy.mock.calls[0][0] as string) as { events: unknown[] };
const parsed = JSON.parse(stdoutSpy.mock.calls[0][0] as string) as { events: unknown[] };
expect(Array.isArray(parsed.events)).toBe(true);
expect(parsed.events).toHaveLength(1);
});
Expand Down
21 changes: 19 additions & 2 deletions packages/cli/src/commands/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,19 @@ function getLog(): ReturnType<typeof createLogger> {
return cachedLog;
}

/** Wait for large JSON payloads to reach stdout instead of relying on Bun's console buffer. */
function writeJsonToStdout(value: unknown): Promise<void> {
return new Promise((resolve, reject) => {
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`, error => {
if (error) {
reject(error);
return;
}
resolve();
});
});
}

/**
* Options for workflow run command
*
Expand Down Expand Up @@ -1986,7 +1999,7 @@ export async function workflowStatusCommand(json?: boolean, verbose?: boolean):
);
runsOutput = runs.map((run, i) => ({ ...run, events: eventsPerRun[i] }));
}
console.log(JSON.stringify({ runs: runsOutput }, null, 2));
await writeJsonToStdout({ runs: runsOutput });
return;
}

Expand Down Expand Up @@ -2071,7 +2084,11 @@ export async function workflowGetCommand(

if (json) {
const output = verbose ? { ...run, events: events ?? [] } : run;
console.log(JSON.stringify(output, null, 2));
if (verbose) {
await writeJsonToStdout(output);
} else {
console.log(JSON.stringify(output, null, 2));
}
return 0;
}

Expand Down
Loading