From 90e39e68991498257a053b893db6ae1393cf21cf Mon Sep 17 00:00:00 2001 From: zyang-dev <267119621+zyang-dev@users.noreply.github.com> Date: Mon, 25 May 2026 17:13:38 -0700 Subject: [PATCH 1/2] fix(onboard): restore DGX Spark and Station vLLM menu entry Signed-off-by: zyang-dev <267119621+zyang-dev@users.noreply.github.com> --- src/lib/onboard.ts | 1 + test/onboard-selection.test.ts | 172 +++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index bdc01905d3..34d4f58f18 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -4350,6 +4350,7 @@ async function setupNim( vllmRunning, vllmProfile, experimental: EXPERIMENTAL, + platform: gpu?.platform, hasVllmImage, }), ); diff --git a/test/onboard-selection.test.ts b/test/onboard-selection.test.ts index 4c28bf2052..e6ff02aab9 100644 --- a/test/onboard-selection.test.ts +++ b/test/onboard-selection.test.ts @@ -648,6 +648,178 @@ const { setupNim } = require(${onboardPath}); assert.doesNotMatch(result.stderr, /INSTALL_VLLM_CALLED/); }); + it("surfaces managed vLLM by default on DGX Spark and Station only", () => { + const repoRoot = path.join(import.meta.dirname, ".."); + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-vllm-platform-")); + const fakeBin = path.join(tmpDir, "bin"); + const scriptPath = path.join(tmpDir, "vllm-platform-menu-check.js"); + const onboardPath = JSON.stringify(path.join(repoRoot, "dist", "lib", "onboard.js")); + const credentialsPath = JSON.stringify( + path.join(repoRoot, "dist", "lib", "credentials", "store.js"), + ); + const runnerPath = JSON.stringify(path.join(repoRoot, "dist", "lib", "runner.js")); + const dockerRunPath = JSON.stringify( + path.join(repoRoot, "dist", "lib", "adapters", "docker", "run.js"), + ); + type VllmPlatformScenario = + | { + name: string; + gpu: { type: string; platform: string }; + vllmExpected: true; + platformLabel: string; + } + | { + name: string; + gpu: { type: string; platform: string }; + vllmExpected: false; + }; + const scenarios: VllmPlatformScenario[] = [ + { + name: "spark", + gpu: { type: "nvidia", platform: "spark" }, + vllmExpected: true, + platformLabel: "DGX Spark", + }, + { + name: "station", + gpu: { type: "nvidia", platform: "station" }, + vllmExpected: true, + platformLabel: "DGX Station", + }, + { + name: "linux", + gpu: { type: "nvidia", platform: "linux" }, + vllmExpected: false, + }, + ]; + + fs.mkdirSync(fakeBin, { recursive: true }); + fs.writeFileSync( + path.join(fakeBin, "curl"), + `#!/usr/bin/env bash +body='{"id":"ok"}' +status="200" +outfile="" +while [ "$#" -gt 0 ]; do + case "$1" in + -o) outfile="$2"; shift 2 ;; + *) shift ;; + esac +done +printf '%s' "$body" > "$outfile" +printf '%s' "$status" +`, + { mode: 0o755 }, + ); + + const script = String.raw` +const credentials = require(${credentialsPath}); +const runner = require(${runnerPath}); +const dockerRun = require(${dockerRunPath}); + +process.env.NEMOCLAW_NON_INTERACTIVE = ""; +process.env.NEMOCLAW_EXPERIMENTAL = ""; +process.env.NEMOCLAW_PROVIDER = ""; +process.env.NEMOCLAW_MODEL = ""; + +credentials.ensureApiKey = async () => { + process.env.NVIDIA_API_KEY = "nvapi-good"; +}; +runner.runCapture = (command) => { + const cmd = Array.isArray(command) ? command.join(" ") : command; + if (cmd.includes("command -v ollama")) return ""; + if (cmd.includes("127.0.0.1:11434/api/tags")) return ""; + if (cmd.includes("127.0.0.1:8000/v1/models")) return ""; + if (cmd.includes("docker images")) return ""; + return ""; +}; +dockerRun.dockerCapture = () => ""; + +const scenarios = ${JSON.stringify(scenarios)}; + +async function runScenario(scenario) { + const messages = []; + const lines = []; + credentials.prompt = async (message) => { + messages.push(message); + if (/Choose \[/.test(message)) return "1"; + return ""; + }; + process.env.NEMOCLAW_PROVIDER = ""; + process.env.NEMOCLAW_MODEL = ""; + process.env.NVIDIA_API_KEY = ""; + delete require.cache[require.resolve(${onboardPath})]; + const { setupNim } = require(${onboardPath}); + const originalLog = console.log; + console.log = (...args) => lines.push(args.join(" ")); + try { + const result = await setupNim(scenario.gpu, null); + return { name: scenario.name, result, messages, lines }; + } finally { + console.log = originalLog; + } +} + +(async () => { + const results = []; + // These scenarios intentionally run serially in one process. The regression + // varies only the gpu.platform argument passed into setupNim(), while the + // expensive module graph and mocks are shared to keep this integration test + // lightweight. + for (const scenario of scenarios) { + results.push(await runScenario(scenario)); + } + console.log(JSON.stringify({ results })); +})().catch((error) => { + console.error(error); + process.exit(1); +}); +`; + fs.writeFileSync(scriptPath, script); + + const result = spawnSync(process.execPath, [scriptPath], { + cwd: repoRoot, + encoding: "utf-8", + env: { + ...process.env, + HOME: tmpDir, + PATH: `${fakeBin}:${process.env.PATH || ""}`, + NEMOCLAW_NON_INTERACTIVE: "", + NEMOCLAW_EXPERIMENTAL: "", + NEMOCLAW_PROVIDER: "", + NEMOCLAW_MODEL: "", + }, + }); + + assert.equal(result.status, 0, result.stderr); + assert.notEqual(result.stdout.trim(), ""); + const payload = JSON.parse(result.stdout.trim()); + + for (const scenario of scenarios) { + const scenarioResult = payload.results.find( + (entry: { name: string }) => entry.name === scenario.name, + ); + assert.ok(scenarioResult, scenario.name); + const menuOutput = scenarioResult.lines.join("\n"); + assert.ok( + scenarioResult.messages.some((message: string) => /Choose \[/.test(message)), + scenario.name, + ); + assert.ok(menuOutput.length > 0, `${scenario.name}: empty menu output`); + + if (scenario.vllmExpected) { + assert.ok( + menuOutput.includes(`Install vLLM (${scenario.platformLabel})`) || + menuOutput.includes(`Start vLLM (${scenario.platformLabel})`), + scenario.name, + ); + } else { + assert.doesNotMatch(menuOutput, /Install vLLM \(/); + assert.doesNotMatch(menuOutput, /Start vLLM \(/); + } + } + }); + it("surfaces a precise error when NEMOCLAW_PROVIDER=install-vllm but no vLLM profile is detected (#3765)", () => { const repoRoot = path.join(import.meta.dirname, ".."); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-install-vllm-no-profile-")); From 68e8358e5c84fb6e134499cbb91fb52bfc2fe38d Mon Sep 17 00:00:00 2001 From: zyang-dev <267119621+zyang-dev@users.noreply.github.com> Date: Mon, 25 May 2026 20:49:39 -0700 Subject: [PATCH 2/2] Remove stale orphaned Ollama lifecycle comment to keep onboard.ts line-neutral Signed-off-by: zyang-dev <267119621+zyang-dev@users.noreply.github.com> --- src/lib/onboard.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 34d4f58f18..65c77e0464 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -4261,7 +4261,6 @@ async function setupNim( // (#2674). const localProbeCurlArgs = ["--connect-timeout", "2", "--max-time", "5"] as const; const hasOllama = hostCommandExists("ollama"); - // run and consumed by the Ollama lifecycle helpers in inference/local.ts. const ollamaHost = findReachableOllamaHost(); const ollamaRunning = ollamaHost !== null; const vllmRunning = !!runCapture(