diff --git a/src/commands/internal/dev/npm-link-or-shim.ts b/src/commands/internal/dev/npm-link-or-shim.ts index 9a34768e610..3726292cedf 100644 --- a/src/commands/internal/dev/npm-link-or-shim.ts +++ b/src/commands/internal/dev/npm-link-or-shim.ts @@ -20,6 +20,6 @@ export default class InternalDevNpmLinkOrShimCommand extends NemoClawCommand { public async run(): Promise { const { flags } = await this.parse(InternalDevNpmLinkOrShimCommand); const result = runNpmLinkOrShim({ repoRoot: flags["repo-root"] ?? this.config.root }); - if (result.status !== 0) process.exit(result.status); + this.applyExitResult(result); } } diff --git a/src/commands/internal/dns/fix-coredns.ts b/src/commands/internal/dns/fix-coredns.ts index 30bb9b84f62..40c8644e1a6 100644 --- a/src/commands/internal/dns/fix-coredns.ts +++ b/src/commands/internal/dns/fix-coredns.ts @@ -22,12 +22,6 @@ export default class InternalDnsFixCoreDnsCommand extends NemoClawCommand { public async run(): Promise { const { args } = await this.parse(InternalDnsFixCoreDnsCommand); const result = runFixCoreDns({ gatewayName: args.gatewayName }); - if (result.exitCode !== 0) { - if (result.message) { - this.error(result.message, { exit: result.exitCode }); - } else { - this.exit(result.exitCode); - } - } + this.applyExitResult(result); } } diff --git a/src/commands/internal/dns/setup-proxy.ts b/src/commands/internal/dns/setup-proxy.ts index b4b91e463f3..72f7bbe2d89 100644 --- a/src/commands/internal/dns/setup-proxy.ts +++ b/src/commands/internal/dns/setup-proxy.ts @@ -23,9 +23,6 @@ export default class InternalDnsSetupProxyCommand extends NemoClawCommand { public async run(): Promise { const { args } = await this.parse(InternalDnsSetupProxyCommand); const result = runSetupDnsProxy({ gatewayName: args.gatewayName, sandboxName: args.sandboxName }); - if (result.exitCode !== 0) { - if (result.message) console.error(result.message); - process.exit(result.exitCode); - } + this.applyExitResult(result); } } diff --git a/src/commands/internal/uninstall/run-plan.ts b/src/commands/internal/uninstall/run-plan.ts index e0336141ebd..b1ea87ebc55 100644 --- a/src/commands/internal/uninstall/run-plan.ts +++ b/src/commands/internal/uninstall/run-plan.ts @@ -29,6 +29,6 @@ export default class InternalUninstallRunPlanCommand extends NemoClawCommand { gatewayName: flags.gateway, keepOpenShell: flags["keep-openshell"] ?? false, }); - process.exit(result.exitCode); + this.applyExitResult(result); } } diff --git a/src/lib/cli/nemoclaw-oclif-command.test.ts b/src/lib/cli/nemoclaw-oclif-command.test.ts new file mode 100644 index 00000000000..f99979064da --- /dev/null +++ b/src/lib/cli/nemoclaw-oclif-command.test.ts @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { NemoClawCommand, type CommandExitResult } from "./nemoclaw-oclif-command"; + +class TestCommand extends NemoClawCommand { + static id = "test"; + + public async run(): Promise { + // Test-only command wrapper. + } + + public apply(result: CommandExitResult): void { + this.applyExitResult(result); + } + + public fail(lines: readonly string[], code?: number): void { + this.failWithLines(lines, code); + } +} + +function makeCommand(): TestCommand { + return Object.create(TestCommand.prototype) as TestCommand; +} + +describe("NemoClawCommand", () => { + afterEach(() => { + vi.restoreAllMocks(); + process.exitCode = undefined; + }); + + it("records status-like command results without throwing", () => { + makeCommand().apply({ status: 7 }); + + expect(process.exitCode).toBe(7); + }); + + it("prefers exitCode and prints failure messages", () => { + const error = vi.spyOn(console, "error").mockImplementation(() => undefined); + + makeCommand().apply({ exitCode: 3, message: "boom", status: 7 }); + + expect(process.exitCode).toBe(3); + expect(error).toHaveBeenCalledWith("boom"); + }); + + it("prints multi-line failures and records the requested code", () => { + const error = vi.spyOn(console, "error").mockImplementation(() => undefined); + + makeCommand().fail(["line 1", "line 2"], 9); + + expect(process.exitCode).toBe(9); + expect(error.mock.calls).toEqual([["line 1"], ["line 2"]]); + }); +}); diff --git a/src/lib/cli/nemoclaw-oclif-command.ts b/src/lib/cli/nemoclaw-oclif-command.ts index fd7dda76f30..a780875d636 100644 --- a/src/lib/cli/nemoclaw-oclif-command.ts +++ b/src/lib/cli/nemoclaw-oclif-command.ts @@ -3,6 +3,12 @@ import { Command, Flags } from "@oclif/core"; +export type CommandExitResult = { + exitCode?: number | null; + message?: string | null; + status?: number | null; +}; + /** * Shared oclif base for NemoClaw commands. * @@ -17,4 +23,24 @@ export abstract class NemoClawCommand extends Command { protected logJson(json: unknown): void { console.log(JSON.stringify(json, null, 2)); } + + protected setExitCode(code: number): void { + process.exitCode = code; + } + + protected failWithLines(lines: readonly string[], code = 1): void { + for (const line of lines) console.error(line); + this.setExitCode(code); + } + + protected applyExitResult(result: CommandExitResult): void { + const code = + typeof result.exitCode === "number" + ? result.exitCode + : typeof result.status === "number" + ? result.status + : 0; + if (code !== 0 && result.message) this.failWithLines([result.message], code); + else this.setExitCode(code); + } } diff --git a/src/lib/commands/credentials/common.ts b/src/lib/commands/credentials/common.ts index 9ebdb0ef3e2..008b787f715 100644 --- a/src/lib/commands/credentials/common.ts +++ b/src/lib/commands/credentials/common.ts @@ -30,15 +30,21 @@ export function printCredentialsUsage(log: (message?: string) => void = console. log(""); } -export async function recoverGatewayOrExit(kind: "query" | "reach"): Promise { +export function credentialsGatewayRecoveryFailureLines(kind: "query" | "reach"): string[] { + const action = kind === "query" ? "query" : "reach"; + return [ + ` Could not ${action} the ${CLI_DISPLAY_NAME} OpenShell gateway. Is it running?`, + ` Run 'openshell gateway start --name nemoclaw' or '${CLI_NAME} onboard' first.`, + ]; +} + +export async function recoverGatewayOrExit( + kind: "query" | "reach", + reportFailure: (lines: readonly string[]) => void = (lines) => lines.forEach((line) => console.error(line)), +): Promise { const recovery = await recoverNamedGatewayRuntime(); - if (recovery.recovered) return; + if (recovery.recovered) return true; - if (kind === "query") { - console.error(` Could not query the ${CLI_DISPLAY_NAME} OpenShell gateway. Is it running?`); - } else { - console.error(` Could not reach the ${CLI_DISPLAY_NAME} OpenShell gateway. Is it running?`); - } - console.error(` Run 'openshell gateway start --name nemoclaw' or '${CLI_NAME} onboard' first.`); - process.exit(1); + reportFailure(credentialsGatewayRecoveryFailureLines(kind)); + return false; } diff --git a/src/lib/commands/credentials/list.ts b/src/lib/commands/credentials/list.ts index 43594f5da05..12dfb2915e8 100644 --- a/src/lib/commands/credentials/list.ts +++ b/src/lib/commands/credentials/list.ts @@ -20,7 +20,7 @@ export default class CredentialsListCommand extends NemoClawCommand { public async run(): Promise { await this.parse(CredentialsListCommand); - await recoverGatewayOrExit("query"); + if (!(await recoverGatewayOrExit("query", (lines) => this.failWithLines(lines)))) return; const result = runOpenshellProviderCommand(["provider", "list", "--names"], { ignoreError: true, @@ -28,9 +28,11 @@ export default class CredentialsListCommand extends NemoClawCommand { timeout: OPENSHELL_OPERATION_TIMEOUT_MS, }); if (result.status !== 0) { - console.error(" Could not query OpenShell gateway. Is it running?"); - console.error(` Run 'openshell gateway start --name nemoclaw' or '${CLI_NAME} onboard' first.`); - process.exit(1); + this.failWithLines([ + " Could not query OpenShell gateway. Is it running?", + ` Run 'openshell gateway start --name nemoclaw' or '${CLI_NAME} onboard' first.`, + ]); + return; } const allNames = String(result.stdout || "") diff --git a/src/lib/commands/credentials/reset.ts b/src/lib/commands/credentials/reset.ts index 42e0571f0b2..f6549f8ad56 100644 --- a/src/lib/commands/credentials/reset.ts +++ b/src/lib/commands/credentials/reset.ts @@ -36,19 +36,21 @@ export default class CredentialsResetCommand extends NemoClawCommand { const key = args.provider; if (!key || key.startsWith("-")) { - console.error(` Usage: ${CLI_NAME} credentials reset [--yes]`); - console.error(` PROVIDER is an OpenShell provider name. Run '${CLI_NAME} credentials list' first.`); - process.exit(1); + this.failWithLines([ + ` Usage: ${CLI_NAME} credentials reset [--yes]`, + ` PROVIDER is an OpenShell provider name. Run '${CLI_NAME} credentials list' first.`, + ]); + return; } if (isBridgeProviderName(key)) { - console.error(` '${key}' is a per-sandbox messaging bridge, not a credential.`); - console.error( + this.failWithLines([ + ` '${key}' is a per-sandbox messaging bridge, not a credential.`, ` Use \`${CLI_NAME} channels remove \` to retire`, - ); - console.error(" the integration (it tears down the bridge provider and rebuilds the sandbox),"); - console.error(` or \`${CLI_NAME} channels stop <…>\` to pause it without clearing tokens.`); - process.exit(1); + " the integration (it tears down the bridge provider and rebuilds the sandbox),", + ` or \`${CLI_NAME} channels stop <…>\` to pause it without clearing tokens.`, + ]); + return; } if (!flags.yes) { @@ -61,7 +63,7 @@ export default class CredentialsResetCommand extends NemoClawCommand { } } - await recoverGatewayOrExit("reach"); + if (!(await recoverGatewayOrExit("reach", (lines) => this.failWithLines(lines)))) return; const result = runOpenshellProviderCommand(["provider", "delete", key], { ignoreError: true, @@ -74,16 +76,18 @@ export default class CredentialsResetCommand extends NemoClawCommand { return; } - console.error(` Could not remove provider '${key}'.`); + const lines = [` Could not remove provider '${key}'.`]; if (/^[A-Z][A-Z0-9_]+$/.test(key)) { - console.error(""); - console.error(` '${key}' looks like a credential env variable name.`); - console.error(" As of this release, 'credentials reset' takes an OpenShell"); - console.error(` provider name. Run '${CLI_NAME} credentials list' to see the`); - console.error(" registered providers, then retry with one of those names."); + lines.push( + "", + ` '${key}' looks like a credential env variable name.`, + " As of this release, 'credentials reset' takes an OpenShell", + ` provider name. Run '${CLI_NAME} credentials list' to see the`, + " registered providers, then retry with one of those names.", + ); } const stderr = String(result.stderr || "").trim(); - if (stderr) console.error(` ${stderr}`); - process.exit(1); + if (stderr) lines.push(` ${stderr}`); + this.failWithLines(lines); } } diff --git a/src/lib/commands/debug.ts b/src/lib/commands/debug.ts index 49765575c97..95d1d5ff469 100644 --- a/src/lib/commands/debug.ts +++ b/src/lib/commands/debug.ts @@ -64,7 +64,10 @@ function buildDebugCommandDeps(rootDir: string): RunDebugCommandDeps { runDebug, log: console.log, error: console.error, - exit: (code: number) => process.exit(code), + exit: (code: number): never => { + process.exitCode = code; + return undefined as never; + }, }; } diff --git a/src/lib/commands/gateway-token.ts b/src/lib/commands/gateway-token.ts index 13826c22bcb..326ee7cef8a 100644 --- a/src/lib/commands/gateway-token.ts +++ b/src/lib/commands/gateway-token.ts @@ -68,7 +68,7 @@ export default class GatewayTokenCliCommand extends NemoClawCommand { // (e.g. `... | head -c 0`). The token has already been written. process.stdout.once("error", (err: NodeJS.ErrnoException) => { if (err.code === "EPIPE") { - process.exit(0); + this.setExitCode(0); return; } throw err; @@ -83,12 +83,10 @@ export default class GatewayTokenCliCommand extends NemoClawCommand { getSandboxAgent: runtime.getSandboxAgent, }, ); - // NCQ #3180: avoid this.exit(code), which throws @oclif/core ExitError. - // The legacy `nemoclaw gateway-token` dispatch did not catch the - // throw, leaking a raw JS stack trace to the user. Always assigning - // process.exitCode keeps the diagnostic output clean and prevents a - // stale non-zero code from a prior run() in the same process from - // bleeding through on a successful invocation. - process.exitCode = exitCode; + // NCQ #3180: avoid throwing ExitError. The legacy + // `nemoclaw gateway-token` dispatch historically leaked raw JS + // stacks for thrown exits; the shared helper records the status without + // throwing and clears stale non-zero codes on success. + this.setExitCode(exitCode); } } diff --git a/src/lib/commands/global-oclif-command-adapters.test.ts b/src/lib/commands/global-oclif-command-adapters.test.ts index 200508ee6b3..0c223bef2e6 100644 --- a/src/lib/commands/global-oclif-command-adapters.test.ts +++ b/src/lib/commands/global-oclif-command-adapters.test.ts @@ -46,18 +46,30 @@ vi.mock("../actions/global", () => ({ vi.mock("../actions/inference-set", () => ({ InferenceSetError: class InferenceSetError extends Error { - exitCode = 1; + exitCode: number; + + constructor(message: string, exitCode = 1) { + super(message); + this.exitCode = exitCode; + } }, runInferenceSet: mocks.runInferenceSet, })); vi.mock("../actions/inference-get", () => ({ InferenceGetError: class InferenceGetError extends Error { - exitCode = 1; + exitCode: number; + + constructor(message: string, exitCode = 1) { + super(message); + this.exitCode = exitCode; + } }, runInferenceGet: mocks.runInferenceGet, })); +import { InferenceGetError } from "../actions/inference-get"; +import { InferenceSetError } from "../actions/inference-set"; import InferenceGetCommand from "./inference/get"; import InferenceSetCommand from "./inference/set"; import ListCommand from "./list"; @@ -167,4 +179,26 @@ describe("global oclif command adapters", () => { expect(mocks.runInferenceGet).toHaveBeenCalledWith({ json: true }); }); + + it("records inference action failures without throwing oclif ExitError", async () => { + const error = vi.spyOn(console, "error").mockImplementation(() => undefined); + const previousExitCode = process.exitCode; + process.exitCode = undefined; + try { + mocks.runInferenceGet.mockRejectedValueOnce(new InferenceGetError("route missing", 3)); + mocks.runInferenceSet.mockRejectedValueOnce(new InferenceSetError("route rejected", 4)); + + await expect(InferenceGetCommand.run([], rootDir)).resolves.toBeUndefined(); + expect(process.exitCode).toBe(3); + expect(error).toHaveBeenCalledWith("route missing"); + + await expect( + InferenceSetCommand.run(["--provider", "nvidia-prod", "--model", "nvidia/model-a"], rootDir), + ).resolves.toBeUndefined(); + expect(process.exitCode).toBe(4); + expect(error).toHaveBeenCalledWith("route rejected"); + } finally { + process.exitCode = previousExitCode; + } + }); }); diff --git a/src/lib/commands/inference/get.ts b/src/lib/commands/inference/get.ts index a9689a90342..0dc80eec89c 100644 --- a/src/lib/commands/inference/get.ts +++ b/src/lib/commands/inference/get.ts @@ -28,7 +28,8 @@ export default class InferenceGetCommand extends NemoClawCommand { await runInferenceGet({ json: flags.json === true }); } catch (error) { if (error instanceof InferenceGetError) { - this.error(error.message, { exit: error.exitCode }); + this.failWithLines([error.message], error.exitCode); + return; } throw error; } diff --git a/src/lib/commands/inference/set.ts b/src/lib/commands/inference/set.ts index b5045e19e77..1fa708f419a 100644 --- a/src/lib/commands/inference/set.ts +++ b/src/lib/commands/inference/set.ts @@ -51,7 +51,8 @@ export default class InferenceSetCommand extends NemoClawCommand { }); } catch (error) { if (error instanceof InferenceSetError) { - this.error(error.message, { exit: error.exitCode }); + this.failWithLines([error.message], error.exitCode); + return; } throw error; } diff --git a/src/lib/commands/maintenance/update.ts b/src/lib/commands/maintenance/update.ts index 3a03080a2e6..1baaec4a47f 100644 --- a/src/lib/commands/maintenance/update.ts +++ b/src/lib/commands/maintenance/update.ts @@ -41,8 +41,6 @@ export default class UpdateCommand extends NemoClawCommand { rootDir: this.config.root, }, ); - if (result.status !== 0) { - this.exit(result.status); - } + this.applyExitResult(result); } } diff --git a/src/lib/commands/sandbox/config/get.ts b/src/lib/commands/sandbox/config/get.ts index 9898502fcaf..d832fb901b1 100644 --- a/src/lib/commands/sandbox/config/get.ts +++ b/src/lib/commands/sandbox/config/get.ts @@ -38,7 +38,7 @@ export default class SandboxConfigGetCommand extends NemoClawCommand { } } -export function printConfigUsageAndExit(): never { +export function printConfigUsageAndExit(): void { console.error(` Usage: ${CLI_NAME} config get [--key dotpath] [--format json|yaml]`); - process.exit(1); + process.exitCode = 1; } diff --git a/src/lib/commands/sandbox/connect.ts b/src/lib/commands/sandbox/connect.ts index aad97fb966b..1cbc209f134 100644 --- a/src/lib/commands/sandbox/connect.ts +++ b/src/lib/commands/sandbox/connect.ts @@ -28,9 +28,11 @@ export default class ConnectCliCommand extends NemoClawCommand { public async run(): Promise { const { args, flags } = await this.parse(ConnectCliCommand); if (flags["dangerously-skip-permissions"]) { - console.error(" --dangerously-skip-permissions was removed; use shields commands instead."); - console.error(` Usage: ${CLI_NAME} connect [--probe-only]`); - process.exit(1); + this.failWithLines([ + " --dangerously-skip-permissions was removed; use shields commands instead.", + ` Usage: ${CLI_NAME} connect [--probe-only]`, + ]); + return; } await connectSandbox(args.sandboxName, { probeOnly: Boolean(flags["probe-only"]), diff --git a/src/lib/commands/sandbox/skill.test.ts b/src/lib/commands/sandbox/skill.test.ts index bade989d18f..a5d3cbdf8a9 100644 --- a/src/lib/commands/sandbox/skill.test.ts +++ b/src/lib/commands/sandbox/skill.test.ts @@ -3,11 +3,31 @@ import { describe, expect, it, vi } from "vitest"; +import SkillCliCommand from "./skill"; import { setSkillInstallRuntimeBridgeFactoryForTest } from "./skill/common"; import SkillInstallCliCommand from "./skill/install"; const rootDir = process.cwd(); +describe("SkillCliCommand", () => { + it("records a parser-style failure when the sandbox name is missing", async () => { + const sandboxSkillInstall = vi.fn().mockResolvedValue(undefined); + setSkillInstallRuntimeBridgeFactoryForTest(() => ({ sandboxSkillInstall })); + const error = vi.spyOn(console, "error").mockImplementation(() => undefined); + const previousExitCode = process.exitCode; + process.exitCode = undefined; + + try { + await expect(SkillCliCommand.run([], rootDir)).resolves.toBeUndefined(); + expect(process.exitCode).toBe(2); + expect(error).toHaveBeenCalledWith("Missing required sandboxName for skill."); + expect(sandboxSkillInstall).not.toHaveBeenCalled(); + } finally { + process.exitCode = previousExitCode; + } + }); +}); + describe("SkillInstallCliCommand", () => { it("runs skill install with the legacy install argv shape", async () => { const sandboxSkillInstall = vi.fn().mockResolvedValue(undefined); diff --git a/src/lib/commands/sandbox/skill.ts b/src/lib/commands/sandbox/skill.ts index 9c961ce693d..71a629b09b0 100644 --- a/src/lib/commands/sandbox/skill.ts +++ b/src/lib/commands/sandbox/skill.ts @@ -13,9 +13,11 @@ export default class SkillCliCommand extends NemoClawCommand { static examples = ["<%= config.bin %> sandbox skill install alpha ./my-skill"]; public async run(): Promise { + this.parsed = true; const [sandboxName, ...actionArgs] = this.argv; if (!sandboxName || sandboxName.trim() === "") { - this.error("Missing required sandboxName for skill.", { exit: 2 }); + this.failWithLines(["Missing required sandboxName for skill."], 2); + return; } await getSkillInstallRuntimeBridge().sandboxSkillInstall(sandboxName, actionArgs); } diff --git a/src/lib/commands/uninstall.ts b/src/lib/commands/uninstall.ts index 9dfe41c2e1f..67355c41728 100644 --- a/src/lib/commands/uninstall.ts +++ b/src/lib/commands/uninstall.ts @@ -29,7 +29,10 @@ export default class UninstallCliCommand extends NemoClawCommand { spawnSyncImpl: spawnSync, log: console.log, error: console.error, - exit: (code: number) => process.exit(code), + exit: (code: number): never => { + this.setExitCode(code); + return undefined as never; + }, }); } } diff --git a/test/credentials-cli-command.test.ts b/test/credentials-cli-command.test.ts index 032288f58c6..3e8c6613f98 100644 --- a/test/credentials-cli-command.test.ts +++ b/test/credentials-cli-command.test.ts @@ -3,7 +3,7 @@ import { createRequire } from "node:module"; import path from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; const require = createRequire(import.meta.url); const REPO_ROOT = path.join(import.meta.dirname, ".."); @@ -39,12 +39,6 @@ type RuntimeBridge = { }; type OpenshellCall = { args: string[]; opts?: RuntimeBridgeRunOptions }; -class ProcessExitError extends Error { - constructor(readonly code: number) { - super(`process.exit(${code})`); - } -} - function loadCommands(): CredentialsCommandClasses { for (const modulePath of Object.values(COMMAND_PATHS)) { delete require.cache[modulePath]; @@ -117,23 +111,14 @@ async function captureOutput( return { stdout, stderr }; } -async function expectProcessExit( - action: () => Promise, - expectedCode: number, -): Promise { - const originalExit = process.exit; - process.exit = ((code?: string | number | null | undefined) => { - throw new ProcessExitError(typeof code === "number" ? code : 1); - }) as typeof process.exit; - +async function expectExitCode(action: () => Promise, expectedCode: number): Promise { + const originalExitCode = process.exitCode; + process.exitCode = undefined; try { await action(); - throw new Error("Expected process.exit to be called"); - } catch (error) { - if (!(error instanceof ProcessExitError)) throw error; - expect(error.code).toBe(expectedCode); + expect(process.exitCode).toBe(expectedCode); } finally { - process.exit = originalExit; + process.exitCode = originalExitCode; } } @@ -205,14 +190,26 @@ describe("credentials oclif commands", () => { }); const { CredentialsListCommand } = loadCommands(); - const output = await captureOutput(() => - expectProcessExit(() => CredentialsListCommand.run([]), 1), - ); + const output = await captureOutput(() => expectExitCode(() => CredentialsListCommand.run([]), 1)); expect(output.stderr).toContain("Could not query OpenShell gateway"); expect(output.stderr).toContain("openshell gateway start --name nemoclaw"); }); + it("records gateway recovery failures without calling provider list", async () => { + const runOpenshell = vi.fn(() => ({ status: 0, stdout: "nvidia-prod" })); + installRuntimeBridge({ + recoverNamedGatewayRuntime: async () => ({ recovered: false }), + runOpenshell, + }); + const { CredentialsListCommand } = loadCommands(); + + const output = await captureOutput(() => expectExitCode(() => CredentialsListCommand.run([]), 1)); + + expect(output.stderr).toContain("Could not query the NemoClaw OpenShell gateway"); + expect(runOpenshell).not.toHaveBeenCalled(); + }); + it("deletes a provider credential with --yes", async () => { const calls = installRuntimeBridge({ runOpenshell: (args, opts) => { @@ -239,7 +236,7 @@ describe("credentials oclif commands", () => { const { CredentialsResetCommand } = loadCommands(); const output = await captureOutput(() => - expectProcessExit(() => CredentialsResetCommand.run(["alpha-telegram-bridge", "--yes"]), 1), + expectExitCode(() => CredentialsResetCommand.run(["alpha-telegram-bridge", "--yes"]), 1), ); expect(output.stderr).toContain("per-sandbox messaging bridge"); @@ -253,7 +250,7 @@ describe("credentials oclif commands", () => { const { CredentialsResetCommand } = loadCommands(); const output = await captureOutput(() => - expectProcessExit(() => CredentialsResetCommand.run(["NVIDIA_API_KEY", "--yes"]), 1), + expectExitCode(() => CredentialsResetCommand.run(["NVIDIA_API_KEY", "--yes"]), 1), ); expect(output.stderr).toContain("Could not remove provider 'NVIDIA_API_KEY'.");