diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index a6af9f63a1..8627c75543 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -90,6 +90,9 @@ const { const { resolveRequestedProviderSelection, }: typeof import("./onboard/provider-selection") = require("./onboard/provider-selection"); +const { + reportProviderSelectionFailure, +}: typeof import("./onboard/provider-selection-failure") = require("./onboard/provider-selection-failure"); const { promptForInferenceProviderSelection, }: typeof import("./onboard/provider-selection-prompt") = require("./onboard/provider-selection-prompt"); @@ -3757,43 +3760,12 @@ async function setupNim( readRecordedModel, }); if (providerSelection.kind === "failure") { - switch (providerSelection.reason.kind) { - case "wsl-recorded-ollama-windows-host": - console.error( - ` Recorded provider '${providerSelection.reason.recordedProvider}' (WSL Ollama) is not available in this environment.`, - ); - console.error( - " Hint: Windows-host Ollama is reachable here; re-run with NEMOCLAW_PROVIDER=ollama to use it explicitly.", - ); - break; - case "recorded-provider-unavailable": - console.error( - ` Recorded provider '${providerSelection.reason.recordedProvider}' is not available in this environment.`, - ); - console.error( - " Set NEMOCLAW_PROVIDER explicitly, or restore the missing local-inference dependency.", - ); - if (providerSelection.reason.windowsHostKey) { - console.error( - ` Hint: Windows-host Ollama is available here — re-run with NEMOCLAW_PROVIDER=${providerSelection.reason.windowsHostKey} to use it.`, - ); - } - break; - case "unsupported-windows-host-ollama": - rejectWindowsHostOllama(providerSelection.reason.providerKey, isWindowsHostOllama); - break; - case "hermes-provider-unavailable": - console.error(" Hermes Provider is only available when onboarding Hermes Agent."); - console.error( - " Re-run with `nemohermes onboard` or `nemoclaw onboard --agent hermes`.", - ); - break; - case "requested-provider-unavailable": - console.error( - ` Requested provider '${providerSelection.reason.providerKey}' is not available in this environment.`, - ); - break; - } + reportProviderSelectionFailure({ + reason: providerSelection.reason, + isWindowsHostOllama, + rejectWindowsHostOllama, + writeError: (message) => console.error(message), + }); process.exit(1); } selected = providerSelection.selected; diff --git a/src/lib/onboard/provider-selection-failure.test.ts b/src/lib/onboard/provider-selection-failure.test.ts new file mode 100644 index 0000000000..51d0b739a8 --- /dev/null +++ b/src/lib/onboard/provider-selection-failure.test.ts @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import assert from "node:assert/strict"; +import { describe, it } from "vitest"; + +import { reportProviderSelectionFailure } from "../../../dist/lib/onboard/provider-selection-failure"; + +function report(overrides: Partial[0]>): { + errors: string[]; + rejected: Array<{ providerKey: string; windowsHostSelected: boolean }>; +} { + const errors: string[] = []; + const rejected: Array<{ providerKey: string; windowsHostSelected: boolean }> = []; + + reportProviderSelectionFailure({ + reason: { kind: "requested-provider-unavailable", providerKey: "missing" }, + isWindowsHostOllama: false, + rejectWindowsHostOllama: (providerKey, windowsHostSelected) => { + rejected.push({ providerKey, windowsHostSelected }); + return true; + }, + writeError: (message) => errors.push(message), + ...overrides, + }); + + return { errors, rejected }; +} + +describe("reportProviderSelectionFailure", () => { + it("reports recorded WSL Ollama recovery as unavailable on Windows host", () => { + const { errors, rejected } = report({ + reason: { + kind: "wsl-recorded-ollama-windows-host", + recordedProvider: "ollama-local", + }, + }); + + assert.deepEqual(rejected, []); + assert.deepEqual(errors, [ + " Recorded provider 'ollama-local' (WSL Ollama) is not available in this environment.", + " Hint: Windows-host Ollama is reachable here; re-run with NEMOCLAW_PROVIDER=ollama to use it explicitly.", + ]); + }); + + it("adds a Windows-host hint when recorded provider recovery has a host action", () => { + const { errors, rejected } = report({ + reason: { + kind: "recorded-provider-unavailable", + recordedProvider: "ollama-local", + recoveredKey: "ollama", + windowsHostKey: "start-windows-ollama", + }, + }); + + assert.deepEqual(rejected, []); + assert.deepEqual(errors, [ + " Recorded provider 'ollama-local' is not available in this environment.", + " Set NEMOCLAW_PROVIDER explicitly, or restore the missing local-inference dependency.", + " Hint: Windows-host Ollama is available here — re-run with NEMOCLAW_PROVIDER=start-windows-ollama to use it.", + ]); + }); + + it("delegates unsupported Windows-host Ollama failures to the rejection helper", () => { + const { errors, rejected } = report({ + reason: { kind: "unsupported-windows-host-ollama", providerKey: "start-windows-ollama" }, + isWindowsHostOllama: true, + }); + + assert.deepEqual(errors, []); + assert.deepEqual(rejected, [ + { providerKey: "start-windows-ollama", windowsHostSelected: true }, + ]); + }); + + it("reports Hermes Provider when requested for an unsupported agent", () => { + const { errors, rejected } = report({ + reason: { kind: "hermes-provider-unavailable" }, + }); + + assert.deepEqual(rejected, []); + assert.deepEqual(errors, [ + " Hermes Provider is only available when onboarding Hermes Agent.", + " Re-run with `nemohermes onboard` or `nemoclaw onboard --agent hermes`.", + ]); + }); + + it("reports unavailable requested providers", () => { + const { errors, rejected } = report({ + reason: { kind: "requested-provider-unavailable", providerKey: "missing-provider" }, + }); + + assert.deepEqual(rejected, []); + assert.deepEqual(errors, [ + " Requested provider 'missing-provider' is not available in this environment.", + ]); + }); +}); diff --git a/src/lib/onboard/provider-selection-failure.ts b/src/lib/onboard/provider-selection-failure.ts new file mode 100644 index 0000000000..a5501fcd12 --- /dev/null +++ b/src/lib/onboard/provider-selection-failure.ts @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { ProviderSelectionFailureReason } from "./provider-selection"; + +export interface ReportProviderSelectionFailureInput { + reason: ProviderSelectionFailureReason; + isWindowsHostOllama: boolean; + rejectWindowsHostOllama(providerKey: string, windowsHostSelected: boolean): boolean; + writeError(message: string): void; +} + +export function reportProviderSelectionFailure(input: ReportProviderSelectionFailureInput): void { + switch (input.reason.kind) { + case "wsl-recorded-ollama-windows-host": + input.writeError( + ` Recorded provider '${input.reason.recordedProvider}' (WSL Ollama) is not available in this environment.`, + ); + input.writeError( + " Hint: Windows-host Ollama is reachable here; re-run with NEMOCLAW_PROVIDER=ollama to use it explicitly.", + ); + break; + case "recorded-provider-unavailable": + input.writeError( + ` Recorded provider '${input.reason.recordedProvider}' is not available in this environment.`, + ); + input.writeError( + " Set NEMOCLAW_PROVIDER explicitly, or restore the missing local-inference dependency.", + ); + if (input.reason.windowsHostKey) { + input.writeError( + ` Hint: Windows-host Ollama is available here — re-run with NEMOCLAW_PROVIDER=${input.reason.windowsHostKey} to use it.`, + ); + } + break; + case "unsupported-windows-host-ollama": + input.rejectWindowsHostOllama(input.reason.providerKey, input.isWindowsHostOllama); + break; + case "hermes-provider-unavailable": + input.writeError(" Hermes Provider is only available when onboarding Hermes Agent."); + input.writeError(" Re-run with `nemohermes onboard` or `nemoclaw onboard --agent hermes`."); + break; + case "requested-provider-unavailable": + input.writeError( + ` Requested provider '${input.reason.providerKey}' is not available in this environment.`, + ); + break; + } +}