diff --git a/ci/platform-matrix.json b/ci/platform-matrix.json
index 7ad3b65a4a0..9f9c2ffcd16 100644
--- a/ci/platform-matrix.json
+++ b/ci/platform-matrix.json
@@ -47,7 +47,7 @@
"status": "tested",
"prd_priority": "P1",
"ci_tested": true,
- "notes": "Use the standard installer and `$$nemoclaw onboard`. For an end-to-end walkthrough with local Ollama inference, see the [NVIDIA Spark playbook](https://build.nvidia.com/spark/nemoclaw)."
+ "notes": "Use the standard installer and `$$nemoclaw onboard`. For an end-to-end walkthrough with local inference, see the [NVIDIA Spark playbook](https://build.nvidia.com/spark/nemoclaw)."
},
{
"name": "Windows WSL2",
diff --git a/ci/test-file-size-budget.json b/ci/test-file-size-budget.json
index c3c785f74a4..5056f6c38f9 100644
--- a/ci/test-file-size-budget.json
+++ b/ci/test-file-size-budget.json
@@ -7,7 +7,7 @@
"src/lib/onboard/preflight.test.ts": 1905,
"test/channels-add-preset.test.ts": 1871,
"test/generate-openclaw-config.test.ts": 1984,
- "test/install-preflight.test.ts": 4207,
+ "test/install-preflight.test.ts": 4006,
"test/nemoclaw-start.test.ts": 5043,
"test/onboard-messaging.test.ts": 2062,
"test/onboard-selection.test.ts": 6888,
diff --git a/docs/get-started/prerequisites.mdx b/docs/get-started/prerequisites.mdx
index 3d9f9c97792..f26536ecb12 100644
--- a/docs/get-started/prerequisites.mdx
+++ b/docs/get-started/prerequisites.mdx
@@ -78,7 +78,7 @@ The table comes from [`ci/platform-matrix.json`](https://github.com/NVIDIA/NemoC
|----|-------------------|--------|-------|
| Linux | Docker | Tested | Primary tested path. Ubuntu 24.04 is the validated distro in production source (`DEFAULT_COMPAT_IMAGE` in `src/lib/onboard/docker-driver-gateway-launch.ts:10` and the preflight tests pin 24.04 only); the installer's package-manager probes assume apt-get. Other distros (Ubuntu 22.04, Fedora, Rocky, Alma, NixOS, Arch) may work but are not validated. |
| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | Start the container runtime (Colima or Docker Desktop) before running the installer. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight. |
-| DGX Spark | Docker | Tested | Use the standard installer and `$$nemoclaw onboard`. For an end-to-end walkthrough with local Ollama inference, see the [NVIDIA Spark playbook](https://build.nvidia.com/spark/nemoclaw). |
+| DGX Spark | Docker | Tested | Use the standard installer and `$$nemoclaw onboard`. For an end-to-end walkthrough with local inference, see the [NVIDIA Spark playbook](https://build.nvidia.com/spark/nemoclaw). |
| Windows WSL2 | Docker Desktop (WSL backend) | Tested with limitations | Requires WSL2 with Docker Desktop backend. |
{/* platform-matrix:end */}
diff --git a/docs/get-started/quickstart.mdx b/docs/get-started/quickstart.mdx
index d9f20248fb8..c2dab865895 100644
--- a/docs/get-started/quickstart.mdx
+++ b/docs/get-started/quickstart.mdx
@@ -75,6 +75,7 @@ curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash
On DGX Spark, DGX Station, and Windows WSL, an interactive installer offers express install after you accept the third-party software notice.
Express install switches onboarding to non-interactive mode, allows `sudo` password prompts for required host changes, and selects the managed local inference path for that platform.
+On DGX Spark and DGX Station, express install selects managed vLLM; DGX Spark uses `qwen3.6-35b-a3b-nvfp4` by default.
Unless `NEMOCLAW_POLICY_TIER` is set, it applies sandbox policy in `suggested` mode with the `balanced` tier by default, using the base sandbox policy plus supported package, model, web-search, local-inference, and read-only weather presets.
On DGX Spark, express install uses `my-spark-assistant` as the sandbox name unless `NEMOCLAW_SANDBOX_NAME` is already set.
On WSL, express install selects the Windows-host Ollama setup path.
diff --git a/docs/reference/platform-support.mdx b/docs/reference/platform-support.mdx
index 16fabdc1ea5..ad66a3a2a74 100644
--- a/docs/reference/platform-support.mdx
+++ b/docs/reference/platform-support.mdx
@@ -79,7 +79,7 @@ For the onboarding-time supported set without deferred rows, see [Prerequisites]
|----|-------------------|--------|--------------|----|-------|
| Linux | Docker | Tested | P0 | Yes | Primary tested path. Ubuntu 24.04 is the validated distro in production source (`DEFAULT_COMPAT_IMAGE` in `src/lib/onboard/docker-driver-gateway-launch.ts:10` and the preflight tests pin 24.04 only); the installer's package-manager probes assume apt-get. Other distros (Ubuntu 22.04, Fedora, Rocky, Alma, NixOS, Arch) may work but are not validated. |
| macOS (Apple Silicon) | Colima, Docker Desktop | Tested with limitations | P0 | Yes | Start the container runtime (Colima or Docker Desktop) before running the installer. Xcode Command Line Tools (`xcode-select --install`) are typically required for Node native modules during install. NemoClaw recommends them but does not enforce them during preflight. |
-| DGX Spark | Docker | Tested | P1 | Yes | Use the standard installer and `$$nemoclaw onboard`. For an end-to-end walkthrough with local Ollama inference, see the [NVIDIA Spark playbook](https://build.nvidia.com/spark/nemoclaw). |
+| DGX Spark | Docker | Tested | P1 | Yes | Use the standard installer and `$$nemoclaw onboard`. For an end-to-end walkthrough with local inference, see the [NVIDIA Spark playbook](https://build.nvidia.com/spark/nemoclaw). |
| Windows WSL2 | Docker Desktop (WSL backend) | Tested with limitations | P1 | No | Requires WSL2 with Docker Desktop backend. |
| DGX Station | Docker | Deferred | P1 | No | The PRD marks this platform as P1. Workstation form-factor with NVIDIA GPUs and the same Docker + NVIDIA Container Toolkit + CDI requirements as DGX Spark. Onboard path not yet validated end-to-end on the hardware; vLLM has a placeholder default model defined for this host class (`Qwen/Qwen3.6-27B-FP8`) that will move out of `deferred` once the hardware run is signed off. |
| NVIDIA RTX (consumer and Pro workstation GPUs) | Docker | Deferred | P1 | No | The PRD marks this platform as P1. Covers RTX consumer cards and RTX Pro workstation cards on Linux hosts that meet the generic-Linux-GPU requirements (NVIDIA Container Toolkit + CDI present). The provider menu emits managed vLLM behind `NEMOCLAW_EXPERIMENTAL=1` or `NEMOCLAW_PROVIDER=install-vllm` for this host class today; the end-to-end onboard path on this hardware is not yet validated in CI. |
diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx
index 6ceac31cc0d..a892e8803af 100644
--- a/docs/reference/troubleshooting.mdx
+++ b/docs/reference/troubleshooting.mdx
@@ -1332,7 +1332,7 @@ Use `--follow` to stream logs in real time while debugging.
## DGX Spark
-For an end-to-end Ollama walkthrough on DGX Spark, refer to the [NVIDIA Spark playbook](https://build.nvidia.com/spark/nemoclaw).
+For an end-to-end walkthrough with local inference on DGX Spark, refer to the [NVIDIA Spark playbook](https://build.nvidia.com/spark/nemoclaw).
### CoreDNS CrashLoop after onboarding
diff --git a/scripts/install.sh b/scripts/install.sh
index 7b75655197d..5ef720a9ca0 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -2429,17 +2429,24 @@ detect_express_platform() {
describe_express_install() {
local platform="$1"
local inference_summary=""
+ local inference_disclosure=""
local sandbox_summary=""
local tier="${NEMOCLAW_POLICY_TIER:-balanced}"
local policy_summary=""
case "$platform" in
"DGX Spark")
- inference_summary="managed local Ollama with model qwen3.6:35b"
+ if [ -n "${NEMOCLAW_VLLM_MODEL:-}" ]; then
+ inference_summary="managed local vLLM with model ${NEMOCLAW_VLLM_MODEL}"
+ else
+ inference_summary="managed local vLLM using the DGX Spark profile default model"
+ fi
+ inference_disclosure="Managed vLLM pulls the configured vLLM image/model and runs a local vLLM inference container."
sandbox_summary="${NEMOCLAW_SANDBOX_NAME:-my-spark-assistant}"
;;
"DGX Station")
inference_summary="managed local vLLM"
+ inference_disclosure="Managed vLLM pulls the configured vLLM image/model and runs a local vLLM inference container."
sandbox_summary="${NEMOCLAW_SANDBOX_NAME:-my-assistant}"
;;
"Windows WSL")
@@ -2471,6 +2478,9 @@ describe_express_install() {
esac
printf " Express install will configure %s.\n" "$inference_summary"
+ if [ -n "$inference_disclosure" ]; then
+ printf " %s\n" "$inference_disclosure"
+ fi
printf " Sandbox name: %s.\n" "$sandbox_summary"
printf " It runs onboarding non-interactively, but still prompts for sudo when host setup needs it.\n"
printf " Sandbox policy: suggested mode, tier '%s'. This uses the %s.\n" "$tier" "$policy_summary"
@@ -2532,8 +2542,10 @@ maybe_offer_express_install() {
case "$platform" in
"DGX Spark")
export NEMOCLAW_SANDBOX_NAME="${NEMOCLAW_SANDBOX_NAME:-my-spark-assistant}"
- export NEMOCLAW_PROVIDER=install-ollama
- export NEMOCLAW_MODEL=qwen3.6:35b
+ export NEMOCLAW_PROVIDER=install-vllm
+ if [ -n "${NEMOCLAW_VLLM_MODEL:-}" ]; then
+ export NEMOCLAW_VLLM_MODEL
+ fi
;;
"DGX Station")
export NEMOCLAW_PROVIDER=install-vllm
diff --git a/test/e2e-scenario/support-tests/e2e-live-project-config.test.ts b/test/e2e-scenario/support-tests/e2e-live-project-config.test.ts
index 0c9dc5acd8b..49050baeba0 100644
--- a/test/e2e-scenario/support-tests/e2e-live-project-config.test.ts
+++ b/test/e2e-scenario/support-tests/e2e-live-project-config.test.ts
@@ -27,6 +27,7 @@ interface RootConfig {
}
const INSTALLER_INTEGRATION_TESTS = [
+ "test/install-express-prompt.test.ts",
"test/install-preflight.test.ts",
"test/install-preflight-docker-bootstrap.test.ts",
"test/install-openshell-version-check.test.ts",
diff --git a/test/install-express-prompt.test.ts b/test/install-express-prompt.test.ts
new file mode 100644
index 00000000000..1606ddc5507
--- /dev/null
+++ b/test/install-express-prompt.test.ts
@@ -0,0 +1,233 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { spawnSync } from "node:child_process";
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+import { describe, expect, it } from "vitest";
+import { INSTALLER_PAYLOAD, TEST_SYSTEM_PATH } from "./helpers/installer-sourced-env";
+
+describe("installer express install prompt (sourced)", () => {
+ function runExpressPromptWithTty(
+ answer: string,
+ stdinMode: "pipe" | "tty",
+ platform = "DGX Spark",
+ extraEnv: Record = {},
+ ) {
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-express-prompt-"));
+ const python =
+ spawnSync("bash", ["--noprofile", "--norc", "-c", "command -v python3"], {
+ encoding: "utf-8",
+ }).stdout.trim() || "python3";
+ const ptyRunner = `
+import os
+import pty
+import select
+import signal
+import sys
+import time
+
+installer = sys.argv[1]
+answer = sys.argv[2].encode()
+stdin_mode = sys.argv[3]
+platform = sys.argv[4]
+script = r'''
+source "$INSTALLER_UNDER_TEST" >/dev/null
+detect_express_platform() { printf "$EXPRESS_PLATFORM"; }
+NON_INTERACTIVE=""
+NEMOCLAW_PROVIDER=""
+NEMOCLAW_NO_EXPRESS=""
+maybe_offer_express_install
+printf "RESULT NON_INTERACTIVE=%s SUDO_MODE=%s PROVIDER=%s MODEL=%s VLLM_MODEL=%s POLICY=%s YES=%s SANDBOX=%s\\n" \\
+ "\${NON_INTERACTIVE:-}" "\${NEMOCLAW_NON_INTERACTIVE_SUDO_MODE:-}" "\${NEMOCLAW_PROVIDER:-}" "\${NEMOCLAW_MODEL:-}" \\
+ "\${NEMOCLAW_VLLM_MODEL:-}" "\${NEMOCLAW_POLICY_MODE:-}" "\${NEMOCLAW_YES:-}" "\${NEMOCLAW_SANDBOX_NAME:-}"
+'''
+env = dict(os.environ)
+env["INSTALLER_UNDER_TEST"] = installer
+env["EXPRESS_PLATFORM"] = platform
+pid, fd = pty.fork()
+if pid == 0:
+ if stdin_mode == "pipe":
+ devnull = os.open(os.devnull, os.O_RDONLY)
+ os.dup2(devnull, 0)
+ os.close(devnull)
+ os.execvpe("bash", ["bash", "-c", script], env)
+
+output = bytearray()
+os.set_blocking(fd, False)
+sent = False
+exit_code = 124
+deadline = time.time() + 10
+while True:
+ ready, _, _ = select.select([fd], [], [], 0.1)
+ if ready:
+ try:
+ chunk = os.read(fd, 4096)
+ except BlockingIOError:
+ chunk = b""
+ except OSError:
+ chunk = b""
+ if chunk:
+ output.extend(chunk)
+ if (not sent) and b"[Y/n]" in output:
+ os.write(fd, answer)
+ sent = True
+ waited = os.waitpid(pid, os.WNOHANG)
+ if waited[0] == pid:
+ exit_code = os.waitstatus_to_exitcode(waited[1])
+ break
+ if time.time() > deadline:
+ try:
+ os.kill(pid, signal.SIGKILL)
+ except ProcessLookupError:
+ pass
+ try:
+ os.waitpid(pid, 0)
+ except ChildProcessError:
+ pass
+ break
+
+try:
+ os.close(fd)
+except OSError:
+ pass
+sys.stdout.buffer.write(output)
+sys.exit(exit_code)
+`;
+ return spawnSync(python, ["-c", ptyRunner, INSTALLER_PAYLOAD, answer, stdinMode, platform], {
+ cwd: tmp,
+ encoding: "utf-8",
+ timeout: 15_000,
+ killSignal: "SIGKILL",
+ env: {
+ HOME: tmp,
+ PATH: TEST_SYSTEM_PATH,
+ ...extraEnv,
+ },
+ });
+ }
+
+ it("offers express install when curl-piped stdin still has a controlling TTY", () => {
+ const result = runExpressPromptWithTty("y\n", "pipe");
+ const output = `${result.stdout}${result.stderr}`;
+ expect(result.status, output).toBe(0);
+ expect(output).toMatch(/Detected DGX Spark/);
+ expect(output).toMatch(
+ /Express install will configure managed local vLLM using the DGX Spark profile default model/,
+ );
+ expect(output).toMatch(
+ /Managed vLLM pulls the configured vLLM image\/model and runs a local vLLM inference container/,
+ );
+ expect(output).toMatch(/Sandbox name: my-spark-assistant/);
+ expect(output).toMatch(/Sandbox policy: suggested mode, tier 'balanced'/);
+ expect(output).toMatch(/Run express install/);
+ expect(output).toMatch(/Using express install for DGX Spark/);
+ expect(output).toMatch(
+ /RESULT NON_INTERACTIVE=1 SUDO_MODE=prompt PROVIDER=install-vllm MODEL= VLLM_MODEL= POLICY=suggested YES=1 SANDBOX=my-spark-assistant/,
+ );
+ });
+
+ it("preserves a preset Spark vLLM model in the prompt and exported env", () => {
+ const result = runExpressPromptWithTty("y\n", "pipe", "DGX Spark", {
+ NEMOCLAW_VLLM_MODEL: "custom-qwen3.6",
+ });
+ const output = `${result.stdout}${result.stderr}`;
+ expect(result.status, output).toBe(0);
+ expect(output).toMatch(/Detected DGX Spark/);
+ expect(output).toMatch(
+ /Express install will configure managed local vLLM with model custom-qwen3\.6/,
+ );
+ expect(output).toMatch(
+ /Managed vLLM pulls the configured vLLM image\/model and runs a local vLLM inference container/,
+ );
+ expect(output).toMatch(
+ /RESULT NON_INTERACTIVE=1 SUDO_MODE=prompt PROVIDER=install-vllm MODEL= VLLM_MODEL=custom-qwen3\.6 POLICY=suggested YES=1 SANDBOX=my-spark-assistant/,
+ );
+ });
+
+ it("detects Windows WSL as an express install platform", () => {
+ const result = spawnSync(
+ "bash",
+ [
+ "-c",
+ `
+source "$INSTALLER_UNDER_TEST" >/dev/null
+detect_express_platform
+`,
+ ],
+ {
+ cwd: path.join(import.meta.dirname, ".."),
+ encoding: "utf-8",
+ env: {
+ HOME: fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-express-wsl-detect-")),
+ PATH: TEST_SYSTEM_PATH,
+ INSTALLER_UNDER_TEST: INSTALLER_PAYLOAD,
+ WSL_DISTRO_NAME: "Ubuntu",
+ },
+ },
+ );
+
+ expect(result.status, `${result.stdout}${result.stderr}`).toBe(0);
+ expect(result.stdout).toBe("Windows WSL");
+ });
+
+ it("maps Windows WSL express install to Windows-host Ollama", () => {
+ const result = runExpressPromptWithTty("\n", "pipe", "Windows WSL");
+ const output = `${result.stdout}${result.stderr}`;
+ expect(result.status, output).toBe(0);
+ expect(output).toMatch(/Detected Windows WSL/);
+ expect(output).toMatch(
+ /Express install will configure Windows-host Ollama through host\.docker\.internal/,
+ );
+ expect(output).toMatch(/Sandbox policy: suggested mode, tier 'balanced'/);
+ expect(output).toMatch(/Run express install/);
+ expect(output).toMatch(/Using express install for Windows WSL/);
+ expect(output).toMatch(
+ /RESULT NON_INTERACTIVE=1 SUDO_MODE=prompt PROVIDER=install-windows-ollama MODEL= VLLM_MODEL= POLICY=suggested YES=1 SANDBOX=/,
+ );
+ });
+
+ it.skipIf(process.platform === "darwin")(
+ "skips express install without a controlling TTY",
+ () => {
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-express-no-tty-"));
+ const result = spawnSync(
+ "setsid",
+ [
+ "bash",
+ "-c",
+ `
+source "$INSTALLER_UNDER_TEST" >/dev/null
+detect_express_platform() { printf "DGX Spark"; }
+NON_INTERACTIVE=""
+NEMOCLAW_PROVIDER=""
+NEMOCLAW_NO_EXPRESS=""
+maybe_offer_express_install
+printf "RESULT NON_INTERACTIVE=%s SUDO_MODE=%s PROVIDER=%s MODEL=%s VLLM_MODEL=%s POLICY=%s YES=%s SANDBOX=%s\\n" \\
+ "\${NON_INTERACTIVE:-}" "\${NEMOCLAW_NON_INTERACTIVE_SUDO_MODE:-}" "\${NEMOCLAW_PROVIDER:-}" "\${NEMOCLAW_MODEL:-}" \\
+ "\${NEMOCLAW_VLLM_MODEL:-}" "\${NEMOCLAW_POLICY_MODE:-}" "\${NEMOCLAW_YES:-}" "\${NEMOCLAW_SANDBOX_NAME:-}"
+`,
+ ],
+ {
+ cwd: tmp,
+ encoding: "utf-8",
+ input: "",
+ env: {
+ HOME: tmp,
+ PATH: TEST_SYSTEM_PATH,
+ INSTALLER_UNDER_TEST: INSTALLER_PAYLOAD,
+ },
+ },
+ );
+ const output = `${result.stdout}${result.stderr}`;
+ expect(result.status, output).toBe(0);
+ expect(output).toMatch(/Detected DGX Spark/);
+ expect(output).toMatch(/Skipping express prompt \(no TTY\)/);
+ expect(output).not.toMatch(/Run express install/);
+ expect(output).toMatch(
+ /RESULT NON_INTERACTIVE= SUDO_MODE= PROVIDER= MODEL= VLLM_MODEL= POLICY= YES= SANDBOX=/,
+ );
+ },
+ );
+});
diff --git a/test/install-preflight.test.ts b/test/install-preflight.test.ts
index ab15bac406f..a2504274080 100644
--- a/test/install-preflight.test.ts
+++ b/test/install-preflight.test.ts
@@ -3174,207 +3174,6 @@ exit 0`,
});
});
-describe("installer express install prompt (sourced)", () => {
- function runExpressPromptWithTty(
- answer: string,
- stdinMode: "pipe" | "tty",
- platform = "DGX Spark",
- ) {
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-express-prompt-"));
- const python =
- spawnSync("bash", ["--noprofile", "--norc", "-c", "command -v python3"], {
- encoding: "utf-8",
- }).stdout.trim() || "python3";
- const ptyRunner = `
-import os
-import pty
-import select
-import signal
-import sys
-import time
-
-installer = sys.argv[1]
-answer = sys.argv[2].encode()
-stdin_mode = sys.argv[3]
-platform = sys.argv[4]
-script = r'''
-source "$INSTALLER_UNDER_TEST" >/dev/null
-detect_express_platform() { printf "$EXPRESS_PLATFORM"; }
-NON_INTERACTIVE=""
-NEMOCLAW_PROVIDER=""
-NEMOCLAW_NO_EXPRESS=""
-maybe_offer_express_install
-printf "RESULT NON_INTERACTIVE=%s SUDO_MODE=%s PROVIDER=%s MODEL=%s POLICY=%s YES=%s SANDBOX=%s\\n" \\
- "\${NON_INTERACTIVE:-}" "\${NEMOCLAW_NON_INTERACTIVE_SUDO_MODE:-}" "\${NEMOCLAW_PROVIDER:-}" "\${NEMOCLAW_MODEL:-}" \\
- "\${NEMOCLAW_POLICY_MODE:-}" "\${NEMOCLAW_YES:-}" "\${NEMOCLAW_SANDBOX_NAME:-}"
-'''
-env = dict(os.environ)
-env["INSTALLER_UNDER_TEST"] = installer
-env["EXPRESS_PLATFORM"] = platform
-pid, fd = pty.fork()
-if pid == 0:
- if stdin_mode == "pipe":
- devnull = os.open(os.devnull, os.O_RDONLY)
- os.dup2(devnull, 0)
- os.close(devnull)
- os.execvpe("bash", ["bash", "-c", script], env)
-
-output = bytearray()
-os.set_blocking(fd, False)
-sent = False
-exit_code = 124
-deadline = time.time() + 10
-while True:
- ready, _, _ = select.select([fd], [], [], 0.1)
- if ready:
- try:
- chunk = os.read(fd, 4096)
- except BlockingIOError:
- chunk = b""
- except OSError:
- chunk = b""
- if chunk:
- output.extend(chunk)
- if (not sent) and b"[Y/n]" in output:
- os.write(fd, answer)
- sent = True
- waited = os.waitpid(pid, os.WNOHANG)
- if waited[0] == pid:
- exit_code = os.waitstatus_to_exitcode(waited[1])
- break
- if time.time() > deadline:
- try:
- os.kill(pid, signal.SIGKILL)
- except ProcessLookupError:
- pass
- try:
- os.waitpid(pid, 0)
- except ChildProcessError:
- pass
- break
-
-try:
- os.close(fd)
-except OSError:
- pass
-sys.stdout.buffer.write(output)
-sys.exit(exit_code)
-`;
- return spawnSync(python, ["-c", ptyRunner, INSTALLER_PAYLOAD, answer, stdinMode, platform], {
- cwd: tmp,
- encoding: "utf-8",
- timeout: 15_000,
- killSignal: "SIGKILL",
- env: {
- HOME: tmp,
- PATH: TEST_SYSTEM_PATH,
- },
- });
- }
-
- it("offers express install when curl-piped stdin still has a controlling TTY", () => {
- const result = runExpressPromptWithTty("y\n", "pipe");
- const output = `${result.stdout}${result.stderr}`;
- expect(result.status, output).toBe(0);
- expect(output).toMatch(/Detected DGX Spark/);
- expect(output).toMatch(
- /Express install will configure managed local Ollama with model qwen3\.6:35b/,
- );
- expect(output).toMatch(/Sandbox name: my-spark-assistant/);
- expect(output).toMatch(/Sandbox policy: suggested mode, tier 'balanced'/);
- expect(output).toMatch(/Run express install/);
- expect(output).toMatch(/Using express install for DGX Spark/);
- expect(output).toMatch(
- /RESULT NON_INTERACTIVE=1 SUDO_MODE=prompt PROVIDER=install-ollama MODEL=qwen3\.6:35b POLICY=suggested YES=1 SANDBOX=my-spark-assistant/,
- );
- });
-
- it("detects Windows WSL as an express install platform", () => {
- const result = spawnSync(
- "bash",
- [
- "-c",
- `
-source "$INSTALLER_UNDER_TEST" >/dev/null
-detect_express_platform
-`,
- ],
- {
- cwd: path.join(import.meta.dirname, ".."),
- encoding: "utf-8",
- env: {
- HOME: fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-express-wsl-detect-")),
- PATH: TEST_SYSTEM_PATH,
- INSTALLER_UNDER_TEST: INSTALLER_PAYLOAD,
- WSL_DISTRO_NAME: "Ubuntu",
- },
- },
- );
-
- expect(result.status, `${result.stdout}${result.stderr}`).toBe(0);
- expect(result.stdout).toBe("Windows WSL");
- });
-
- it("maps Windows WSL express install to Windows-host Ollama", () => {
- const result = runExpressPromptWithTty("\n", "pipe", "Windows WSL");
- const output = `${result.stdout}${result.stderr}`;
- expect(result.status, output).toBe(0);
- expect(output).toMatch(/Detected Windows WSL/);
- expect(output).toMatch(
- /Express install will configure Windows-host Ollama through host\.docker\.internal/,
- );
- expect(output).toMatch(/Sandbox policy: suggested mode, tier 'balanced'/);
- expect(output).toMatch(/Run express install/);
- expect(output).toMatch(/Using express install for Windows WSL/);
- expect(output).toMatch(
- /RESULT NON_INTERACTIVE=1 SUDO_MODE=prompt PROVIDER=install-windows-ollama MODEL= POLICY=suggested YES=1 SANDBOX=/,
- );
- });
-
- it("skips express install without a controlling TTY", () => {
- if (process.platform === "darwin") {
- return;
- }
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-express-no-tty-"));
- const result = spawnSync(
- "setsid",
- [
- "bash",
- "-c",
- `
-source "$INSTALLER_UNDER_TEST" >/dev/null
-detect_express_platform() { printf "DGX Spark"; }
-NON_INTERACTIVE=""
-NEMOCLAW_PROVIDER=""
-NEMOCLAW_NO_EXPRESS=""
-maybe_offer_express_install
-printf "RESULT NON_INTERACTIVE=%s SUDO_MODE=%s PROVIDER=%s MODEL=%s POLICY=%s YES=%s SANDBOX=%s\\n" \\
- "\${NON_INTERACTIVE:-}" "\${NEMOCLAW_NON_INTERACTIVE_SUDO_MODE:-}" "\${NEMOCLAW_PROVIDER:-}" "\${NEMOCLAW_MODEL:-}" \\
- "\${NEMOCLAW_POLICY_MODE:-}" "\${NEMOCLAW_YES:-}" "\${NEMOCLAW_SANDBOX_NAME:-}"
-`,
- ],
- {
- cwd: tmp,
- encoding: "utf-8",
- input: "",
- env: {
- HOME: tmp,
- PATH: TEST_SYSTEM_PATH,
- INSTALLER_UNDER_TEST: INSTALLER_PAYLOAD,
- },
- },
- );
- const output = `${result.stdout}${result.stderr}`;
- expect(result.status, output).toBe(0);
- expect(output).toMatch(/Detected DGX Spark/);
- expect(output).toMatch(/Skipping express prompt \(no TTY\)/);
- expect(output).not.toMatch(/Run express install/);
- expect(output).toMatch(
- /RESULT NON_INTERACTIVE= SUDO_MODE= PROVIDER= MODEL= POLICY= YES= SANDBOX=/,
- );
- });
-});
-
// ---------------------------------------------------------------------------
// scripts/install.sh (curl-pipe installer) release-tag resolution
// ---------------------------------------------------------------------------
diff --git a/test/pr-workflow-contract.test.ts b/test/pr-workflow-contract.test.ts
index 12327c81228..ab05afac934 100644
--- a/test/pr-workflow-contract.test.ts
+++ b/test/pr-workflow-contract.test.ts
@@ -459,6 +459,7 @@ describe("pull request and main workflow contracts", () => {
expect(vitestConfig).toContain('name: "e2e-vitest-support"');
expect(vitestConfig).toContain('include: ["test/**/*.test.{js,ts}", "src/**/*.test.ts"]');
expect(vitestConfig).toContain('"test/e2e/**"');
+ expect(vitestConfig).toContain('"test/install-express-prompt.test.ts"');
expect(vitestConfig).toContain('"test/install-preflight.test.ts"');
expect(vitestConfig).toContain('"test/install-openshell-version-check.test.ts"');
});
diff --git a/vitest.config.ts b/vitest.config.ts
index e463c496fd7..6f6b819ad2f 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -43,6 +43,7 @@ export default defineConfig({
// and require Docker + a real onboard to pass. Excluding here keeps
// the cli project (and pre-commit `Test (cli)`) green locally.
"test/e2e-scenario/live/**",
+ "test/install-express-prompt.test.ts",
"test/install-preflight.test.ts",
"test/install-preflight-docker-bootstrap.test.ts",
"test/install-openshell-version-check.test.ts",
@@ -54,6 +55,7 @@ export default defineConfig({
name: "installer-integration",
include: runInstallerIntegration
? [
+ "test/install-express-prompt.test.ts",
"test/install-preflight.test.ts",
"test/install-preflight-docker-bootstrap.test.ts",
"test/install-openshell-version-check.test.ts",