diff --git a/apps/server/src/provider/providerMaintenanceRunner.test.ts b/apps/server/src/provider/providerMaintenanceRunner.test.ts index adf53c5267203..4da2a713ada4c 100644 --- a/apps/server/src/provider/providerMaintenanceRunner.test.ts +++ b/apps/server/src/provider/providerMaintenanceRunner.test.ts @@ -713,6 +713,65 @@ describe("providerMaintenanceRunner", () => { ), ); + it.effect("reports the updater's error rather than a trailing hint after it", () => + Effect.forEach( + [ + { + stdout: "Current version: 2.1.310\nChecking for updates to latest version...\n", + stderr: [ + "Error: Failed to install native update", + "TelemetrySafeError: Failed to fetch version from https://downloads.claude.ai/claude-code-releases/latest after 3 attempt(s): Request failed with status code 403", + 'Try running "claude doctor" for diagnostics', + "", + ].join("\n"), + reason: "Request failed with status code 403", + hint: "claude doctor", + }, + { + stdout: "", + stderr: [ + "Failed to check for updates", + "Unable to fetch latest version from npm registry", + "", + "Possible causes:", + " • Network connectivity issues", + " • npm registry is unreachable", + " • Corporate proxy/firewall blocking npm", + "", + "Try:", + " • Check your internet connection", + " • Run with --debug flag for more details", + " • Manually check: npm view @anthropic-ai/claude-code version", + " • Check if you need to login: npm whoami", + "", + ].join("\n"), + reason: "Failed to check for updates", + hint: "npm whoami", + }, + ], + ({ stdout, stderr, reason, hint }) => + Effect.gen(function* () { + const { registry } = yield* makeRegistry(baseNativeCliProvider); + const updater = yield* makeTestRunner(registry); + + const result = yield* updater.updateProvider(NATIVE_CLI_DRIVER); + const message = result.providers[0]?.updateState?.message ?? ""; + + assert.include(message, reason); + assert.notInclude(message, hint); + }).pipe( + Effect.provide( + Layer.mergeAll( + layerNonWindowsPlatform, + layerLatestVersionHttpClient("0.0.0"), + layerMockSpawner(() => ({ stdout, stderr, code: 1 })), + ), + ), + ), + { discard: true }, + ), + ); + it.effect("keeps the exit code message when a failed command prints nothing", () => Effect.gen(function* () { const { registry } = yield* makeRegistry(); diff --git a/apps/server/src/provider/providerMaintenanceRunner.ts b/apps/server/src/provider/providerMaintenanceRunner.ts index d3fe7fdf86739..0ec191a89a1b0 100644 --- a/apps/server/src/provider/providerMaintenanceRunner.ts +++ b/apps/server/src/provider/providerMaintenanceRunner.ts @@ -250,18 +250,20 @@ function commandOutput(result: ProviderMaintenanceCommandResult): string | null return truncateText(output, UPDATE_OUTPUT_MAX_BYTES); } -// Updaters print their reason last, usually on stderr. -function lastOutputLine(result: ProviderMaintenanceCommandResult): string | null { - for (const output of [result.stderr, result.stdout]) { - const line = output +// Updaters report on stderr, but may follow their error with hints (`claude update` +// ends with "Try running claude doctor"), so prefer the last line naming an error. +function failureReason(result: ProviderMaintenanceCommandResult): string | null { + const streams = [result.stderr, result.stdout].map((output) => + output .split(/\r\n|\r|\n/) .map(toProgressLine) - .findLast(Boolean); - if (line) { - return line; - } - } - return null; + .filter((line) => line !== null), + ); + return ( + streams.map((lines) => lines.findLast((line) => /error|fail/i.test(line))).find(Boolean) ?? + streams.map((lines) => lines.at(-1)).find(Boolean) ?? + null + ); } function failureMessage(result: ProviderMaintenanceCommandResult): string { @@ -272,7 +274,7 @@ function failureMessage(result: ProviderMaintenanceCommandResult): string { result.exitCode !== null && result.exitCode !== 0 ? `Update command exited with code ${result.exitCode}` : "Update command failed"; - const reason = lastOutputLine(result); + const reason = failureReason(result); return reason ? `${summary}: ${reason}` : `${summary}.`; }