From 0e30738f28008709b58bdf9b6fa6b93fa221d6ff Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 30 Sep 2026 02:19:19 -0700 Subject: [PATCH 1/2] fix(sim-cli): report an embedded SimApiError with its own exit code The embedded renderer returned 1 for every SimApiError, so an operation wait that timed out (exit 4) or needs configuration (exit 3) read as a generic failure in-process while the installed CLI reported the real status. Return error.exitCode, as the terminal renderer does. --- packages/sim-cli/src/embed.test.ts | 19 +++++++++++++++++++ packages/sim-cli/src/embed.ts | 6 +++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/sim-cli/src/embed.test.ts b/packages/sim-cli/src/embed.test.ts index a81c9549909..24fde421c8b 100644 --- a/packages/sim-cli/src/embed.test.ts +++ b/packages/sim-cli/src/embed.test.ts @@ -97,6 +97,25 @@ describe('runEmbeddedCli', () => { expect(JSON.parse(a.stdout).data[0].id).toBe(wsA) expect(JSON.parse(b.stdout).data[0].id).toBe(wsB) }) + + it('reports a thrown error with its own exit code, as the installed CLI does', async () => { + const receipt = { + operationId: 'op-1', + requestId: 'req-1', + workspaceId: IDENTITY.workspaceId, + kind: 'workflow_import', + applied: true, + status: 'processing', + issues: [], + } + const result = await runEmbeddedCli( + ['--output', 'json', 'workspaces', 'operations', 'wait', 'op-1', '--wait-timeout', '0.01'], + { ...IDENTITY, transport: async () => jsonResponse({ data: receipt }) } + ) + expect(result.exitCode).toBe(4) + expect(JSON.parse(result.stdout)).toMatchObject({ operationId: 'op-1', status: 'processing' }) + expect(result.stderr).toContain('OPERATION_WAIT_TIMEOUT') + }) }) describe('embedded artifact destinations', () => { diff --git a/packages/sim-cli/src/embed.ts b/packages/sim-cli/src/embed.ts index b517aa06f8b..7c8d8335abb 100644 --- a/packages/sim-cli/src/embed.ts +++ b/packages/sim-cli/src/embed.ts @@ -70,8 +70,8 @@ export function createEmbeddedClient(identity: EmbeddedCliIdentity): SimClient { /** * Runs one CLI invocation in-process. `argv` is the token list exactly as the * terminal would receive it (no leading node/binary tokens). Errors the - * installed CLI would print-and-exit-1 on come back the same way: rendered to - * stderr, exitCode 1 — never thrown. + * installed CLI would print-and-exit on come back the same way: rendered to + * stderr with the same exit code — never thrown. */ export async function runEmbeddedCli( argv: string[], @@ -191,7 +191,7 @@ function renderEmbeddedError(ctx: EmbedContext, error: unknown): number { if (error.details !== undefined) { for (const line of formatApiErrorDetails(error.details)) ctx.stderr.diagnostic(sanitize(line)) } - return 1 + return error.exitCode } ctx.stderr.diagnostic( // utils-lint-allow: this published standalone CLI cannot import the private @sim/utils package. From 1634d0c0cc31c5e5edc9ee6fde9b377ed7884293 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 30 Sep 2026 03:24:13 -0700 Subject: [PATCH 2/2] test(sim-cli): make the embedded wait-timeout test independent of runner speed The test gave the wait 10 ms of real time, so a slow runner could reach the deadline before the first status check and exit 4 without printing the receipt. The clock now advances only when the stubbed status is served, so the wait times out only after it has a receipt to print. --- packages/sim-cli/src/embed.test.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/sim-cli/src/embed.test.ts b/packages/sim-cli/src/embed.test.ts index 24fde421c8b..3234b73c57d 100644 --- a/packages/sim-cli/src/embed.test.ts +++ b/packages/sim-cli/src/embed.test.ts @@ -108,9 +108,19 @@ describe('runEmbeddedCli', () => { status: 'processing', issues: [], } + // The clock moves only when a status is served, so the wait can time out + // only after it has a receipt to print, however slow the runner is. + let now = Date.now() + vi.spyOn(Date, 'now').mockImplementation(() => now) const result = await runEmbeddedCli( - ['--output', 'json', 'workspaces', 'operations', 'wait', 'op-1', '--wait-timeout', '0.01'], - { ...IDENTITY, transport: async () => jsonResponse({ data: receipt }) } + ['--output', 'json', 'workspaces', 'operations', 'wait', 'op-1', '--wait-timeout', '60'], + { + ...IDENTITY, + transport: async () => { + now += 61_000 + return jsonResponse({ data: receipt }) + }, + } ) expect(result.exitCode).toBe(4) expect(JSON.parse(result.stdout)).toMatchObject({ operationId: 'op-1', status: 'processing' })