diff --git a/src/lib/credentials-cli-command-source.test.ts b/src/lib/credentials-cli-command-source.test.ts new file mode 100644 index 00000000000..655c236e801 --- /dev/null +++ b/src/lib/credentials-cli-command-source.test.ts @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + prompt: vi.fn().mockResolvedValue("yes"), + recoverNamedGatewayRuntime: vi.fn().mockResolvedValue({ recovered: true }), + runOpenshellProviderCommand: vi.fn(), +})); + +vi.mock("./credentials", () => ({ prompt: mocks.prompt })); +vi.mock("./global-cli-actions", () => ({ + recoverNamedGatewayRuntime: mocks.recoverNamedGatewayRuntime, + runOpenshellProviderCommand: mocks.runOpenshellProviderCommand, +})); + +import { + CredentialsCommand, + CredentialsListCommand, + CredentialsResetCommand, +} from "./credentials-cli-command"; + +const rootDir = process.cwd(); + +describe("credentials oclif adapter source coverage", () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.recoverNamedGatewayRuntime.mockResolvedValue({ recovered: true }); + mocks.runOpenshellProviderCommand.mockReturnValue({ status: 0, stdout: "nvidia-prod\n" }); + }); + + it("prints top-level credentials usage", async () => { + const log = vi.spyOn(console, "log").mockImplementation(() => {}); + + await CredentialsCommand.run([], rootDir); + + const output = log.mock.calls.map((call) => String(call[0] ?? "")).join("\n"); + log.mockRestore(); + expect(output).toContain("Usage: nemoclaw credentials "); + expect(output).toContain("reset [--yes]"); + }); + + it("lists credential providers while hiding messaging bridge providers", async () => { + mocks.runOpenshellProviderCommand.mockReturnValue({ + status: 0, + stdout: "alpha-telegram-bridge\nnvidia-prod\nopenai-prod\n", + }); + const log = vi.spyOn(console, "log").mockImplementation(() => {}); + + await CredentialsListCommand.run([], rootDir); + + expect(mocks.recoverNamedGatewayRuntime).toHaveBeenCalledWith(); + expect(mocks.runOpenshellProviderCommand).toHaveBeenCalledWith(["provider", "list", "--names"], { + ignoreError: true, + stdio: ["ignore", "pipe", "pipe"], + timeout: 30_000, + }); + const output = log.mock.calls.map((call) => String(call[0] ?? "")).join("\n"); + log.mockRestore(); + expect(output).toContain("nvidia-prod"); + expect(output).toContain("openai-prod"); + expect(output).toContain("1 per-sandbox messaging bridge"); + expect(output).not.toContain("alpha-telegram-bridge\n"); + }); + + it("deletes provider credentials with --yes", async () => { + const log = vi.spyOn(console, "log").mockImplementation(() => {}); + + await CredentialsResetCommand.run(["nvidia-prod", "--yes"], rootDir); + + expect(mocks.prompt).not.toHaveBeenCalled(); + expect(mocks.runOpenshellProviderCommand).toHaveBeenCalledWith(["provider", "delete", "nvidia-prod"], { + ignoreError: true, + stdio: ["ignore", "pipe", "pipe"], + timeout: 30_000, + }); + const output = log.mock.calls.map((call) => String(call[0] ?? "")).join("\n"); + log.mockRestore(); + expect(output).toContain("Removed provider 'nvidia-prod'"); + }); +}); diff --git a/src/lib/credentials-cli-command.ts b/src/lib/credentials-cli-command.ts index a9349d463fe..f8a0ade4f16 100644 --- a/src/lib/credentials-cli-command.ts +++ b/src/lib/credentials-cli-command.ts @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -/* v8 ignore start -- thin oclif adapter covered through CLI integration tests. */ - import { Args, Command, Flags } from "@oclif/core"; import { CLI_DISPLAY_NAME, CLI_NAME } from "./branding"; diff --git a/src/lib/debug-cli-command.ts b/src/lib/debug-cli-command.ts index 95c8ae99744..f7d606877d3 100644 --- a/src/lib/debug-cli-command.ts +++ b/src/lib/debug-cli-command.ts @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -/* v8 ignore start -- thin oclif adapter covered through CLI integration tests. */ - import { Command, Flags } from "@oclif/core"; import { CLI_NAME } from "./branding"; diff --git a/src/lib/deploy-cli-command.ts b/src/lib/deploy-cli-command.ts index 36cf13f8ea3..854f3650845 100644 --- a/src/lib/deploy-cli-command.ts +++ b/src/lib/deploy-cli-command.ts @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -/* v8 ignore start -- thin oclif adapter covered through CLI integration tests. */ - import { Args, Command, Flags } from "@oclif/core"; import { runDeployAction } from "./global-cli-actions"; diff --git a/src/lib/gateway-token-cli-command.ts b/src/lib/gateway-token-cli-command.ts index b083188cff7..901fb5aa5b2 100644 --- a/src/lib/gateway-token-cli-command.ts +++ b/src/lib/gateway-token-cli-command.ts @@ -1,16 +1,30 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -/* v8 ignore start -- thin oclif adapter covered through CLI integration tests. */ - import { Args, Command, Flags } from "@oclif/core"; import { runGatewayTokenCommand } from "./gateway-token-command"; -const { fetchGatewayAuthTokenFromSandbox } = require("./onboard") as { +type GatewayTokenRuntimeBridge = { fetchGatewayAuthTokenFromSandbox: (sandboxName: string) => string | null; }; +/* v8 ignore next -- source tests inject this bridge; CLI subprocess tests cover the real onboard module. */ +let runtimeBridgeFactory = (): GatewayTokenRuntimeBridge => { + const onboard = require("./onboard") as GatewayTokenRuntimeBridge; + return { fetchGatewayAuthTokenFromSandbox: onboard.fetchGatewayAuthTokenFromSandbox }; +}; + +export function setGatewayTokenRuntimeBridgeFactoryForTest( + factory: () => GatewayTokenRuntimeBridge, +): void { + runtimeBridgeFactory = factory; +} + +function getRuntimeBridge(): GatewayTokenRuntimeBridge { + return runtimeBridgeFactory(); +} + export default class GatewayTokenCliCommand extends Command { static id = "sandbox:gateway:token"; static strict = true; @@ -37,14 +51,15 @@ export default class GatewayTokenCliCommand extends Command { const { args, flags } = await this.parse(GatewayTokenCliCommand); // Suppress EPIPE traces when the consumer closes the pipe early // (e.g. `... | head -c 0`). The token has already been written. - process.stdout.on("error", (err: NodeJS.ErrnoException) => { + process.stdout.on("error", /* v8 ignore next -- pipe-close behavior is covered by CLI usage. */ (err: NodeJS.ErrnoException) => { if (err.code === "EPIPE") process.exit(0); }); + const runtime = getRuntimeBridge(); const exitCode = runGatewayTokenCommand( args.sandboxName, { quiet: flags.quiet === true }, - { fetchToken: fetchGatewayAuthTokenFromSandbox }, + { fetchToken: runtime.fetchGatewayAuthTokenFromSandbox }, ); if (exitCode !== 0) this.exit(exitCode); } diff --git a/src/lib/help-version-cli-commands.ts b/src/lib/help-version-cli-commands.ts index 3727cd8c323..7b65966abba 100644 --- a/src/lib/help-version-cli-commands.ts +++ b/src/lib/help-version-cli-commands.ts @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -/* v8 ignore start -- thin oclif adapters covered through CLI integration tests. */ - import { Command } from "@oclif/core"; import { showRootHelp, showVersion } from "./global-cli-actions"; diff --git a/src/lib/simple-global-oclif-adapters.test.ts b/src/lib/simple-global-oclif-adapters.test.ts new file mode 100644 index 00000000000..4db7426541d --- /dev/null +++ b/src/lib/simple-global-oclif-adapters.test.ts @@ -0,0 +1,157 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + buildVersionedUninstallUrl: vi.fn((version: string) => `https://example.test/${version}/uninstall.sh`), + fetchGatewayAuthTokenFromSandbox: vi.fn(() => "token"), + getVersion: vi.fn(() => "1.2.3"), + captureOpenshellCommand: vi.fn(() => ({ status: 0, output: "alpha\n" })), + listSandboxes: vi.fn(() => ({ sandboxes: [] })), + resolveOpenshell: vi.fn(() => "/usr/bin/openshell"), + runDebugCommandWithOptions: vi.fn(), + runDeployAction: vi.fn().mockResolvedValue(undefined), + runGatewayTokenCommand: vi.fn(() => 0), + runStartCommand: vi.fn().mockResolvedValue(undefined), + runStopCommand: vi.fn(), + runUninstallCommand: vi.fn(), + showRootHelp: vi.fn(), + showVersion: vi.fn(), + spawnSync: vi.fn(), + startAll: vi.fn(), + stopAll: vi.fn(), +})); + +vi.mock("node:child_process", () => ({ spawnSync: mocks.spawnSync })); +vi.mock("./debug", () => ({ runDebug: vi.fn() })); +vi.mock("./debug-command", () => ({ + runDebugCommandWithOptions: mocks.runDebugCommandWithOptions, +})); +vi.mock("./gateway-token-command", () => ({ + runGatewayTokenCommand: mocks.runGatewayTokenCommand, +})); +vi.mock("./global-cli-actions", () => ({ + runDeployAction: mocks.runDeployAction, + showRootHelp: mocks.showRootHelp, + showVersion: mocks.showVersion, +})); +vi.mock("./openshell", () => ({ captureOpenshellCommand: mocks.captureOpenshellCommand })); +vi.mock("./registry", () => ({ listSandboxes: mocks.listSandboxes })); +vi.mock("./resolve-openshell", () => ({ resolveOpenshell: mocks.resolveOpenshell })); +vi.mock("./services", () => ({ startAll: mocks.startAll, stopAll: mocks.stopAll })); +vi.mock("./services-command", () => ({ + runStartCommand: mocks.runStartCommand, + runStopCommand: mocks.runStopCommand, +})); +vi.mock("./uninstall-command", () => ({ + buildVersionedUninstallUrl: mocks.buildVersionedUninstallUrl, + runUninstallCommand: mocks.runUninstallCommand, +})); +vi.mock("./version", () => ({ getVersion: mocks.getVersion })); + +import DebugCliCommand from "./debug-cli-command"; +import DeployCliCommand from "./deploy-cli-command"; +import GatewayTokenCliCommand, { + setGatewayTokenRuntimeBridgeFactoryForTest, +} from "./gateway-token-cli-command"; +import { RootHelpCommand, VersionCommand } from "./help-version-cli-commands"; +import { + DeprecatedStartCommand, + DeprecatedStopCommand, + TunnelStartCommand, + TunnelStopCommand, +} from "./tunnel-commands"; +import UninstallCliCommand from "./uninstall-cli-command"; + +const rootDir = process.cwd(); + +describe("simple global oclif adapters", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("maps debug and deploy parser output to actions", async () => { + await DebugCliCommand.run(["--quick", "--output", "/tmp/debug.tar.gz", "--sandbox", "alpha"], rootDir); + await DeployCliCommand.run(["gpu-alpha"], rootDir); + + expect(mocks.runDebugCommandWithOptions).toHaveBeenCalledWith( + { quick: true, output: "/tmp/debug.tar.gz", sandboxName: "alpha" }, + expect.objectContaining({ getDefaultSandbox: expect.any(Function), runDebug: expect.any(Function) }), + ); + expect(mocks.runDeployAction).toHaveBeenCalledWith("gpu-alpha"); + }); + + it("builds debug defaults from the sandbox registry and OpenShell liveness", async () => { + mocks.listSandboxes.mockReturnValue({ + defaultSandbox: "alpha", + sandboxes: [{ name: "alpha" }], + } as never); + await DebugCliCommand.run(["--quick"], rootDir); + + const deps = mocks.runDebugCommandWithOptions.mock.calls[0][1]; + expect(deps.getDefaultSandbox()).toBe("alpha"); + expect(mocks.captureOpenshellCommand).toHaveBeenCalledWith( + "/usr/bin/openshell", + ["sandbox", "list"], + expect.objectContaining({ cwd: rootDir, ignoreError: true }), + ); + }); + + it("maps gateway-token flags to the gateway token action", async () => { + setGatewayTokenRuntimeBridgeFactoryForTest(() => ({ + fetchGatewayAuthTokenFromSandbox: mocks.fetchGatewayAuthTokenFromSandbox, + })); + + await GatewayTokenCliCommand.run(["alpha", "--quiet"], rootDir); + + expect(mocks.runGatewayTokenCommand).toHaveBeenCalledWith( + "alpha", + { quiet: true }, + { fetchToken: mocks.fetchGatewayAuthTokenFromSandbox }, + ); + }); + + it("runs hidden root help and version adapters", async () => { + await RootHelpCommand.run([], rootDir); + await VersionCommand.run([], rootDir); + + expect(mocks.showRootHelp).toHaveBeenCalledWith(); + expect(mocks.showVersion).toHaveBeenCalledWith(); + }); + + it("maps tunnel and deprecated service commands to service actions", async () => { + await TunnelStartCommand.run([], rootDir); + await TunnelStopCommand.run([], rootDir); + await DeprecatedStartCommand.run([], rootDir); + await DeprecatedStopCommand.run([], rootDir); + + expect(mocks.runStartCommand).toHaveBeenCalledTimes(2); + expect(mocks.runStopCommand).toHaveBeenCalledTimes(2); + expect(mocks.runStartCommand).toHaveBeenCalledWith( + expect.objectContaining({ listSandboxes: expect.any(Function), startAll: mocks.startAll }), + ); + expect(mocks.runStopCommand).toHaveBeenCalledWith( + expect.objectContaining({ listSandboxes: expect.any(Function), stopAll: mocks.stopAll }), + ); + }); + + it("passes uninstall runtime dependencies to the uninstall action", async () => { + const originalEnv = process.env; + await UninstallCliCommand.run(["--yes"], rootDir); + + expect(mocks.buildVersionedUninstallUrl).toHaveBeenCalledWith("1.2.3"); + expect(mocks.runUninstallCommand).toHaveBeenCalledWith( + expect.objectContaining({ + args: ["--yes"], + rootDir, + remoteScriptUrl: "https://example.test/1.2.3/uninstall.sh", + env: originalEnv, + spawnSyncImpl: mocks.spawnSync, + log: console.log, + error: console.error, + exit: expect.any(Function), + }), + ); + }); +}); diff --git a/src/lib/tunnel-commands.ts b/src/lib/tunnel-commands.ts index 69aca9bff92..1147a484543 100644 --- a/src/lib/tunnel-commands.ts +++ b/src/lib/tunnel-commands.ts @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -/* v8 ignore start -- thin oclif adapters covered through CLI integration tests. */ - import { Command, Flags } from "@oclif/core"; import { CLI_NAME } from "./branding"; diff --git a/src/lib/uninstall-cli-command.ts b/src/lib/uninstall-cli-command.ts index a52abc37da6..95ff41119a8 100644 --- a/src/lib/uninstall-cli-command.ts +++ b/src/lib/uninstall-cli-command.ts @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -/* v8 ignore start -- thin oclif adapter covered through CLI integration tests. */ - import { spawnSync } from "node:child_process"; import { Command, Flags } from "@oclif/core"; @@ -32,7 +30,7 @@ export default class UninstallCliCommand extends Command { spawnSyncImpl: spawnSync, log: console.log, error: console.error, - exit: (code: number) => process.exit(code), + exit: /* v8 ignore next -- uninstall exit behavior is covered by uninstall command tests. */ (code: number) => process.exit(code), }); } }