diff --git a/docs/get-started/windows-preparation.mdx b/docs/get-started/windows-preparation.mdx index 721b610a93c..2ad8cd37e19 100644 --- a/docs/get-started/windows-preparation.mdx +++ b/docs/get-started/windows-preparation.mdx @@ -168,9 +168,11 @@ You can also start it yourself beforehand with `ollama serve`. You can also use Ollama for Windows. During onboarding, NemoClaw can use an already-running Windows-host daemon, start or restart an installed daemon, or install Ollama on the Windows host. -If the installer offers express install on WSL, accepting it selects this Windows-host Ollama path automatically. -The Windows-host Ollama path requires Docker Desktop WSL integration, and the express prompt appears on WSL regardless of the container runtime. -With native Docker Engine inside WSL, decline the express prompt (or set `NEMOCLAW_NO_EXPRESS=1`) and select the WSL Ollama instance during onboarding; the menu labels the Windows-host actions as requiring Docker Desktop integration. +If the installer offers express install on WSL, accepting it selects a local Ollama path automatically based on the container runtime: the Windows-host Ollama path only when it detects Docker Desktop WSL integration, and WSL-local Ollama otherwise. +The Windows-host Ollama path requires Docker Desktop WSL integration; the express prompt appears on WSL regardless of the container runtime. +With native Docker Engine inside WSL, or when the Docker runtime is unavailable or cannot be probed, express install configures WSL-local Ollama instead of aborting. +When containers cannot reach host loopback, onboarding fronts that WSL-local daemon with the sandbox auth proxy. +You can still decline the express prompt (or set `NEMOCLAW_NO_EXPRESS=1`) to choose a provider manually; the onboarding menu labels the Windows-host actions as requiring Docker Desktop integration. When Ollama runs on the Windows host, NemoClaw detects it from WSL through `host.docker.internal` and pulls missing models through the Ollama HTTP API. Do not run both the Windows and WSL Ollama instances on port `11434` at the same time. Use one instance, or move one of them to a different port before running `$$nemoclaw onboard`. diff --git a/scripts/checks/vitest-project-overlap.mts b/scripts/checks/vitest-project-overlap.mts index e4af1a8239e..7e07ea9b9eb 100644 --- a/scripts/checks/vitest-project-overlap.mts +++ b/scripts/checks/vitest-project-overlap.mts @@ -43,6 +43,7 @@ const INSTALLER_INTEGRATION_TESTS = new Set([ "test/install-build-dependency-preflight.test.ts", "test/install-clone-ref.test.ts", "test/install-express-prompt.test.ts", + "test/install-express-wsl-ollama.test.ts", "test/install-openshell-version-check.test.ts", "test/install-preflight-docker-bootstrap.test.ts", "test/install-preflight.test.ts", diff --git a/scripts/install.sh b/scripts/install.sh index 2d9ceff16fb..6271f3b60e0 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -3639,6 +3639,88 @@ clear_station_express_resume() { done } +# Report the container runtime's operating-system string (e.g. "Docker Desktop" +# or "Ubuntu 24.04.4 LTS"). Bounded with a hard timeout so a wedged, +# misconfigured, or dead-DOCKER_HOST daemon cannot hang the interactive express +# prompt: this runs from describe_express_install before ensure_docker, and WSL +# skips ensure_docker entirely. Empty on timeout or error. +express_wsl_docker_operating_system() { + timeout 10 docker info --format '{{.OperatingSystem}}' 2>/dev/null +} + +# Resolve Docker's effective context name: the DOCKER_CONTEXT override if set, +# otherwise the persisted currentContext from Docker's config (what +# `docker context use` writes). A missing config or a config with no +# currentContext uses Docker's "default"; an unreadable or unparseable config +# fails closed as non-local. +express_wsl_docker_active_context() { + if [ -n "${DOCKER_CONTEXT:-}" ]; then + printf '%s' "${DOCKER_CONTEXT}" + return 0 + fi + local cfg="${DOCKER_CONFIG:-${HOME:-}/.docker}/config.json" + local ctx="" parse_status=0 + if [ ! -e "$cfg" ]; then + printf '%s' "default" + return 0 + fi + if [ -e "$cfg" ] && [ ! -r "$cfg" ]; then + printf '%s' "__unknown__" + return 0 + fi + if command -v node >/dev/null 2>&1; then + ctx="$( + node - "$cfg" <<'NODE' +const fs = require("node:fs"); + +let config; +try { + config = JSON.parse(fs.readFileSync(process.argv[2], "utf8")); +} catch { + process.exit(1); +} +if (config === null || typeof config !== "object" || Array.isArray(config)) process.exit(1); +if (!Object.prototype.hasOwnProperty.call(config, "currentContext")) process.exit(2); +if (typeof config.currentContext !== "string") process.exit(1); +process.stdout.write(config.currentContext); +NODE + )" || parse_status=$? + if [ "$parse_status" -eq 0 ]; then + printf '%s' "$ctx" + return 0 + fi + if [ "$parse_status" -eq 2 ]; then + printf '%s' "default" + return 0 + fi + printf '%s' "__unknown__" + return 0 + fi + printf '%s' "__unknown__" +} + +# True only when the Docker CLI targets the LOCAL default daemon: the active +# context is "default" and no DOCKER_HOST override is set. A DOCKER_HOST, a +# DOCKER_CONTEXT override, or a persisted currentContext other than "default" +# (a context name like desktop-linux is not proof of a local endpoint — it can be +# pointed at a remote daemon) can reach a remote Docker Desktop whose sandbox +# containers cannot reach this machine's Windows-host Ollama (PRA-1). Fails closed +# (non-local) on any non-default, unreadable, or unparseable context. +express_wsl_docker_target_is_local() { + [ -z "${DOCKER_HOST:-}" ] || return 1 + [ "$(express_wsl_docker_active_context)" = "default" ] +} + +# Windows-host Ollama only works through LOCAL Docker Desktop WSL integration +# (host.docker.internal routes to the Windows host). Native Docker Engine (#3695), +# a remote/unknown target, or a failed probe can't reach it, so use WSL-local +# Ollama instead; the onboard provider setup fronts that loopback daemon with +# the sandbox auth proxy when containers cannot reach host loopback (#7318). +express_wsl_can_use_windows_host_ollama() { + express_wsl_docker_target_is_local || return 1 + express_wsl_docker_operating_system | grep -qi 'docker desktop' +} + activate_express_install() { local platform="$1" _SELECTED_EXPRESS_PLATFORM="$platform" @@ -3668,7 +3750,11 @@ activate_express_install() { configure_station_express_model ;; "Windows WSL") - export NEMOCLAW_PROVIDER=install-windows-ollama + if express_wsl_can_use_windows_host_ollama; then + export NEMOCLAW_PROVIDER=install-windows-ollama + else + export NEMOCLAW_PROVIDER=install-ollama + fi ;; esac } @@ -3839,7 +3925,11 @@ describe_express_install() { sandbox_summary="${NEMOCLAW_SANDBOX_NAME:-my-assistant}" ;; "Windows WSL") - inference_summary="Windows-host Ollama through host.docker.internal" + if express_wsl_can_use_windows_host_ollama; then + inference_summary="Windows-host Ollama through host.docker.internal" + else + inference_summary="WSL-local Ollama, with a sandbox auth proxy when containers cannot reach host loopback" + fi sandbox_summary="${NEMOCLAW_SANDBOX_NAME:-my-assistant}" ;; *) diff --git a/src/lib/onboard/setup-nim-ollama.test.ts b/src/lib/onboard/setup-nim-ollama.test.ts index 856d5296ebc..3bd822383d6 100644 --- a/src/lib/onboard/setup-nim-ollama.test.ts +++ b/src/lib/onboard/setup-nim-ollama.test.ts @@ -341,6 +341,46 @@ describe("createSetupNimOllamaHandlers", () => { assert.equal(state.allowToolsIncompatible, true); }); + it("fronts installed WSL-local Ollama with the sandbox proxy when host loopback is not container-reachable (#7318)", async () => { + const state = makeState(); + const install = vi.fn(() => ({ ok: true })); + const startProxy = vi.fn(() => true); + const selectModel = vi.fn(async () => ({ + outcome: "selected", + model: "qwen3:0.6b", + allowToolsIncompatible: false, + })); + const log = vi.spyOn(console, "log").mockImplementation(() => {}); + const { handleInstallOllamaSelection } = createSetupNimOllamaHandlers( + makeDeps({ + process: { ...process, platform: "linux" } as NodeJS.Process, + installOllamaOnLinux: install, + shouldFrontOllamaWithProxy: () => true, + startOllamaAuthProxy: startProxy, + getLocalProviderBaseUrl: () => "http://host.openshell.internal:11435/v1", + selectAndValidateOllamaModel: selectModel, + }), + ); + + const result = await handleInstallOllamaSelection(null, null, null, state, { + hasUpgradableOllama: false, + }); + + expect(result).toBe("selected"); + expect(install).toHaveBeenCalledTimes(1); + expect(startProxy).toHaveBeenCalledTimes(1); + expect(state).toMatchObject({ + provider: "ollama-local", + endpointUrl: "http://host.openshell.internal:11435/v1", + model: "qwen3:0.6b", + credentialEnv: null, + preferredInferenceApi: "openai-completions", + }); + expect(state.endpointUrl).not.toContain("127.0.0.1"); + expect(log).toHaveBeenCalledWith(" ✓ Using Ollama on localhost:11434 (proxy on :11435)"); + log.mockRestore(); + }); + it("fails closed on unknown Ollama startup outcomes without mutating state", async () => { const state = makeState(); const before = { ...state, hermesToolGateways: [...state.hermesToolGateways] }; diff --git a/test/install-express-prompt.test.ts b/test/install-express-prompt.test.ts index 8b6a1c92974..28ec0495a32 100644 --- a/test/install-express-prompt.test.ts +++ b/test/install-express-prompt.test.ts @@ -1430,22 +1430,6 @@ printf 'PROMPT_REACHED\n' expect(output).not.toContain("PROMPT_REACHED"); }); - 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", () => { diff --git a/test/install-express-wsl-ollama.test.ts b/test/install-express-wsl-ollama.test.ts new file mode 100644 index 00000000000..b169db3f686 --- /dev/null +++ b/test/install-express-wsl-ollama.test.ts @@ -0,0 +1,325 @@ +// 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, + writeExecutable, +} from "./helpers/installer-sourced-env"; + +describe("installer Windows WSL express Ollama selection (sourced)", () => { + function runInstallerSourced(body: string, extraEnv: Record = {}) { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-express-wsl-sourced-")); + const result = spawnSync( + "bash", + ["--noprofile", "--norc", "-c", `source "$INSTALLER_UNDER_TEST" >/dev/null\n${body}`], + { + cwd: path.resolve(import.meta.dirname, ".."), + encoding: "utf-8", + env: { + HOME: home, + PATH: TEST_SYSTEM_PATH, + INSTALLER_UNDER_TEST: INSTALLER_PAYLOAD, + ...extraEnv, + }, + }, + ); + return { home, result, output: `${result.stdout}${result.stderr}` }; + } + + function dockerStubBin(operatingSystem: string, exitCode = 0) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-docker-stub-")); + writeExecutable(path.join(dir, "timeout"), '#!/usr/bin/env bash\nshift\nexec "$@"\n'); + writeExecutable( + path.join(dir, "docker"), + `#!/usr/bin/env bash\nif [ "$1" = "info" ]; then\n printf '%s\\n' "${operatingSystem}"\nfi\nexit ${exitCode}\n`, + ); + return dir; + } + + function runWslExpressPrompt(extraEnv: Record) { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-express-wsl-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] +script = r''' +source "$INSTALLER_UNDER_TEST" >/dev/null +detect_express_platform() { printf "Windows WSL"; } +NON_INTERACTIVE="\${NON_INTERACTIVE:-}" +NEMOCLAW_PROVIDER="\${NEMOCLAW_PROVIDER:-}" +NEMOCLAW_NO_EXPRESS="\${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 +pid, fd = pty.fork() +if pid == 0: + devnull = os.open(os.devnull, os.O_RDONLY) + os.dup2(devnull, 0) + os.close(devnull) + os.execvpe("bash", ["bash", "-c", script, "nemoclaw-express-wsl-prompt"], 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, b"\\n") + 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], { + cwd: tmp, + encoding: "utf-8", + timeout: 15_000, + killSignal: "SIGKILL", + env: { + HOME: tmp, + PATH: TEST_SYSTEM_PATH, + ...extraEnv, + }, + }); + } + + it("maps Windows WSL express install to Windows-host Ollama under Docker Desktop", () => { + const dockerBin = dockerStubBin("Docker Desktop"); + const result = runWslExpressPrompt({ PATH: `${dockerBin}:${TEST_SYSTEM_PATH}` }); + 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("maps Windows WSL express install to WSL-local Ollama under native Docker Engine", () => { + const dockerBin = dockerStubBin("Ubuntu 24.04.4 LTS"); + const result = runWslExpressPrompt({ PATH: `${dockerBin}:${TEST_SYSTEM_PATH}` }); + 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 WSL-local Ollama, with a sandbox auth proxy when containers cannot reach host loopback/, + ); + expect(output).not.toMatch(/native Docker Engine detected/); + expect(output).toMatch(/Using express install for Windows WSL/); + expect(output).toMatch( + /RESULT NON_INTERACTIVE=1 SUDO_MODE=prompt PROVIDER=install-ollama MODEL= VLLM_MODEL= POLICY=suggested YES=1 SANDBOX=/, + ); + }); + + it("uses a runtime-neutral WSL-local Ollama summary when the docker probe fails", () => { + const dockerBin = dockerStubBin("", 1); + const result = runWslExpressPrompt({ PATH: `${dockerBin}:${TEST_SYSTEM_PATH}` }); + const output = `${result.stdout}${result.stderr}`; + expect(result.status, output).toBe(0); + expect(output).toMatch( + /Express install will configure WSL-local Ollama, with a sandbox auth proxy when containers cannot reach host loopback/, + ); + expect(output).not.toMatch(/native Docker Engine detected/); + expect(output).toMatch( + /RESULT NON_INTERACTIVE=1 SUDO_MODE=prompt PROVIDER=install-ollama MODEL= VLLM_MODEL= POLICY=suggested YES=1 SANDBOX=/, + ); + }); + + it("activate_express_install keeps Windows-host Ollama when Docker Desktop is detected", () => { + const { result, output } = runInstallerSourced( + `express_wsl_docker_operating_system() { printf 'Docker Desktop\\n'; }\n` + + `activate_express_install "Windows WSL"\n` + + `printf 'PROVIDER=%s\\n' "$NEMOCLAW_PROVIDER"\n`, + ); + expect(result.status, output).toBe(0); + expect(output).toContain("PROVIDER=install-windows-ollama"); + }); + + it("activate_express_install falls back to WSL-local Ollama under native Docker Engine", () => { + const { result, output } = runInstallerSourced( + `express_wsl_docker_operating_system() { printf 'Ubuntu 24.04.4 LTS\\n'; }\n` + + `activate_express_install "Windows WSL"\n` + + `printf 'PROVIDER=%s\\n' "$NEMOCLAW_PROVIDER"\n`, + ); + expect(result.status, output).toBe(0); + expect(output).toContain("PROVIDER=install-ollama"); + }); + + it("activate_express_install falls back to WSL-local Ollama when the docker probe fails or times out", () => { + const { result, output } = runInstallerSourced( + `express_wsl_docker_operating_system() { return 124; }\n` + + `activate_express_install "Windows WSL"\n` + + `printf 'PROVIDER=%s\\n' "$NEMOCLAW_PROVIDER"\n`, + ); + expect(result.status, output).toBe(0); + expect(output).toContain("PROVIDER=install-ollama"); + }); + + it("activate_express_install rejects a remote Docker Desktop target via DOCKER_HOST", () => { + const { result, output } = runInstallerSourced( + `export DOCKER_HOST=tcp://10.0.0.5:2375\n` + + `express_wsl_docker_operating_system() { printf 'Docker Desktop\\n'; }\n` + + `activate_express_install "Windows WSL"\n` + + `printf 'PROVIDER=%s\\n' "$NEMOCLAW_PROVIDER"\n`, + ); + expect(result.status, output).toBe(0); + expect(output).toContain("PROVIDER=install-ollama"); + }); + + it("activate_express_install rejects a remote Docker Desktop target via DOCKER_CONTEXT", () => { + const { result, output } = runInstallerSourced( + `export DOCKER_CONTEXT=my-remote\n` + + `express_wsl_docker_operating_system() { printf 'Docker Desktop\\n'; }\n` + + `activate_express_install "Windows WSL"\n` + + `printf 'PROVIDER=%s\\n' "$NEMOCLAW_PROVIDER"\n`, + ); + expect(result.status, output).toBe(0); + expect(output).toContain("PROVIDER=install-ollama"); + }); + + it("activate_express_install does not trust a desktop-linux context name as local", () => { + const { result, output } = runInstallerSourced( + `export DOCKER_CONTEXT=desktop-linux\n` + + `express_wsl_docker_operating_system() { printf 'Docker Desktop\\n'; }\n` + + `activate_express_install "Windows WSL"\n` + + `printf 'PROVIDER=%s\\n' "$NEMOCLAW_PROVIDER"\n`, + ); + expect(result.status, output).toBe(0); + expect(output).toContain("PROVIDER=install-ollama"); + }); + + it("activate_express_install fails closed on a persisted remote currentContext", () => { + const { result, output } = runInstallerSourced( + `mkdir -p "$HOME/.docker"\n` + + `printf '%s' '{"currentContext":"remote-prod"}' > "$HOME/.docker/config.json"\n` + + `express_wsl_docker_operating_system() { printf 'Docker Desktop\\n'; }\n` + + `activate_express_install "Windows WSL"\n` + + `printf 'PROVIDER=%s\\n' "$NEMOCLAW_PROVIDER"\n`, + ); + expect(result.status, output).toBe(0); + expect(output).toContain("PROVIDER=install-ollama"); + }); + + it("activate_express_install fails closed on a multiline persisted remote currentContext", () => { + const { result, output } = runInstallerSourced( + `mkdir -p "$HOME/.docker"\n` + + `cat > "$HOME/.docker/config.json" <<'JSON'\n` + + `{\n` + + ` "auths": {},\n` + + ` "currentContext":\n` + + ` "remote-prod"\n` + + `}\n` + + `JSON\n` + + `express_wsl_docker_operating_system() { printf 'Docker Desktop\\n'; }\n` + + `activate_express_install "Windows WSL"\n` + + `printf 'PROVIDER=%s\\n' "$NEMOCLAW_PROVIDER"\n`, + { PATH: `${path.dirname(process.execPath)}:${TEST_SYSTEM_PATH}` }, + ); + expect(result.status, output).toBe(0); + expect(output).toContain("PROVIDER=install-ollama"); + }); + + it("activate_express_install fails closed when a readable Docker config cannot be parsed", () => { + const { result, output } = runInstallerSourced( + `mkdir -p "$HOME/.docker"\n` + + `printf '%s' '{"currentContext":"default"' > "$HOME/.docker/config.json"\n` + + `express_wsl_docker_operating_system() { printf 'Docker Desktop\\n'; }\n` + + `activate_express_install "Windows WSL"\n` + + `printf 'PROVIDER=%s\\n' "$NEMOCLAW_PROVIDER"\n`, + { PATH: `${path.dirname(process.execPath)}:${TEST_SYSTEM_PATH}` }, + ); + expect(result.status, output).toBe(0); + expect(output).toContain("PROVIDER=install-ollama"); + }); + + it("activate_express_install fails closed on malformed Docker config when Node is unavailable", () => { + const { result, output } = runInstallerSourced( + `mkdir -p "$HOME/.docker"\n` + + `printf '%s' 'not-json {"currentContext":"default"}' > "$HOME/.docker/config.json"\n` + + `express_wsl_docker_operating_system() { printf 'Docker Desktop\\n'; }\n` + + `activate_express_install "Windows WSL"\n` + + `printf 'PROVIDER=%s\\n' "$NEMOCLAW_PROVIDER"\n`, + ); + expect(result.status, output).toBe(0); + expect(output).toContain("PROVIDER=install-ollama"); + }); + + it("activate_express_install fails closed on an unreadable Docker config", () => { + const { result, output } = runInstallerSourced( + `mkdir -p "$HOME/.docker"\n` + + `printf '%s' '{"currentContext":"remote-prod"}' > "$HOME/.docker/config.json"\n` + + `chmod 000 "$HOME/.docker/config.json"\n` + + `express_wsl_docker_operating_system() { printf 'Docker Desktop\\n'; }\n` + + `activate_express_install "Windows WSL"\n` + + `printf 'PROVIDER=%s\\n' "$NEMOCLAW_PROVIDER"\n`, + ); + expect(result.status, output).toBe(0); + expect(output).toContain("PROVIDER=install-ollama"); + }); + + it("activate_express_install treats a default persisted currentContext as local", () => { + const { result, output } = runInstallerSourced( + `mkdir -p "$HOME/.docker"\n` + + `printf '%s' '{"currentContext":"default"}' > "$HOME/.docker/config.json"\n` + + `express_wsl_docker_operating_system() { printf 'Docker Desktop\\n'; }\n` + + `activate_express_install "Windows WSL"\n` + + `printf 'PROVIDER=%s\\n' "$NEMOCLAW_PROVIDER"\n`, + { PATH: `${path.dirname(process.execPath)}:${TEST_SYSTEM_PATH}` }, + ); + expect(result.status, output).toBe(0); + expect(output).toContain("PROVIDER=install-windows-ollama"); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 5354910a3e6..a30bcb63cb1 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -134,6 +134,7 @@ export default defineConfig({ "test/e2e/support/**", "test/package-contract/**", "test/install-express-prompt.test.ts", + "test/install-express-wsl-ollama.test.ts", "test/install-station-vllm-continuation.test.ts", "test/install-build-dependency-preflight.test.ts", "test/install-clone-ref.test.ts", @@ -158,6 +159,7 @@ export default defineConfig({ setupFiles: [fixtureUmaskSetup], include: [ "test/install-express-prompt.test.ts", + "test/install-express-wsl-ollama.test.ts", "test/install-station-vllm-continuation.test.ts", "test/install-build-dependency-preflight.test.ts", "test/install-clone-ref.test.ts",