From fea8bb7f5ba9655ddce7d93d31cb438341e49ec3 Mon Sep 17 00:00:00 2001 From: Wooseong Kim <2222333+innocarpe@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:20:30 +0900 Subject: [PATCH] fix(cli): preserve runtime recovery steps (#24800) Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> --- src/cli/cli-error.ts | 12 ++++- src/cli/format-recovery.test.ts | 86 +++++++++++++++++++++++++++++++-- 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/src/cli/cli-error.ts b/src/cli/cli-error.ts index b626e60d8fa..cbd37f3e9fa 100644 --- a/src/cli/cli-error.ts +++ b/src/cli/cli-error.ts @@ -76,7 +76,7 @@ export function formatCliError(error: unknown, context: CliErrorContext = {}): s if (hasOrchestrationRequestId(error.data)) { return message } - return `${message}\nOrca is not running. Run 'orca open' first.` + return formatRuntimeUnavailableError(message, error.data) } // Match before generic recovery; flattened errors may retain only the conflict token. const conflict = automationOwnerConflictRecovery(matchAutomationOwnerConflict(error)) @@ -103,7 +103,7 @@ export function formatCliError(error: unknown, context: CliErrorContext = {}): s error instanceof RuntimeRpcFailureError && error.response.error.code === 'runtime_unavailable' ) { - return `${message}\nOrca is not running. Run 'orca open' first.` + return formatRuntimeUnavailableError(message, error.response.error.data) } if (error instanceof RuntimeRpcFailureError) { return formatMessageWithNextSteps(message, nextStepsFromData(error.response.error.data)) @@ -119,6 +119,14 @@ function hasOrchestrationRequestId(data: unknown): boolean { ) } +function formatRuntimeUnavailableError(message: string, data: unknown): string { + const nextSteps = nextStepsFromData(data) + if (nextSteps.length > 0) { + return formatMessageWithNextSteps(message, nextSteps) + } + return `${message}\nOrca is not running. Run 'orca open' first.` +} + export function reportCliError(error: unknown, json: boolean, context: CliErrorContext = {}): void { const selector = contextRecovery(errorCode(error), context) if (json) { diff --git a/src/cli/format-recovery.test.ts b/src/cli/format-recovery.test.ts index c78e62a9bcd..976314de57b 100644 --- a/src/cli/format-recovery.test.ts +++ b/src/cli/format-recovery.test.ts @@ -252,17 +252,97 @@ describe('CLI error recovery', () => { log.mockRestore() }) + it('preserves structured recovery for a local runtime-unavailable error', () => { + const error = new RuntimeClientError( + 'runtime_unavailable', + 'The local runtime has a recovery step.', + { nextSteps: ['Run `orca runtime status --json` and retry.'] } + ) + + expect(formatCliError(error)).toBe( + [ + 'The local runtime has a recovery step.', + 'Next step: Run `orca runtime status --json` and retry.' + ].join('\n') + ) + }) + + it('preserves structured recovery for an RPC runtime-unavailable error', () => { + const error = new RuntimeRpcFailureError({ + id: 'req_runtime_recovery', + ok: false, + error: { + code: 'runtime_unavailable', + message: 'The runtime returned a recovery step.', + data: { nextSteps: ['Reconnect the runtime, then retry.'] } + }, + _meta: { runtimeId: 'runtime_local' } + }) + + expect(formatCliError(error)).toBe( + [ + 'The runtime returned a recovery step.', + 'Next step: Reconnect the runtime, then retry.' + ].join('\n') + ) + }) + + it('keeps startup guidance when a local runtime-unavailable error has no next steps', () => { + const message = 'Could not connect to the running Orca app.' + + expect(formatCliError(new RuntimeClientError('runtime_unavailable', message))).toBe( + `${message}\nOrca is not running. Run 'orca open' first.` + ) + }) + + it('keeps startup guidance when an RPC runtime-unavailable error has no next steps', () => { + const message = 'The RPC runtime is unavailable.' + const error = new RuntimeRpcFailureError({ + id: 'req_rpc_startup_fallback', + ok: false, + error: { + code: 'runtime_unavailable', + message + }, + _meta: { runtimeId: 'runtime_local' } + }) + + expect(formatCliError(error)).toBe(`${message}\nOrca is not running. Run 'orca open' first.`) + }) + + it('preserves the local request-id protection for an RPC runtime-unavailable error', () => { + const error = new RuntimeRpcFailureError({ + id: 'req_rpc_runtime_mutation', + ok: false, + error: { + code: 'runtime_unavailable', + message: 'Re-issue the same RPC command with --retry-request mutation_2.', + data: { + orchestrationRequestId: 'mutation_2', + nextSteps: ['Read the RPC mutation status before retrying.'] + } + }, + _meta: { runtimeId: 'runtime_local' } + }) + + expect(formatCliError(error)).toBe( + 'Re-issue the same RPC command with --retry-request mutation_2.' + ) + }) + it('does not replace mutation recovery with generic runtime startup advice', () => { const error = new RuntimeClientError( 'runtime_unavailable', 'Re-issue the same command with --retry-request mutation_1.', - { orchestrationRequestId: 'mutation_1' } + { + orchestrationRequestId: 'mutation_1', + nextSteps: ['Read the mutation request status before retrying.'] + } ) const output = formatCliError(error) - expect(output).toContain('--retry-request mutation_1') - expect(output).not.toContain('orca open') + expect(output).toBe('Re-issue the same command with --retry-request mutation_1.') }) })