diff --git a/.github/workflows/commit-lint.yaml b/.github/workflows/commit-lint.yaml index b1d29db4..03d259ea 100644 --- a/.github/workflows/commit-lint.yaml +++ b/.github/workflows/commit-lint.yaml @@ -31,7 +31,7 @@ jobs: cache: npm - name: Install dependencies - run: npm ci --ignore-scripts + run: npm install --ignore-scripts - name: Lint commits if: github.event.action != 'edited' diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 95c40cac..732bfece 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -69,7 +69,7 @@ jobs: npm run build - name: Run root unit tests - run: node --test --experimental-test-coverage test/*.test.js + run: npx vitest run - name: Run TypeScript unit tests with coverage working-directory: nemoclaw diff --git a/package.json b/package.json index 274f57dc..aacdee31 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "nemoclaw": "./bin/nemoclaw.js" }, "scripts": { - "test": "node --test test/*.test.js", + "test": "vitest run", "prepare": "husky || true", "prepublishOnly": "cd nemoclaw && env -u npm_config_global -u npm_config_prefix -u npm_config_omit npm install --ignore-scripts && ./node_modules/.bin/tsc" }, @@ -53,6 +53,7 @@ "@commitlint/config-conventional": "^20.5.0", "husky": "^9.1.7", "lint-staged": "^16.4.0", - "shellcheck": "^4.1.0" + "shellcheck": "^4.1.0", + "vitest": "^3.1.1" } } diff --git a/test/cli.test.js b/test/cli.test.js index 3b0bfaf0..0fc28064 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -const { describe, it } = require("node:test"); -const assert = require("node:assert/strict"); const { execSync } = require("child_process"); const path = require("path"); @@ -24,71 +22,71 @@ function run(args) { describe("CLI dispatch", () => { it("help exits 0 and shows sections", () => { const r = run("help"); - assert.equal(r.code, 0); - assert.ok(r.out.includes("Getting Started"), "missing Getting Started section"); - assert.ok(r.out.includes("Sandbox Management"), "missing Sandbox Management section"); - assert.ok(r.out.includes("Policy Presets"), "missing Policy Presets section"); + expect(r.code).toBe(0); + expect(r.out.includes("Getting Started")).toBeTruthy(); + expect(r.out.includes("Sandbox Management")).toBeTruthy(); + expect(r.out.includes("Policy Presets")).toBeTruthy(); }); it("--help exits 0", () => { - assert.equal(run("--help").code, 0); + expect(run("--help").code).toBe(0); }); it("-h exits 0", () => { - assert.equal(run("-h").code, 0); + expect(run("-h").code).toBe(0); }); it("no args exits 0 (shows help)", () => { const r = run(""); - assert.equal(r.code, 0); - assert.ok(r.out.includes("nemoclaw")); + expect(r.code).toBe(0); + expect(r.out.includes("nemoclaw")).toBeTruthy(); }); it("unknown command exits 1", () => { const r = run("boguscmd"); - assert.equal(r.code, 1); - assert.ok(r.out.includes("Unknown command")); + expect(r.code).toBe(1); + expect(r.out.includes("Unknown command")).toBeTruthy(); }); it("list exits 0", () => { const r = run("list"); - assert.equal(r.code, 0); + expect(r.code).toBe(0); // With empty HOME, should say no sandboxes - assert.ok(r.out.includes("No sandboxes")); + expect(r.out.includes("No sandboxes")).toBeTruthy(); }); it("unknown onboard option exits 1", () => { const r = run("onboard --non-interactiv"); - assert.equal(r.code, 1); - assert.ok(r.out.includes("Unknown onboard option")); + expect(r.code).toBe(1); + expect(r.out.includes("Unknown onboard option")).toBeTruthy(); }); it("debug --help exits 0 and shows usage", () => { const r = run("debug --help"); - assert.equal(r.code, 0); - assert.ok(r.out.includes("Collect NemoClaw diagnostic information"), "should show description"); - assert.ok(r.out.includes("--quick"), "should mention --quick flag"); - assert.ok(r.out.includes("--output"), "should mention --output flag"); + expect(r.code).toBe(0); + expect(r.out.includes("Collect NemoClaw diagnostic information")).toBeTruthy(); + expect(r.out.includes("--quick")).toBeTruthy(); + expect(r.out.includes("--output")).toBeTruthy(); }); it("debug --quick exits 0 and produces diagnostic output", () => { const r = run("debug --quick"); - assert.equal(r.code, 0, "debug --quick should exit 0"); - assert.ok(r.out.includes("Collecting diagnostics"), "should show collection header"); - assert.ok(r.out.includes("System"), "should include System section"); - assert.ok(r.out.includes("Done"), "should show completion message"); + expect(r.code).toBe(0); + expect(r.out.includes("Collecting diagnostics")).toBeTruthy(); + expect(r.out.includes("System")).toBeTruthy(); + expect(r.out.includes("Done")).toBeTruthy(); }); it("debug exits 1 on unknown option", () => { const r = run("debug --quik"); - assert.equal(r.code, 1, "misspelled flag should exit non-zero"); - assert.ok(r.out.includes("Unknown option"), "should report unknown option"); + expect(r.code).toBe(1); + expect(r.out.includes("Unknown option")).toBeTruthy(); }); it("help mentions debug command", () => { const r = run("help"); - assert.equal(r.code, 0); - assert.ok(r.out.includes("Troubleshooting"), "missing Troubleshooting section"); - assert.ok(r.out.includes("nemoclaw debug"), "help should mention debug command"); + expect(r.code).toBe(0); + expect(r.out.includes("Troubleshooting")).toBeTruthy(); + expect(r.out.includes("nemoclaw debug")).toBeTruthy(); }); }); diff --git a/test/credentials.test.js b/test/credentials.test.js index 08ce5d79..830135ae 100644 --- a/test/credentials.test.js +++ b/test/credentials.test.js @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -const { describe, it } = require("node:test"); -const assert = require("node:assert/strict"); const path = require("node:path"); const { spawnSync } = require("node:child_process"); @@ -27,6 +25,6 @@ describe("credential prompts", () => { timeout: 5000, }); - assert.equal(result.status, 0); + expect(result.status).toBe(0); }); }); diff --git a/test/inference-config.test.js b/test/inference-config.test.js index 2a1a47b8..5ace8645 100644 --- a/test/inference-config.test.js +++ b/test/inference-config.test.js @@ -1,9 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -const { describe, it } = require("node:test"); -const assert = require("node:assert/strict"); - const { CLOUD_MODEL_OPTIONS, DEFAULT_OLLAMA_MODEL, @@ -17,21 +14,18 @@ const { describe("inference selection config", () => { it("exposes the curated cloud model picker options", () => { - assert.deepEqual( - CLOUD_MODEL_OPTIONS.map((option) => option.id), - [ - "nvidia/nemotron-3-super-120b-a12b", - "moonshotai/kimi-k2.5", - "z-ai/glm5", - "minimaxai/minimax-m2.5", - "qwen/qwen3.5-397b-a17b", - "openai/gpt-oss-120b", - ], - ); + expect(CLOUD_MODEL_OPTIONS.map((option) => option.id)).toEqual([ + "nvidia/nemotron-3-super-120b-a12b", + "moonshotai/kimi-k2.5", + "z-ai/glm5", + "minimaxai/minimax-m2.5", + "qwen/qwen3.5-397b-a17b", + "openai/gpt-oss-120b", + ]); }); it("maps ollama-local to the sandbox inference route and default model", () => { - assert.deepEqual(getProviderSelectionConfig("ollama-local"), { + expect(getProviderSelectionConfig("ollama-local")).toEqual({ endpointType: "custom", endpointUrl: INFERENCE_ROUTE_URL, ncpPartner: null, @@ -44,7 +38,9 @@ describe("inference selection config", () => { }); it("maps nvidia-nim to the sandbox inference route", () => { - assert.deepEqual(getProviderSelectionConfig("nvidia-nim", "nvidia/nemotron-3-super-120b-a12b"), { + expect( + getProviderSelectionConfig("nvidia-nim", "nvidia/nemotron-3-super-120b-a12b") + ).toEqual({ endpointType: "custom", endpointUrl: INFERENCE_ROUTE_URL, ncpPartner: null, @@ -57,9 +53,6 @@ describe("inference selection config", () => { }); it("builds a qualified OpenClaw primary model for ollama-local", () => { - assert.equal( - getOpenClawPrimaryModel("ollama-local", "nemotron-3-nano:30b"), - `${MANAGED_PROVIDER_ID}/nemotron-3-nano:30b`, - ); + expect(getOpenClawPrimaryModel("ollama-local", "nemotron-3-nano:30b")).toBe(`${MANAGED_PROVIDER_ID}/nemotron-3-nano:30b`); }); }); diff --git a/test/install-preflight.test.js b/test/install-preflight.test.js index a0693c03..68b62148 100644 --- a/test/install-preflight.test.js +++ b/test/install-preflight.test.js @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -const { describe, it } = require("node:test"); -const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); @@ -99,11 +97,11 @@ exit 98 }); const output = `${result.stdout}${result.stderr}`; - assert.notEqual(result.status, 0); - assert.match(output, /Unsupported runtime detected/); - assert.match(output, /Node\.js >=20 and npm >=10/); - assert.match(output, /v18\.19\.1/); - assert.match(output, /9\.8\.1/); + expect(result.status).not.toBe(0); + expect(output).toMatch(/Unsupported runtime detected/); + expect(output).toMatch(/Node\.js >=20 and npm >=10/); + expect(output).toMatch(/v18\.19\.1/); + expect(output).toMatch(/9\.8\.1/); }); it("uses the HTTPS GitHub fallback when not installing from a repo checkout", () => { @@ -198,8 +196,8 @@ exit 98 }, }); - assert.equal(result.status, 0); - assert.match(fs.readFileSync(gitLog, "utf-8"), /clone.*NemoClaw\.git/); + expect(result.status).toBe(0); + expect(fs.readFileSync(gitLog, "utf-8")).toMatch(/clone.*NemoClaw\.git/); }); it("prints the HTTPS GitHub remediation when the binary is missing", () => { @@ -280,9 +278,9 @@ exit 98 }); const output = `${result.stdout}${result.stderr}`; - assert.notEqual(result.status, 0); - assert.match(output, new RegExp(GITHUB_INSTALL_URL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); - assert.doesNotMatch(output, /npm install -g nemoclaw/); + expect(result.status).not.toBe(0); + expect(output).toMatch(new RegExp(GITHUB_INSTALL_URL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); + expect(output).not.toMatch(/npm install -g nemoclaw/); }); it("does not silently prefer Colima when both macOS runtimes are available", () => { @@ -360,9 +358,9 @@ echo "Darwin" }); const output = `${result.stdout}${result.stderr}`; - assert.notEqual(result.status, 0); - assert.match(output, /Both Colima and Docker Desktop are available/); - assert.doesNotMatch(output, /colima should not be started/); + expect(result.status).not.toBe(0); + expect(output).toMatch(/Both Colima and Docker Desktop are available/); + expect(output).not.toMatch(/colima should not be started/); }); it("can run via stdin without a sibling runtime.sh file", () => { @@ -474,9 +472,9 @@ exit 0 }); const output = `${result.stdout}${result.stderr}`; - assert.equal(result.status, 0); - assert.match(output, /Installation complete!/); - assert.match(output, /nemoclaw v0\.1\.0-test is ready/); + expect(result.status).toBe(0); + expect(output).toMatch(/Installation complete!/); + expect(output).toMatch(/nemoclaw v0\.1\.0-test is ready/); }); it("--help exits 0 and shows install usage", () => { @@ -485,15 +483,15 @@ exit 0 encoding: "utf-8", }); - assert.equal(result.status, 0); + expect(result.status).toBe(0); const output = `${result.stdout}${result.stderr}`; - assert.match(output, /NemoClaw Installer/); - assert.match(output, /--non-interactive/); - assert.match(output, /--version/); - assert.match(output, /NEMOCLAW_PROVIDER/); - assert.match(output, /NEMOCLAW_POLICY_MODE/); - assert.match(output, /NEMOCLAW_SANDBOX_NAME/); - assert.match(output, /nvidia\.com\/nemoclaw\.sh/); + expect(output).toMatch(/NemoClaw Installer/); + expect(output).toMatch(/--non-interactive/); + expect(output).toMatch(/--version/); + expect(output).toMatch(/NEMOCLAW_PROVIDER/); + expect(output).toMatch(/NEMOCLAW_POLICY_MODE/); + expect(output).toMatch(/NEMOCLAW_SANDBOX_NAME/); + expect(output).toMatch(/nvidia\.com\/nemoclaw\.sh/); }); it("--version exits 0 and prints the version number", () => { @@ -502,8 +500,8 @@ exit 0 encoding: "utf-8", }); - assert.equal(result.status, 0); - assert.match(`${result.stdout}${result.stderr}`, /nemoclaw-installer v\d+\.\d+\.\d+/); + expect(result.status).toBe(0); + expect(`${result.stdout}${result.stderr}`).toMatch(/nemoclaw-installer v\d+\.\d+\.\d+/); }); it("-v exits 0 and prints the version number", () => { @@ -512,8 +510,8 @@ exit 0 encoding: "utf-8", }); - assert.equal(result.status, 0); - assert.match(`${result.stdout}${result.stderr}`, /nemoclaw-installer v\d+\.\d+\.\d+/); + expect(result.status).toBe(0); + expect(`${result.stdout}${result.stderr}`).toMatch(/nemoclaw-installer v\d+\.\d+\.\d+/); }); it("uses npm install + npm link for a source checkout (no -g)", () => { @@ -572,13 +570,13 @@ fi`, }, }); - assert.equal(result.status, 0); + expect(result.status).toBe(0); const log = fs.readFileSync(npmLog, "utf-8"); // install (no -g) and link must both have been called - assert.match(log, /^install(?!\s+-g)/m); - assert.match(log, /^link/m); + expect(log).toMatch(/^install(?!\s+-g)/m); + expect(log).toMatch(/^link/m); // the GitHub URL must NOT appear — this is a local install - assert.doesNotMatch(log, new RegExp(GITHUB_INSTALL_URL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); + expect(log).not.toMatch(new RegExp(GITHUB_INSTALL_URL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); }); it("spin() non-TTY: dumps wrapped-command output and exits non-zero on failure", () => { @@ -626,8 +624,8 @@ fi`, }, }); - assert.notEqual(result.status, 0); - assert.match(`${result.stdout}${result.stderr}`, /ENOTFOUND simulated network error/); + expect(result.status).not.toBe(0); + expect(`${result.stdout}${result.stderr}`).toMatch(/ENOTFOUND simulated network error/); }); it("creates a user-local shim when npm installs outside the current PATH", () => { @@ -741,8 +739,8 @@ exit 0 }); const shimPath = path.join(tmp, ".local", "bin", "nemoclaw"); - assert.equal(result.status, 0); - assert.equal(fs.readlinkSync(shimPath), path.join(prefix, "bin", "nemoclaw")); - assert.match(`${result.stdout}${result.stderr}`, /Created user-local shim/); + expect(result.status).toBe(0); + expect(fs.readlinkSync(shimPath)).toBe(path.join(prefix, "bin", "nemoclaw")); + expect(`${result.stdout}${result.stderr}`).toMatch(/Created user-local shim/); }); }); diff --git a/test/local-inference.test.js b/test/local-inference.test.js index 5b77ee2f..f6710e88 100644 --- a/test/local-inference.test.js +++ b/test/local-inference.test.js @@ -1,9 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -const { describe, it } = require("node:test"); -const assert = require("node:assert/strict"); - const { CONTAINER_REACHABILITY_IMAGE, DEFAULT_OLLAMA_MODEL, @@ -21,30 +18,20 @@ const { describe("local inference helpers", () => { it("returns the expected base URL for vllm-local", () => { - assert.equal( - getLocalProviderBaseUrl("vllm-local"), - "http://host.openshell.internal:8000/v1", - ); + expect(getLocalProviderBaseUrl("vllm-local")).toBe("http://host.openshell.internal:8000/v1"); }); it("returns the expected base URL for ollama-local", () => { - assert.equal( - getLocalProviderBaseUrl("ollama-local"), - "http://host.openshell.internal:11434/v1", - ); + expect(getLocalProviderBaseUrl("ollama-local")).toBe("http://host.openshell.internal:11434/v1"); }); it("returns the expected health check command for ollama-local", () => { - assert.equal( - getLocalProviderHealthCheck("ollama-local"), - "curl -sf http://localhost:11434/api/tags 2>/dev/null", - ); + expect(getLocalProviderHealthCheck("ollama-local")).toBe("curl -sf http://localhost:11434/api/tags 2>/dev/null"); }); it("returns the expected container reachability command for ollama-local", () => { - assert.equal( - getLocalProviderContainerReachabilityCheck("ollama-local"), - `docker run --rm --add-host host.openshell.internal:host-gateway ${CONTAINER_REACHABILITY_IMAGE} -sf http://host.openshell.internal:11434/api/tags 2>/dev/null`, + expect(getLocalProviderContainerReachabilityCheck("ollama-local")).toBe( + `docker run --rm --add-host host.openshell.internal:host-gateway ${CONTAINER_REACHABILITY_IMAGE} -sf http://host.openshell.internal:11434/api/tags 2>/dev/null` ); }); @@ -54,14 +41,14 @@ describe("local inference helpers", () => { callCount += 1; return '{"models":[]}'; }); - assert.deepEqual(result, { ok: true }); - assert.equal(callCount, 2); + expect(result).toEqual({ ok: true }); + expect(callCount).toBe(2); }); it("returns a clear error when ollama-local is unavailable", () => { const result = validateLocalProvider("ollama-local", () => ""); - assert.equal(result.ok, false); - assert.match(result.message, /http:\/\/localhost:11434/); + expect(result.ok).toBe(false); + expect(result.message).toMatch(/http:\/\/localhost:11434/); }); it("returns a clear error when ollama-local is not reachable from containers", () => { @@ -70,72 +57,66 @@ describe("local inference helpers", () => { callCount += 1; return callCount === 1 ? '{"models":[]}' : ""; }); - assert.equal(result.ok, false); - assert.match(result.message, /host\.openshell\.internal:11434/); - assert.match(result.message, /0\.0\.0\.0:11434/); + expect(result.ok).toBe(false); + expect(result.message).toMatch(/host\.openshell\.internal:11434/); + expect(result.message).toMatch(/0\.0\.0\.0:11434/); }); it("returns a clear error when vllm-local is unavailable", () => { const result = validateLocalProvider("vllm-local", () => ""); - assert.equal(result.ok, false); - assert.match(result.message, /http:\/\/localhost:8000/); + expect(result.ok).toBe(false); + expect(result.message).toMatch(/http:\/\/localhost:8000/); }); it("parses model names from ollama list output", () => { - assert.deepEqual( - parseOllamaList( - [ - "NAME ID SIZE MODIFIED", - "nemotron-3-nano:30b abc123 24 GB 2 hours ago", - "qwen3:32b def456 20 GB 1 day ago", - ].join("\n"), - ), - ["nemotron-3-nano:30b", "qwen3:32b"], - ); + expect(parseOllamaList( + [ + "NAME ID SIZE MODIFIED", + "nemotron-3-nano:30b abc123 24 GB 2 hours ago", + "qwen3:32b def456 20 GB 1 day ago", + ].join("\n"), + )).toEqual(["nemotron-3-nano:30b", "qwen3:32b"]); }); it("returns parsed ollama model options when available", () => { - assert.deepEqual( - getOllamaModelOptions(() => "nemotron-3-nano:30b abc 24 GB now\nqwen3:32b def 20 GB now"), - ["nemotron-3-nano:30b", "qwen3:32b"], - ); + expect( + getOllamaModelOptions(() => "nemotron-3-nano:30b abc 24 GB now\nqwen3:32b def 20 GB now") + ).toEqual(["nemotron-3-nano:30b", "qwen3:32b"]); }); it("falls back to the default ollama model when list output is empty", () => { - assert.deepEqual(getOllamaModelOptions(() => ""), [DEFAULT_OLLAMA_MODEL]); + expect(getOllamaModelOptions(() => "")).toEqual([DEFAULT_OLLAMA_MODEL]); }); it("prefers the default ollama model when present", () => { - assert.equal( - getDefaultOllamaModel(() => "qwen3:32b abc 20 GB now\nnemotron-3-nano:30b def 24 GB now"), - DEFAULT_OLLAMA_MODEL, - ); + expect( + getDefaultOllamaModel(() => "qwen3:32b abc 20 GB now\nnemotron-3-nano:30b def 24 GB now") + ).toBe(DEFAULT_OLLAMA_MODEL); }); it("falls back to the first listed ollama model when the default is absent", () => { - assert.equal( - getDefaultOllamaModel(() => "qwen3:32b abc 20 GB now\ngemma3:4b def 3 GB now"), - "qwen3:32b", - ); + expect( + getDefaultOllamaModel(() => "qwen3:32b abc 20 GB now\ngemma3:4b def 3 GB now") + ).toBe("qwen3:32b"); }); it("builds a background warmup command for ollama models", () => { const command = getOllamaWarmupCommand("nemotron-3-nano:30b"); - assert.match(command, /^nohup curl -s http:\/\/localhost:11434\/api\/generate /); - assert.match(command, /"model":"nemotron-3-nano:30b"/); - assert.match(command, /"keep_alive":"15m"/); + expect(command).toMatch(/^nohup curl -s http:\/\/localhost:11434\/api\/generate /); + expect(command).toMatch(/"model":"nemotron-3-nano:30b"/); + expect(command).toMatch(/"keep_alive":"15m"/); }); it("builds a foreground probe command for ollama models", () => { const command = getOllamaProbeCommand("nemotron-3-nano:30b"); - assert.match(command, /^curl -sS --max-time 120 http:\/\/localhost:11434\/api\/generate /); - assert.match(command, /"model":"nemotron-3-nano:30b"/); + expect(command).toMatch(/^curl -sS --max-time 120 http:\/\/localhost:11434\/api\/generate /); + expect(command).toMatch(/"model":"nemotron-3-nano:30b"/); }); it("fails ollama model validation when the probe times out or returns nothing", () => { const result = validateOllamaModel("nemotron-3-nano:30b", () => ""); - assert.equal(result.ok, false); - assert.match(result.message, /did not answer the local probe in time/); + expect(result.ok).toBe(false); + expect(result.message).toMatch(/did not answer the local probe in time/); }); it("fails ollama model validation when Ollama returns an error payload", () => { @@ -143,8 +124,8 @@ describe("local inference helpers", () => { "gabegoodhart/minimax-m2.1:latest", () => JSON.stringify({ error: "model requires more system memory" }), ); - assert.equal(result.ok, false); - assert.match(result.message, /requires more system memory/); + expect(result.ok).toBe(false); + expect(result.message).toMatch(/requires more system memory/); }); it("passes ollama model validation when the probe returns a normal payload", () => { @@ -152,6 +133,6 @@ describe("local inference helpers", () => { "nemotron-3-nano:30b", () => JSON.stringify({ model: "nemotron-3-nano:30b", response: "hello", done: true }), ); - assert.deepEqual(result, { ok: true }); + expect(result).toEqual({ ok: true }); }); }); diff --git a/test/nim.test.js b/test/nim.test.js index 8166cf6c..dadef4c4 100644 --- a/test/nim.test.js +++ b/test/nim.test.js @@ -1,43 +1,37 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -const { describe, it } = require("node:test"); -const assert = require("node:assert/strict"); - const nim = require("../bin/lib/nim"); describe("nim", () => { describe("listModels", () => { it("returns 5 models", () => { - assert.equal(nim.listModels().length, 5); + expect(nim.listModels().length).toBe(5); }); it("each model has name, image, and minGpuMemoryMB", () => { for (const m of nim.listModels()) { - assert.ok(m.name, "missing name"); - assert.ok(m.image, "missing image"); - assert.ok(typeof m.minGpuMemoryMB === "number", "minGpuMemoryMB should be number"); - assert.ok(m.minGpuMemoryMB > 0, "minGpuMemoryMB should be positive"); + expect(m.name).toBeTruthy(); + expect(m.image).toBeTruthy(); + expect(typeof m.minGpuMemoryMB === "number").toBeTruthy(); + expect(m.minGpuMemoryMB > 0).toBeTruthy(); } }); }); describe("getImageForModel", () => { it("returns correct image for known model", () => { - assert.equal( - nim.getImageForModel("nvidia/nemotron-3-nano-30b-a3b"), - "nvcr.io/nim/nvidia/nemotron-3-nano-30b-a3b:latest" - ); + expect(nim.getImageForModel("nvidia/nemotron-3-nano-30b-a3b")).toBe("nvcr.io/nim/nvidia/nemotron-3-nano-30b-a3b:latest"); }); it("returns null for unknown model", () => { - assert.equal(nim.getImageForModel("bogus/model"), null); + expect(nim.getImageForModel("bogus/model")).toBe(null); }); }); describe("containerName", () => { it("prefixes with nemoclaw-nim-", () => { - assert.equal(nim.containerName("my-sandbox"), "nemoclaw-nim-my-sandbox"); + expect(nim.containerName("my-sandbox")).toBe("nemoclaw-nim-my-sandbox"); }); }); @@ -45,25 +39,25 @@ describe("nim", () => { it("returns object or null", () => { const gpu = nim.detectGpu(); if (gpu !== null) { - assert.ok(gpu.type, "gpu should have type"); - assert.ok(typeof gpu.count === "number", "count should be number"); - assert.ok(typeof gpu.totalMemoryMB === "number", "totalMemoryMB should be number"); - assert.ok(typeof gpu.nimCapable === "boolean", "nimCapable should be boolean"); + expect(gpu.type).toBeTruthy(); + expect(typeof gpu.count === "number").toBeTruthy(); + expect(typeof gpu.totalMemoryMB === "number").toBeTruthy(); + expect(typeof gpu.nimCapable === "boolean").toBeTruthy(); } }); it("nvidia type is nimCapable", () => { const gpu = nim.detectGpu(); if (gpu && gpu.type === "nvidia") { - assert.equal(gpu.nimCapable, true); + expect(gpu.nimCapable).toBe(true); } }); it("apple type is not nimCapable", () => { const gpu = nim.detectGpu(); if (gpu && gpu.type === "apple") { - assert.equal(gpu.nimCapable, false); - assert.ok(gpu.name, "apple gpu should have name"); + expect(gpu.nimCapable).toBe(false); + expect(gpu.name).toBeTruthy(); } }); }); @@ -71,7 +65,7 @@ describe("nim", () => { describe("nimStatus", () => { it("returns not running for nonexistent container", () => { const st = nim.nimStatus("nonexistent-test-xyz"); - assert.equal(st.running, false); + expect(st.running).toBe(false); }); }); }); diff --git a/test/onboard-readiness.test.js b/test/onboard-readiness.test.js index 13a44817..9a9e6530 100644 --- a/test/onboard-readiness.test.js +++ b/test/onboard-readiness.test.js @@ -1,47 +1,46 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -const { describe, it } = require("node:test"); -const assert = require("node:assert/strict"); - const { buildPolicySetCommand, buildPolicyGetCommand } = require("../bin/lib/policies"); const { hasStaleGateway, isSandboxReady } = require("../bin/lib/onboard"); describe("sandbox readiness parsing", () => { it("detects Ready sandbox", () => { - assert.ok(isSandboxReady("my-assistant Ready 2m ago", "my-assistant")); + expect(isSandboxReady("my-assistant Ready 2m ago", "my-assistant")).toBeTruthy(); }); it("rejects NotReady sandbox", () => { - assert.ok(!isSandboxReady("my-assistant NotReady init failed", "my-assistant")); + expect(!isSandboxReady("my-assistant NotReady init failed", "my-assistant")).toBeTruthy(); }); it("rejects empty output", () => { - assert.ok(!isSandboxReady("No sandboxes found.", "my-assistant")); - assert.ok(!isSandboxReady("", "my-assistant")); + expect(!isSandboxReady("No sandboxes found.", "my-assistant")).toBeTruthy(); + expect(!isSandboxReady("", "my-assistant")).toBeTruthy(); }); it("strips ANSI escape codes before matching", () => { - assert.ok(isSandboxReady( + expect(isSandboxReady( "\x1b[1mmy-assistant\x1b[0m \x1b[32mReady\x1b[0m 2m ago", "my-assistant" - )); + )).toBeTruthy(); }); it("rejects ANSI-wrapped NotReady", () => { - assert.ok(!isSandboxReady( + expect(!isSandboxReady( "\x1b[1mmy-assistant\x1b[0m \x1b[31mNotReady\x1b[0m crash", "my-assistant" - )); + )).toBeTruthy(); }); it("exact-matches sandbox name in first column", () => { // "my" should NOT match "my-assistant" - assert.ok(!isSandboxReady("my-assistant Ready 2m ago", "my")); + expect(!isSandboxReady("my-assistant Ready 2m ago", "my")).toBeTruthy(); }); it("does not match sandbox name in non-first column", () => { - assert.ok(!isSandboxReady("other-box Ready owned-by-my-assistant", "my-assistant")); + expect( + !isSandboxReady("other-box Ready owned-by-my-assistant", "my-assistant") + ).toBeTruthy(); }); it("handles multiple sandboxes in output", () => { @@ -51,23 +50,27 @@ describe("sandbox readiness parsing", () => { "my-assistant Ready 2m ago", "staging Ready 10m ago", ].join("\n"); - assert.ok(isSandboxReady(output, "my-assistant")); - assert.ok(!isSandboxReady(output, "dev-box")); // NotReady - assert.ok(isSandboxReady(output, "staging")); - assert.ok(!isSandboxReady(output, "prod")); // not present + expect(isSandboxReady(output, "my-assistant")).toBeTruthy(); + expect(!isSandboxReady(output, "dev-box")).toBeTruthy(); // NotReady + expect(isSandboxReady(output, "staging")).toBeTruthy(); + expect(!isSandboxReady(output, "prod")).toBeTruthy(); // not present }); it("handles Ready sandbox with extra status columns", () => { - assert.ok(isSandboxReady("my-assistant Ready Running 2m ago 1/1", "my-assistant")); + expect( + isSandboxReady("my-assistant Ready Running 2m ago 1/1", "my-assistant") + ).toBeTruthy(); }); it("rejects when output only contains name in a URL or path", () => { - assert.ok(!isSandboxReady("Connecting to my-assistant.openshell.internal Ready", "my-assistant")); + expect( + !isSandboxReady("Connecting to my-assistant.openshell.internal Ready", "my-assistant") + ).toBeTruthy(); // "my-assistant.openshell.internal" is cols[0], not "my-assistant" }); it("handles tab-separated output", () => { - assert.ok(isSandboxReady("my-assistant\tReady\t2m ago", "my-assistant")); + expect(isSandboxReady("my-assistant\tReady\t2m ago", "my-assistant")).toBeTruthy(); }); }); @@ -76,52 +79,43 @@ describe("sandbox readiness parsing", () => { describe("WSL sandbox name handling", () => { it("buildPolicySetCommand preserves hyphenated sandbox name", () => { const cmd = buildPolicySetCommand("/tmp/policy.yaml", "my-assistant"); - assert.ok(cmd.includes("'my-assistant'"), `Expected quoted name in: ${cmd}`); - assert.ok(!cmd.includes(' my-assistant '), "Name must be quoted, not bare"); + expect(cmd.includes("'my-assistant'")).toBeTruthy(); + expect(!cmd.includes(' my-assistant ')).toBeTruthy(); }); it("buildPolicyGetCommand preserves hyphenated sandbox name", () => { const cmd = buildPolicyGetCommand("my-assistant"); - assert.ok(cmd.includes("'my-assistant'"), `Expected quoted name in: ${cmd}`); + expect(cmd.includes("'my-assistant'")).toBeTruthy(); }); it("buildPolicySetCommand preserves multi-hyphen names", () => { const cmd = buildPolicySetCommand("/tmp/p.yaml", "my-dev-assistant-v2"); - assert.ok(cmd.includes("'my-dev-assistant-v2'")); + expect(cmd.includes("'my-dev-assistant-v2'")).toBeTruthy(); }); it("buildPolicySetCommand preserves single-char name", () => { // If WSL truncates "my-assistant" to "m", the single-char name should // still be quoted and passed through unchanged const cmd = buildPolicySetCommand("/tmp/p.yaml", "m"); - assert.ok(cmd.includes("'m'")); + expect(cmd.includes("'m'")).toBeTruthy(); }); it("applyPreset rejects truncated/invalid sandbox name", () => { const policies = require("../bin/lib/policies"); // Empty name - assert.throws( - () => policies.applyPreset("", "npm"), - /Invalid or truncated sandbox name/ - ); + expect(() => policies.applyPreset("", "npm")).toThrow(/Invalid or truncated sandbox name/); // Name with uppercase (not valid per RFC 1123) - assert.throws( - () => policies.applyPreset("My-Assistant", "npm"), - /Invalid or truncated sandbox name/ - ); + expect(() => policies.applyPreset("My-Assistant", "npm")).toThrow(/Invalid or truncated sandbox name/); // Name starting with hyphen - assert.throws( - () => policies.applyPreset("-broken", "npm"), - /Invalid or truncated sandbox name/ - ); + expect(() => policies.applyPreset("-broken", "npm")).toThrow(/Invalid or truncated sandbox name/); }); it("readiness check uses exact match preventing truncated name false-positive", () => { // If "my-assistant" was truncated to "m", the readiness check should // NOT match a sandbox named "my-assistant" when searching for "m" - assert.ok(!isSandboxReady("my-assistant Ready 2m ago", "m")); - assert.ok(!isSandboxReady("my-assistant Ready 2m ago", "my")); - assert.ok(!isSandboxReady("my-assistant Ready 2m ago", "my-")); + expect(!isSandboxReady("my-assistant Ready 2m ago", "m")).toBeTruthy(); + expect(!isSandboxReady("my-assistant Ready 2m ago", "my")).toBeTruthy(); + expect(!isSandboxReady("my-assistant Ready 2m ago", "my-")).toBeTruthy(); }); }); @@ -137,7 +131,7 @@ describe("stale gateway detection", () => { " Gateway: nemoclaw", " Gateway endpoint: https://127.0.0.1:8080", ].join("\n"); - assert.ok(hasStaleGateway(output)); + expect(hasStaleGateway(output)).toBeTruthy(); }); it("detects gateway from ANSI-colored output", () => { @@ -145,21 +139,21 @@ describe("stale gateway detection", () => { "\x1b[1m\x1b[36mGateway Info\x1b[39m\x1b[0m\n\n" + " \x1b[2mGateway:\x1b[0m nemoclaw\n" + " \x1b[2mGateway endpoint:\x1b[0m https://127.0.0.1:8080"; - assert.ok(hasStaleGateway(output)); + expect(hasStaleGateway(output)).toBeTruthy(); }); it("returns false for empty string (no gateway running)", () => { - assert.ok(!hasStaleGateway("")); + expect(!hasStaleGateway("")).toBeTruthy(); }); it("returns false for null/undefined", () => { - assert.ok(!hasStaleGateway(null)); - assert.ok(!hasStaleGateway(undefined)); + expect(!hasStaleGateway(null)).toBeTruthy(); + expect(!hasStaleGateway(undefined)).toBeTruthy(); }); it("returns false for error output without gateway name", () => { - assert.ok(!hasStaleGateway("Error: no gateway found")); - assert.ok(!hasStaleGateway("connection refused")); + expect(!hasStaleGateway("Error: no gateway found")).toBeTruthy(); + expect(!hasStaleGateway("connection refused")).toBeTruthy(); }); it("returns false for a different gateway name", () => { @@ -170,6 +164,6 @@ describe("stale gateway detection", () => { " Gateway: my-other-gateway", " Gateway endpoint: https://127.0.0.1:8080", ].join("\n"); - assert.ok(!hasStaleGateway(output)); + expect(!hasStaleGateway(output)).toBeTruthy(); }); }); diff --git a/test/onboard-selection.test.js b/test/onboard-selection.test.js index 9000943b..7eed9e02 100644 --- a/test/onboard-selection.test.js +++ b/test/onboard-selection.test.js @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -const { describe, it } = require("node:test"); -const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); @@ -69,16 +67,20 @@ const { setupNim } = require(${onboardPath}); }, }); - assert.equal(result.status, 0, result.stderr); - assert.notEqual(result.stdout.trim(), "", result.stderr); + expect(result.status).toBe(0); + expect(result.stdout.trim()).not.toBe(""); const payload = JSON.parse(result.stdout.trim()); - assert.equal(payload.result.provider, "nvidia-nim"); - assert.equal(payload.result.model, "nvidia/nemotron-3-super-120b-a12b"); - assert.equal(payload.promptCalls, 2); - assert.match(payload.messages[0], /Choose \[/); - assert.match(payload.messages[1], /Choose model \[1\]/); - assert.ok(payload.lines.some((line) => line.includes("Detected local inference option"))); - assert.ok(payload.lines.some((line) => line.includes("Press Enter to keep the cloud default"))); - assert.ok(payload.lines.some((line) => line.includes("Cloud models:"))); + expect(payload.result.provider).toBe("nvidia-nim"); + expect(payload.result.model).toBe("nvidia/nemotron-3-super-120b-a12b"); + expect(payload.promptCalls).toBe(2); + expect(payload.messages[0]).toMatch(/Choose \[/); + expect(payload.messages[1]).toMatch(/Choose model \[1\]/); + expect( + payload.lines.some((line) => line.includes("Detected local inference option")) + ).toBeTruthy(); + expect( + payload.lines.some((line) => line.includes("Press Enter to keep the cloud default")) + ).toBeTruthy(); + expect(payload.lines.some((line) => line.includes("Cloud models:"))).toBeTruthy(); }); }); diff --git a/test/onboard.test.js b/test/onboard.test.js index 4d35de7b..a34055df 100644 --- a/test/onboard.test.js +++ b/test/onboard.test.js @@ -4,8 +4,6 @@ const fs = require("fs"); const os = require("os"); const path = require("path"); -const { describe, it } = require("node:test"); -const assert = require("node:assert/strict"); const { buildSandboxConfigSyncScript, @@ -27,36 +25,33 @@ describe("onboard helpers", () => { }); // Writes NemoClaw selection config to writable ~/.nemoclaw/ - assert.match(script, /cat > ~\/\.nemoclaw\/config\.json/); - assert.match(script, /"model": "nemotron-3-nano:30b"/); - assert.match(script, /"credentialEnv": "OPENAI_API_KEY"/); + expect(script).toMatch(/cat > ~\/\.nemoclaw\/config\.json/); + expect(script).toMatch(/"model": "nemotron-3-nano:30b"/); + expect(script).toMatch(/"credentialEnv": "OPENAI_API_KEY"/); // Must NOT modify openclaw config from inside the sandbox — model routing // is handled by the host-side gateway (openshell inference set) - assert.doesNotMatch(script, /openclaw\.json/); - assert.doesNotMatch(script, /openclaw models set/); + expect(script).not.toMatch(/openclaw\.json/); + expect(script).not.toMatch(/openclaw models set/); - assert.match(script, /^exit$/m); + expect(script).toMatch(/^exit$/m); }); it("pins the gateway image to the installed OpenShell release version", () => { - assert.equal(getInstalledOpenshellVersion("openshell 0.0.12"), "0.0.12"); - assert.equal(getInstalledOpenshellVersion("openshell 0.0.13-dev.8+gbbcaed2ea"), "0.0.13"); - assert.equal(getInstalledOpenshellVersion("bogus"), null); - assert.equal( - getStableGatewayImageRef("openshell 0.0.12"), - "ghcr.io/nvidia/openshell/cluster:0.0.12" - ); - assert.equal(getStableGatewayImageRef("openshell 0.0.13-dev.8+gbbcaed2ea"), "ghcr.io/nvidia/openshell/cluster:0.0.13"); - assert.equal(getStableGatewayImageRef("bogus"), null); + expect(getInstalledOpenshellVersion("openshell 0.0.12")).toBe("0.0.12"); + expect(getInstalledOpenshellVersion("openshell 0.0.13-dev.8+gbbcaed2ea")).toBe("0.0.13"); + expect(getInstalledOpenshellVersion("bogus")).toBe(null); + expect(getStableGatewayImageRef("openshell 0.0.12")).toBe("ghcr.io/nvidia/openshell/cluster:0.0.12"); + expect(getStableGatewayImageRef("openshell 0.0.13-dev.8+gbbcaed2ea")).toBe("ghcr.io/nvidia/openshell/cluster:0.0.13"); + expect(getStableGatewayImageRef("bogus")).toBe(null); }); it("writes sandbox sync scripts to a temp file for stdin redirection", () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-test-")); try { const scriptFile = writeSandboxConfigSyncFile("echo test", tmpDir, 1234); - assert.equal(scriptFile, path.join(tmpDir, "nemoclaw-sync-1234.sh")); - assert.equal(fs.readFileSync(scriptFile, "utf8"), "echo test\n"); + expect(scriptFile).toBe(path.join(tmpDir, "nemoclaw-sync-1234.sh")); + expect(fs.readFileSync(scriptFile, "utf8")).toBe("echo test\n"); } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); } diff --git a/test/platform.test.js b/test/platform.test.js index 6a20e16e..b97417a8 100644 --- a/test/platform.test.js +++ b/test/platform.test.js @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -const { describe, it } = require("node:test"); -const assert = require("node:assert/strict"); const path = require("node:path"); const { @@ -18,32 +16,26 @@ const { describe("platform helpers", () => { describe("isWsl", () => { it("detects WSL from environment", () => { - assert.equal( - isWsl({ - platform: "linux", - env: { WSL_DISTRO_NAME: "Ubuntu" }, - release: "6.6.87.2-microsoft-standard-WSL2", - }), - true, - ); + expect(isWsl({ + platform: "linux", + env: { WSL_DISTRO_NAME: "Ubuntu" }, + release: "6.6.87.2-microsoft-standard-WSL2", + })).toBe(true); }); it("does not treat macOS as WSL", () => { - assert.equal( - isWsl({ - platform: "darwin", - env: {}, - release: "24.6.0", - }), - false, - ); + expect(isWsl({ + platform: "darwin", + env: {}, + release: "24.6.0", + })).toBe(false); }); }); describe("getDockerSocketCandidates", () => { it("returns macOS candidates in priority order", () => { const home = "/tmp/test-home"; - assert.deepEqual(getDockerSocketCandidates({ platform: "darwin", home }), [ + expect(getDockerSocketCandidates({ platform: "darwin", home })).toEqual([ path.join(home, ".colima/default/docker.sock"), path.join(home, ".config/colima/default/docker.sock"), path.join(home, ".docker/run/docker.sock"), @@ -51,7 +43,7 @@ describe("platform helpers", () => { }); it("does not auto-detect sockets on Linux", () => { - assert.deepEqual(getDockerSocketCandidates({ platform: "linux", home: "/tmp/test-home" }), []); + expect(getDockerSocketCandidates({ platform: "linux", home: "/tmp/test-home" })).toEqual([]); }); }); @@ -61,28 +53,22 @@ describe("platform helpers", () => { const sockets = new Set([path.join(home, ".config/colima/default/docker.sock")]); const existsSync = (socketPath) => sockets.has(socketPath); - assert.equal( - findColimaDockerSocket({ home, existsSync }), - path.join(home, ".config/colima/default/docker.sock"), - ); + expect(findColimaDockerSocket({ home, existsSync })).toBe(path.join(home, ".config/colima/default/docker.sock")); }); }); describe("detectDockerHost", () => { it("respects an existing DOCKER_HOST", () => { - assert.deepEqual( - detectDockerHost({ - env: { DOCKER_HOST: "unix:///custom/docker.sock" }, - platform: "darwin", - home: "/tmp/test-home", - existsSync: () => false, - }), - { - dockerHost: "unix:///custom/docker.sock", - source: "env", - socketPath: null, - }, - ); + expect(detectDockerHost({ + env: { DOCKER_HOST: "unix:///custom/docker.sock" }, + platform: "darwin", + home: "/tmp/test-home", + existsSync: () => false, + })).toEqual({ + dockerHost: "unix:///custom/docker.sock", + source: "env", + socketPath: null, + }); }); it("prefers Colima over Docker Desktop on macOS", () => { @@ -93,14 +79,11 @@ describe("platform helpers", () => { ]); const existsSync = (socketPath) => sockets.has(socketPath); - assert.deepEqual( - detectDockerHost({ env: {}, platform: "darwin", home, existsSync }), - { - dockerHost: `unix://${path.join(home, ".colima/default/docker.sock")}`, - source: "socket", - socketPath: path.join(home, ".colima/default/docker.sock"), - }, - ); + expect(detectDockerHost({ env: {}, platform: "darwin", home, existsSync })).toEqual({ + dockerHost: `unix://${path.join(home, ".colima/default/docker.sock")}`, + source: "socket", + socketPath: path.join(home, ".colima/default/docker.sock"), + }); }); it("detects Docker Desktop when Colima is absent", () => { @@ -108,58 +91,52 @@ describe("platform helpers", () => { const socketPath = path.join(home, ".docker/run/docker.sock"); const existsSync = (candidate) => candidate === socketPath; - assert.deepEqual( - detectDockerHost({ env: {}, platform: "darwin", home, existsSync }), - { - dockerHost: `unix://${socketPath}`, - source: "socket", - socketPath, - }, - ); + expect(detectDockerHost({ env: {}, platform: "darwin", home, existsSync })).toEqual({ + dockerHost: `unix://${socketPath}`, + source: "socket", + socketPath, + }); }); it("returns null when no auto-detected socket is available", () => { - assert.equal( - detectDockerHost({ - env: {}, - platform: "linux", - home: "/tmp/test-home", - existsSync: () => false, - }), - null, - ); + expect(detectDockerHost({ + env: {}, + platform: "linux", + home: "/tmp/test-home", + existsSync: () => false, + })).toBe(null); }); }); describe("inferContainerRuntime", () => { it("detects podman", () => { - assert.equal(inferContainerRuntime("podman version 5.4.1"), "podman"); + expect(inferContainerRuntime("podman version 5.4.1")).toBe("podman"); }); it("detects Docker Desktop", () => { - assert.equal(inferContainerRuntime("Docker Desktop 4.42.0 (190636)"), "docker-desktop"); + expect(inferContainerRuntime("Docker Desktop 4.42.0 (190636)")).toBe("docker-desktop"); }); it("detects Colima", () => { - assert.equal(inferContainerRuntime("Server: Colima\n Docker Engine - Community"), "colima"); + expect(inferContainerRuntime("Server: Colima\n Docker Engine - Community")).toBe("colima"); }); }); describe("isUnsupportedMacosRuntime", () => { it("flags podman on macOS", () => { - assert.equal(isUnsupportedMacosRuntime("podman", { platform: "darwin" }), true); + expect(isUnsupportedMacosRuntime("podman", { platform: "darwin" })).toBe(true); }); it("does not flag podman on Linux", () => { - assert.equal(isUnsupportedMacosRuntime("podman", { platform: "linux" }), false); + expect(isUnsupportedMacosRuntime("podman", { platform: "linux" })).toBe(false); }); }); describe("shouldPatchCoredns", () => { it("patches CoreDNS for Colima only", () => { - assert.equal(shouldPatchCoredns("colima"), true); - assert.equal(shouldPatchCoredns("docker-desktop"), false); - assert.equal(shouldPatchCoredns("docker"), false); + expect(shouldPatchCoredns("colima")).toBe(true); + expect(shouldPatchCoredns("docker-desktop")).toBe(false); + expect(shouldPatchCoredns("docker")).toBe(false); }); }); }); diff --git a/test/policies.test.js b/test/policies.test.js index 040910bb..51747a7a 100644 --- a/test/policies.test.js +++ b/test/policies.test.js @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -const { describe, it } = require("node:test"); -const assert = require("node:assert/strict"); const path = require("path"); const policies = require("../bin/lib/policies"); @@ -11,37 +9,37 @@ describe("policies", () => { describe("listPresets", () => { it("returns all 9 presets", () => { const presets = policies.listPresets(); - assert.equal(presets.length, 9); + expect(presets.length).toBe(9); }); it("each preset has name and description", () => { for (const p of policies.listPresets()) { - assert.ok(p.name, `preset missing name: ${p.file}`); - assert.ok(p.description, `preset missing description: ${p.file}`); + expect(p.name).toBeTruthy(); + expect(p.description).toBeTruthy(); } }); it("returns expected preset names", () => { const names = policies.listPresets().map((p) => p.name).sort(); const expected = ["discord", "docker", "huggingface", "jira", "npm", "outlook", "pypi", "slack", "telegram"]; - assert.deepEqual(names, expected); + expect(names).toEqual(expected); }); }); describe("loadPreset", () => { it("loads existing preset", () => { const content = policies.loadPreset("outlook"); - assert.ok(content); - assert.ok(content.includes("network_policies:")); + expect(content).toBeTruthy(); + expect(content.includes("network_policies:")).toBeTruthy(); }); it("returns null for nonexistent preset", () => { - assert.equal(policies.loadPreset("nonexistent"), null); + expect(policies.loadPreset("nonexistent")).toBe(null); }); it("rejects path traversal attempts", () => { - assert.equal(policies.loadPreset("../../etc/passwd"), null); - assert.equal(policies.loadPreset("../../../etc/shadow"), null); + expect(policies.loadPreset("../../etc/passwd")).toBe(null); + expect(policies.loadPreset("../../../etc/shadow")).toBe(null); }); }); @@ -49,23 +47,23 @@ describe("policies", () => { it("extracts hosts from outlook preset", () => { const content = policies.loadPreset("outlook"); const hosts = policies.getPresetEndpoints(content); - assert.ok(hosts.includes("graph.microsoft.com")); - assert.ok(hosts.includes("login.microsoftonline.com")); - assert.ok(hosts.includes("outlook.office365.com")); - assert.ok(hosts.includes("outlook.office.com")); + expect(hosts.includes("graph.microsoft.com")).toBeTruthy(); + expect(hosts.includes("login.microsoftonline.com")).toBeTruthy(); + expect(hosts.includes("outlook.office365.com")).toBeTruthy(); + expect(hosts.includes("outlook.office.com")).toBeTruthy(); }); it("extracts hosts from telegram preset", () => { const content = policies.loadPreset("telegram"); const hosts = policies.getPresetEndpoints(content); - assert.deepEqual(hosts, ["api.telegram.org"]); + expect(hosts).toEqual(["api.telegram.org"]); }); it("every preset has at least one endpoint", () => { for (const p of policies.listPresets()) { const content = policies.loadPreset(p.name); const hosts = policies.getPresetEndpoints(content); - assert.ok(hosts.length > 0, `${p.name} has no endpoints`); + expect(hosts.length > 0).toBeTruthy(); } }); }); @@ -73,26 +71,26 @@ describe("policies", () => { describe("buildPolicySetCommand", () => { it("shell-quotes sandbox name to prevent injection", () => { const cmd = policies.buildPolicySetCommand("/tmp/policy.yaml", "my-assistant"); - assert.equal(cmd, "openshell policy set --policy '/tmp/policy.yaml' --wait 'my-assistant'"); + expect(cmd).toBe("openshell policy set --policy '/tmp/policy.yaml' --wait 'my-assistant'"); }); it("escapes shell metacharacters in sandbox name", () => { const cmd = policies.buildPolicySetCommand("/tmp/policy.yaml", "test; whoami"); - assert.ok(cmd.includes("'test; whoami'"), "metacharacters must be shell-quoted"); + expect(cmd.includes("'test; whoami'")).toBeTruthy(); }); it("places --wait before the sandbox name", () => { const cmd = policies.buildPolicySetCommand("/tmp/policy.yaml", "test-box"); const waitIdx = cmd.indexOf("--wait"); const nameIdx = cmd.indexOf("'test-box'"); - assert.ok(waitIdx < nameIdx, "--wait must come before sandbox name"); + expect(waitIdx < nameIdx).toBeTruthy(); }); }); describe("buildPolicyGetCommand", () => { it("shell-quotes sandbox name", () => { const cmd = policies.buildPolicyGetCommand("my-assistant"); - assert.equal(cmd, "openshell policy get --full 'my-assistant' 2>/dev/null"); + expect(cmd).toBe("openshell policy get --full 'my-assistant' 2>/dev/null"); }); }); @@ -107,7 +105,9 @@ describe("policies", () => { // rules: at 4-space indent (same level as endpoints:) is wrong // rules: at 8+ space indent (inside an endpoint) is correct if (/^\s{4}rules:/.test(line)) { - assert.fail(`${p.name} line ${i + 1}: rules at policy level (should be inside endpoint)`); + expect.unreachable( + `${p.name} line ${i + 1}: rules at policy level (should be inside endpoint)` + ); } } } @@ -116,7 +116,7 @@ describe("policies", () => { it("every preset has network_policies section", () => { for (const p of policies.listPresets()) { const content = policies.loadPreset(p.name); - assert.ok(content.includes("network_policies:"), `${p.name} missing network_policies`); + expect(content.includes("network_policies:")).toBeTruthy(); } }); }); diff --git a/test/preflight.test.js b/test/preflight.test.js index 6471d798..f2bc39bd 100644 --- a/test/preflight.test.js +++ b/test/preflight.test.js @@ -1,9 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -const { describe, it } = require("node:test"); -const assert = require("node:assert/strict"); - const net = require("net"); const { checkPortAvailable } = require("../bin/lib/preflight"); @@ -20,7 +17,7 @@ describe("checkPortAvailable", () => { }); }); const result = await checkPortAvailable(freePort, { lsofOutput: "" }); - assert.deepEqual(result, { ok: true }); + expect(result).toEqual({ ok: true }); }); it("net probe catches occupied port even when lsof returns empty", async () => { @@ -32,9 +29,9 @@ describe("checkPortAvailable", () => { }); try { const result = await checkPortAvailable(port, { lsofOutput: "" }); - assert.equal(result.ok, false); - assert.equal(result.process, "unknown"); - assert.ok(result.reason.includes("EADDRINUSE")); + expect(result.ok).toBe(false); + expect(result.process).toBe("unknown"); + expect(result.reason.includes("EADDRINUSE")).toBeTruthy(); } finally { await new Promise((resolve) => srv.close(resolve)); } @@ -46,10 +43,10 @@ describe("checkPortAvailable", () => { "openclaw 12345 root 7u IPv4 54321 0t0 TCP *:18789 (LISTEN)", ].join("\n"); const result = await checkPortAvailable(18789, { lsofOutput }); - assert.equal(result.ok, false); - assert.equal(result.process, "openclaw"); - assert.equal(result.pid, 12345); - assert.ok(result.reason.includes("openclaw")); + expect(result.ok).toBe(false); + expect(result.process).toBe("openclaw"); + expect(result.pid).toBe(12345); + expect(result.reason.includes("openclaw")).toBeTruthy(); }); it("picks first listener when lsof shows multiple", async () => { @@ -59,9 +56,9 @@ describe("checkPortAvailable", () => { "node 222 root 8u IPv4 54322 0t0 TCP *:18789 (LISTEN)", ].join("\n"); const result = await checkPortAvailable(18789, { lsofOutput }); - assert.equal(result.ok, false); - assert.equal(result.process, "gateway"); - assert.equal(result.pid, 111); + expect(result.ok).toBe(false); + expect(result.process).toBe("gateway"); + expect(result.pid).toBe(111); }); it("net probe returns ok for a free port", async () => { @@ -74,7 +71,7 @@ describe("checkPortAvailable", () => { }); }); const result = await checkPortAvailable(freePort, { skipLsof: true }); - assert.deepEqual(result, { ok: true }); + expect(result).toEqual({ ok: true }); }); it("net probe detects occupied port", async () => { @@ -84,9 +81,9 @@ describe("checkPortAvailable", () => { }); try { const result = await checkPortAvailable(port, { skipLsof: true }); - assert.equal(result.ok, false); - assert.equal(result.process, "unknown"); - assert.ok(result.reason.includes("EADDRINUSE")); + expect(result.ok).toBe(false); + expect(result.process).toBe("unknown"); + expect(result.reason.includes("EADDRINUSE")).toBeTruthy(); } finally { await new Promise((resolve) => srv.close(resolve)); } @@ -101,13 +98,13 @@ describe("checkPortAvailable", () => { }); }); const result = await checkPortAvailable(freePort); - assert.equal(result.ok, true); + expect(result.ok).toBe(true); }); it("defaults to port 18789 when no args given", async () => { // Should not throw — just verify it returns a valid result object const result = await checkPortAvailable(); - assert.equal(typeof result.ok, "boolean"); + expect(typeof result.ok).toBe("boolean"); }); it("checks gateway port 8080", async () => { @@ -120,6 +117,6 @@ describe("checkPortAvailable", () => { }); // Verify the function works with any port (including 8080-range) const result = await checkPortAvailable(freePort); - assert.equal(result.ok, true); + expect(result.ok).toBe(true); }); }); diff --git a/test/registry.test.js b/test/registry.test.js index 0f471b0b..99268444 100644 --- a/test/registry.test.js +++ b/test/registry.test.js @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -const { describe, it, beforeEach } = require("node:test"); -const assert = require("node:assert/strict"); const fs = require("fs"); const path = require("path"); const os = require("os"); @@ -22,45 +20,45 @@ beforeEach(() => { describe("registry", () => { it("starts empty", () => { const { sandboxes, defaultSandbox } = registry.listSandboxes(); - assert.equal(sandboxes.length, 0); - assert.equal(defaultSandbox, null); + expect(sandboxes.length).toBe(0); + expect(defaultSandbox).toBe(null); }); it("registers a sandbox and sets it as default", () => { registry.registerSandbox({ name: "alpha", model: "test-model", provider: "nvidia-nim" }); const sb = registry.getSandbox("alpha"); - assert.equal(sb.name, "alpha"); - assert.equal(sb.model, "test-model"); - assert.equal(registry.getDefault(), "alpha"); + expect(sb.name).toBe("alpha"); + expect(sb.model).toBe("test-model"); + expect(registry.getDefault()).toBe("alpha"); }); it("first registered becomes default", () => { registry.registerSandbox({ name: "first" }); registry.registerSandbox({ name: "second" }); - assert.equal(registry.getDefault(), "first"); + expect(registry.getDefault()).toBe("first"); }); it("setDefault changes the default", () => { registry.registerSandbox({ name: "a" }); registry.registerSandbox({ name: "b" }); registry.setDefault("b"); - assert.equal(registry.getDefault(), "b"); + expect(registry.getDefault()).toBe("b"); }); it("setDefault returns false for nonexistent sandbox", () => { - assert.equal(registry.setDefault("nope"), false); + expect(registry.setDefault("nope")).toBe(false); }); it("updateSandbox modifies fields", () => { registry.registerSandbox({ name: "up" }); registry.updateSandbox("up", { policies: ["pypi", "npm"], model: "new-model" }); const sb = registry.getSandbox("up"); - assert.deepEqual(sb.policies, ["pypi", "npm"]); - assert.equal(sb.model, "new-model"); + expect(sb.policies).toEqual(["pypi", "npm"]); + expect(sb.model).toBe("new-model"); }); it("updateSandbox returns false for nonexistent sandbox", () => { - assert.equal(registry.updateSandbox("nope", {}), false); + expect(registry.updateSandbox("nope", {})).toBe(false); }); it("removeSandbox deletes and shifts default", () => { @@ -68,31 +66,31 @@ describe("registry", () => { registry.registerSandbox({ name: "y" }); registry.setDefault("x"); registry.removeSandbox("x"); - assert.equal(registry.getSandbox("x"), null); - assert.equal(registry.getDefault(), "y"); + expect(registry.getSandbox("x")).toBe(null); + expect(registry.getDefault()).toBe("y"); }); it("removeSandbox last sandbox sets default to null", () => { registry.registerSandbox({ name: "only" }); registry.removeSandbox("only"); - assert.equal(registry.getDefault(), null); - assert.equal(registry.listSandboxes().sandboxes.length, 0); + expect(registry.getDefault()).toBe(null); + expect(registry.listSandboxes().sandboxes.length).toBe(0); }); it("removeSandbox returns false for nonexistent", () => { - assert.equal(registry.removeSandbox("nope"), false); + expect(registry.removeSandbox("nope")).toBe(false); }); it("getSandbox returns null for nonexistent", () => { - assert.equal(registry.getSandbox("nope"), null); + expect(registry.getSandbox("nope")).toBe(null); }); it("persists to disk and survives reload", () => { registry.registerSandbox({ name: "persist", model: "m1" }); // Read file directly const data = JSON.parse(fs.readFileSync(regFile, "utf-8")); - assert.equal(data.sandboxes.persist.model, "m1"); - assert.equal(data.defaultSandbox, "persist"); + expect(data.sandboxes.persist.model).toBe("m1"); + expect(data.defaultSandbox).toBe("persist"); }); it("handles corrupt registry file gracefully", () => { @@ -100,6 +98,6 @@ describe("registry", () => { fs.writeFileSync(regFile, "NOT JSON"); // Should not throw, returns empty const { sandboxes } = registry.listSandboxes(); - assert.equal(sandboxes.length, 0); + expect(sandboxes.length).toBe(0); }); }); diff --git a/test/runner.test.js b/test/runner.test.js index ffe064fc..a0547181 100644 --- a/test/runner.test.js +++ b/test/runner.test.js @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -const { describe, it } = require("node:test"); -const assert = require("node:assert/strict"); const path = require("node:path"); const childProcess = require("node:child_process"); const { spawnSync } = childProcess; @@ -26,8 +24,8 @@ describe("runner helpers", () => { input: "preserved-answer\n", }); - assert.equal(result.status, 0); - assert.equal(result.stdout, "preserved-answer\n"); + expect(result.status).toBe(0); + expect(result.stdout).toBe("preserved-answer\n"); }); it("uses inherited stdio for interactive commands only", () => { @@ -48,9 +46,9 @@ describe("runner helpers", () => { delete require.cache[require.resolve(runnerPath)]; } - assert.equal(calls.length, 2); - assert.deepEqual(calls[0][2].stdio, ["ignore", "inherit", "inherit"]); - assert.equal(calls[1][2].stdio, "inherit"); + expect(calls.length).toBe(2); + expect(calls[0][2].stdio).toEqual(["ignore", "inherit", "inherit"]); + expect(calls[1][2].stdio).toBe("inherit"); }); it("preserves process env when opts.env is provided", () => { @@ -77,29 +75,29 @@ describe("runner helpers", () => { delete require.cache[require.resolve(runnerPath)]; } - assert.equal(calls.length, 1); - assert.equal(calls[0][2].env.OPENSHELL_CLUSTER_IMAGE, "ghcr.io/nvidia/openshell/cluster:0.0.12"); - assert.equal(calls[0][2].env.PATH, "/usr/local/bin:/usr/bin"); + expect(calls.length).toBe(1); + expect(calls[0][2].env.OPENSHELL_CLUSTER_IMAGE).toBe("ghcr.io/nvidia/openshell/cluster:0.0.12"); + expect(calls[0][2].env.PATH).toBe("/usr/local/bin:/usr/bin"); }); describe("shellQuote", () => { it("wraps in single quotes", () => { const { shellQuote } = require(runnerPath); - assert.equal(shellQuote("hello"), "'hello'"); + expect(shellQuote("hello")).toBe("'hello'"); }); it("escapes embedded single quotes", () => { const { shellQuote } = require(runnerPath); - assert.equal(shellQuote("it's"), "'it'\\''s'"); + expect(shellQuote("it's")).toBe("'it'\\''s'"); }); it("neutralizes shell metacharacters", () => { const { shellQuote } = require(runnerPath); const dangerous = "test; rm -rf /"; const quoted = shellQuote(dangerous); - assert.equal(quoted, "'test; rm -rf /'"); + expect(quoted).toBe("'test; rm -rf /'"); const result = spawnSync("bash", ["-c", `echo ${quoted}`], { encoding: "utf-8" }); - assert.equal(result.stdout.trim(), dangerous); + expect(result.stdout.trim()).toBe(dangerous); }); it("handles backticks and dollar signs", () => { @@ -107,39 +105,39 @@ describe("runner helpers", () => { const payload = "test`whoami`$HOME"; const quoted = shellQuote(payload); const result = spawnSync("bash", ["-c", `echo ${quoted}`], { encoding: "utf-8" }); - assert.equal(result.stdout.trim(), payload); + expect(result.stdout.trim()).toBe(payload); }); }); describe("validateName", () => { it("accepts valid RFC 1123 names", () => { const { validateName } = require(runnerPath); - assert.equal(validateName("my-sandbox"), "my-sandbox"); - assert.equal(validateName("test123"), "test123"); - assert.equal(validateName("a"), "a"); + expect(validateName("my-sandbox")).toBe("my-sandbox"); + expect(validateName("test123")).toBe("test123"); + expect(validateName("a")).toBe("a"); }); it("rejects names with shell metacharacters", () => { const { validateName } = require(runnerPath); - assert.throws(() => validateName("test; whoami"), /Invalid/); - assert.throws(() => validateName("test`id`"), /Invalid/); - assert.throws(() => validateName("test$(cat /etc/passwd)"), /Invalid/); - assert.throws(() => validateName("../etc/passwd"), /Invalid/); + expect(() => validateName("test; whoami")).toThrow(/Invalid/); + expect(() => validateName("test`id`")).toThrow(/Invalid/); + expect(() => validateName("test$(cat /etc/passwd)")).toThrow(/Invalid/); + expect(() => validateName("../etc/passwd")).toThrow(/Invalid/); }); it("rejects empty and overlength names", () => { const { validateName } = require(runnerPath); - assert.throws(() => validateName(""), /required/); - assert.throws(() => validateName(null), /required/); - assert.throws(() => validateName("a".repeat(64)), /too long/); + expect(() => validateName("")).toThrow(/required/); + expect(() => validateName(null)).toThrow(/required/); + expect(() => validateName("a".repeat(64))).toThrow(/too long/); }); it("rejects uppercase and special characters", () => { const { validateName } = require(runnerPath); - assert.throws(() => validateName("MyBox"), /Invalid/); - assert.throws(() => validateName("my_box"), /Invalid/); - assert.throws(() => validateName("-leading"), /Invalid/); - assert.throws(() => validateName("trailing-"), /Invalid/); + expect(() => validateName("MyBox")).toThrow(/Invalid/); + expect(() => validateName("my_box")).toThrow(/Invalid/); + expect(() => validateName("-leading")).toThrow(/Invalid/); + expect(() => validateName("trailing-")).toThrow(/Invalid/); }); }); @@ -150,7 +148,7 @@ describe("runner helpers", () => { const lines = src.split("\n"); for (let i = 0; i < lines.length; i++) { if (lines[i].includes("execSync") && !lines[i].includes("execFileSync")) { - assert.fail(`bin/nemoclaw.js:${i + 1} uses execSync — use execFileSync instead`); + expect.unreachable(`bin/nemoclaw.js:${i + 1} uses execSync — use execFileSync instead`); } } }); @@ -174,8 +172,8 @@ describe("runner helpers", () => { defs.push(file.replace(binDir, "bin")); } } - assert.equal(defs.length, 1, `Expected 1 shellQuote definition, found ${defs.length}: ${defs.join(", ")}`); - assert.ok(defs[0].includes("runner"), `shellQuote should be in runner.js, found in ${defs[0]}`); + expect(defs.length).toBe(1); + expect(defs[0].includes("runner")).toBeTruthy(); }); it("CLI rejects malicious sandbox names before shell commands (e2e)", () => { @@ -193,8 +191,8 @@ describe("runner helpers", () => { timeout: 10000, cwd: path.join(__dirname, ".."), }); - assert.notEqual(result.status, 0, "CLI should reject malicious sandbox name"); - assert.equal(fs.existsSync(canary), false, "shell payload must never execute"); + expect(result.status).not.toBe(0); + expect(fs.existsSync(canary)).toBe(false); } finally { fs.rmSync(canaryDir, { recursive: true, force: true }); } @@ -203,8 +201,8 @@ describe("runner helpers", () => { it("telegram bridge validates SANDBOX_NAME on startup", () => { const fs = require("fs"); const src = fs.readFileSync(path.join(__dirname, "..", "scripts", "telegram-bridge.js"), "utf-8"); - assert.ok(src.includes("validateName(SANDBOX"), "telegram-bridge.js must validate SANDBOX_NAME"); - assert.ok(!src.includes("execSync"), "telegram-bridge.js should not use execSync"); + expect(src.includes("validateName(SANDBOX")).toBeTruthy(); + expect(!src.includes("execSync")).toBeTruthy(); }); }); }); diff --git a/test/runtime-shell.test.js b/test/runtime-shell.test.js index 979460b9..e80ce7c6 100644 --- a/test/runtime-shell.test.js +++ b/test/runtime-shell.test.js @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -const { afterEach, describe, it } = require("node:test"); -const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); @@ -27,8 +25,8 @@ describe("shell runtime helpers", () => { HOME: "/tmp/unused-home", }); - assert.equal(result.status, 0); - assert.equal(result.stdout.trim(), "unix:///custom/docker.sock"); + expect(result.status).toBe(0); + expect(result.stdout.trim()).toBe("unix:///custom/docker.sock"); }); it("prefers Colima over Docker Desktop", () => { @@ -41,8 +39,8 @@ describe("shell runtime helpers", () => { NEMOCLAW_TEST_SOCKET_PATHS: `${colimaSocket}:${dockerDesktopSocket}`, }); - assert.equal(result.status, 0); - assert.equal(result.stdout.trim(), `unix://${colimaSocket}`); + expect(result.status).toBe(0); + expect(result.stdout.trim()).toBe(`unix://${colimaSocket}`); fs.rmSync(home, { recursive: true, force: true }); }); @@ -55,16 +53,16 @@ describe("shell runtime helpers", () => { NEMOCLAW_TEST_SOCKET_PATHS: dockerDesktopSocket, }); - assert.equal(result.status, 0); - assert.equal(result.stdout.trim(), `unix://${dockerDesktopSocket}`); + expect(result.status).toBe(0); + expect(result.stdout.trim()).toBe(`unix://${dockerDesktopSocket}`); fs.rmSync(home, { recursive: true, force: true }); }); it("classifies a Docker Desktop DOCKER_HOST correctly", () => { const result = runShell(`source "${RUNTIME_SH}"; docker_host_runtime "unix:///Users/test/.docker/run/docker.sock"`); - assert.equal(result.status, 0); - assert.equal(result.stdout.trim(), "docker-desktop"); + expect(result.status).toBe(0); + expect(result.stdout.trim()).toBe("docker-desktop"); }); it("selects the matching gateway cluster when a gateway name is present", () => { @@ -73,8 +71,8 @@ describe("shell runtime helpers", () => { select_openshell_cluster_container "nemoclaw" $'openshell-cluster-alpha\\nopenshell-cluster-nemoclaw'`, ); - assert.equal(result.status, 0); - assert.equal(result.stdout.trim(), "openshell-cluster-nemoclaw"); + expect(result.status).toBe(0); + expect(result.stdout.trim()).toBe("openshell-cluster-nemoclaw"); }); it("fails on ambiguous cluster selection", () => { @@ -83,7 +81,7 @@ describe("shell runtime helpers", () => { select_openshell_cluster_container "" $'openshell-cluster-a\\nopenshell-cluster-b'`, ); - assert.notEqual(result.status, 0); + expect(result.status).not.toBe(0); }); it("finds the XDG Colima socket", () => { @@ -95,42 +93,42 @@ describe("shell runtime helpers", () => { NEMOCLAW_TEST_SOCKET_PATHS: xdgColimaSocket, }); - assert.equal(result.status, 0); - assert.equal(result.stdout.trim(), xdgColimaSocket); + expect(result.status).toBe(0); + expect(result.stdout.trim()).toBe(xdgColimaSocket); fs.rmSync(home, { recursive: true, force: true }); }); it("detects podman from docker info output", () => { const result = runShell(`source "${RUNTIME_SH}"; infer_container_runtime_from_info "podman version 5.4.1"`); - assert.equal(result.status, 0); - assert.equal(result.stdout.trim(), "podman"); + expect(result.status).toBe(0); + expect(result.stdout.trim()).toBe("podman"); }); it("flags podman on macOS as unsupported", () => { const result = runShell(`source "${RUNTIME_SH}"; is_unsupported_macos_runtime Darwin podman`); - assert.equal(result.status, 0); + expect(result.status).toBe(0); }); it("does not flag podman on Linux", () => { const result = runShell(`source "${RUNTIME_SH}"; is_unsupported_macos_runtime Linux podman`); - assert.notEqual(result.status, 0); + expect(result.status).not.toBe(0); }); it("returns the vllm-local base URL", () => { const result = runShell(`source "${RUNTIME_SH}"; get_local_provider_base_url vllm-local`); - assert.equal(result.status, 0); - assert.equal(result.stdout.trim(), "http://host.openshell.internal:8000/v1"); + expect(result.status).toBe(0); + expect(result.stdout.trim()).toBe("http://host.openshell.internal:8000/v1"); }); it("returns the ollama-local base URL", () => { const result = runShell(`source "${RUNTIME_SH}"; get_local_provider_base_url ollama-local`); - assert.equal(result.status, 0); - assert.equal(result.stdout.trim(), "http://host.openshell.internal:11434/v1"); + expect(result.status).toBe(0); + expect(result.stdout.trim()).toBe("http://host.openshell.internal:11434/v1"); }); it("rejects unknown local providers", () => { const result = runShell(`source "${RUNTIME_SH}"; get_local_provider_base_url bogus-provider`); - assert.notEqual(result.status, 0); + expect(result.status).not.toBe(0); }); it("returns the first non-loopback nameserver", () => { @@ -138,8 +136,8 @@ describe("shell runtime helpers", () => { `source "${RUNTIME_SH}"; first_non_loopback_nameserver $'nameserver 127.0.0.11\\nnameserver 10.0.0.2'`, ); - assert.equal(result.status, 0); - assert.equal(result.stdout.trim(), "10.0.0.2"); + expect(result.status).toBe(0); + expect(result.stdout.trim()).toBe("10.0.0.2"); }); it("prefers the container nameserver when it is not loopback", () => { @@ -147,8 +145,8 @@ describe("shell runtime helpers", () => { `source "${RUNTIME_SH}"; resolve_coredns_upstream $'nameserver 10.0.0.2' $'nameserver 1.1.1.1' colima`, ); - assert.equal(result.status, 0); - assert.equal(result.stdout.trim(), "10.0.0.2"); + expect(result.status).toBe(0); + expect(result.stdout.trim()).toBe("10.0.0.2"); }); it("falls back to the Colima VM nameserver when the container resolver is loopback", () => { @@ -158,8 +156,8 @@ describe("shell runtime helpers", () => { resolve_coredns_upstream $'nameserver 127.0.0.11' $'nameserver 1.1.1.1' colima`, ); - assert.equal(result.status, 0); - assert.equal(result.stdout.trim(), "192.168.5.1"); + expect(result.status).toBe(0); + expect(result.stdout.trim()).toBe("192.168.5.1"); }); it("falls back to the host nameserver when no Colima VM nameserver is available", () => { @@ -169,8 +167,8 @@ describe("shell runtime helpers", () => { resolve_coredns_upstream $'nameserver 127.0.0.11' $'nameserver 9.9.9.9' colima`, ); - assert.equal(result.status, 0); - assert.equal(result.stdout.trim(), "9.9.9.9"); + expect(result.status).toBe(0); + expect(result.stdout.trim()).toBe("9.9.9.9"); }); it("does not consume installer stdin when reading the Colima VM nameserver", () => { @@ -183,7 +181,7 @@ describe("shell runtime helpers", () => { }`, ); - assert.equal(result.status, 0); - assert.equal(result.stdout.trim(), "sandbox-answer"); + expect(result.status).toBe(0); + expect(result.stdout.trim()).toBe("sandbox-answer"); }); }); diff --git a/test/security-c2-dockerfile-injection.test.js b/test/security-c2-dockerfile-injection.test.js index e167a5c4..084a7002 100644 --- a/test/security-c2-dockerfile-injection.test.js +++ b/test/security-c2-dockerfile-injection.test.js @@ -11,8 +11,6 @@ "use strict"; -const { describe, it } = require("node:test"); -const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); @@ -58,15 +56,15 @@ describe("C-2 PoC: vulnerable pattern (ARG interpolation into python3 -c)", () = it("benign URL works in the vulnerable pattern (baseline)", () => { const src = vulnerableSource("http://127.0.0.1:18789"); const result = runPython(src); - assert.equal(result.status, 0, `python3 exit ${result.status}: ${result.stderr}`); - assert.ok(result.stdout.includes("127.0.0.1")); + expect(result.status).toBe(0); + expect(result.stdout.includes("127.0.0.1")).toBeTruthy(); }); it("single-quote in URL causes SyntaxError", () => { const src = vulnerableSource("http://x'.evil.com"); const result = runPython(src); - assert.notEqual(result.status, 0, "Expected non-zero exit (SyntaxError)"); - assert.ok(result.stderr.includes("SyntaxError")); + expect(result.status).not.toBe(0); + expect(result.stderr.includes("SyntaxError")).toBeTruthy(); }); it("injection payload writes canary file — arbitrary Python executes", () => { @@ -76,11 +74,8 @@ describe("C-2 PoC: vulnerable pattern (ARG interpolation into python3 -c)", () = const src = vulnerableSource(payload); runPython(src); - assert.ok( - fs.existsSync(canary), - "Canary file must exist — injection payload executed arbitrary Python", - ); - assert.equal(fs.readFileSync(canary, "utf-8"), "PWNED"); + expect(fs.existsSync(canary)).toBeTruthy(); + expect(fs.readFileSync(canary, "utf-8")).toBe("PWNED"); } finally { try { fs.unlinkSync(canary); } catch { /* cleanup */ } } @@ -93,14 +88,14 @@ describe("C-2 PoC: vulnerable pattern (ARG interpolation into python3 -c)", () = describe("C-2 fix: env var pattern (os.environ) is safe", () => { it("benign URL works through env var", () => { const result = runPython(fixedSource(), { CHAT_UI_URL: "http://127.0.0.1:18789" }); - assert.equal(result.status, 0); - assert.ok(result.stdout.includes("127.0.0.1")); + expect(result.status).toBe(0); + expect(result.stdout.includes("127.0.0.1")).toBeTruthy(); }); it("single-quote in URL is treated as data, not a code boundary", () => { const result = runPython(fixedSource(), { CHAT_UI_URL: "http://x'.evil.com" }); - assert.equal(result.status, 0, `Expected exit 0: ${result.stderr}`); - assert.ok(result.stdout.includes("x'.evil.com")); + expect(result.status).toBe(0); + expect(result.stdout.includes("x'.evil.com")).toBeTruthy(); }); it("injection payload does NOT execute — URL is inert data", () => { @@ -109,12 +104,8 @@ describe("C-2 fix: env var pattern (os.environ) is safe", () => { const payload = `http://x'; open('${canary}','w').write('PWNED') #`; const result = runPython(fixedSource(), { CHAT_UI_URL: payload }); - assert.equal(result.status, 0); - assert.equal( - fs.existsSync(canary), - false, - "Canary file must NOT exist — injection payload must not execute", - ); + expect(result.status).toBe(0); + expect(fs.existsSync(canary)).toBe(false); } finally { try { fs.unlinkSync(canary); } catch { /* cleanup */ } } @@ -127,10 +118,7 @@ describe("C-2 fix: env var pattern (os.environ) is safe", () => { // the key property is that no code injection occurs. Check stdout or stderr // does NOT contain evidence of os.system/subprocess execution. const combined = result.stdout + result.stderr; - assert.ok( - !combined.includes("uid="), - "No command execution output should appear — URL is inert data", - ); + expect(!combined.includes("uid=")).toBeTruthy(); }); }); @@ -149,10 +137,10 @@ describe("C-2 regression: Dockerfile must not interpolate build-args into Python inPythonRunBlock = true; } if (inPythonRunBlock && vulnerablePattern.test(line)) { - assert.fail( + expect.unreachable( `Dockerfile:${i + 1} interpolates CHAT_UI_URL into a Python string literal.\n` + ` Line: ${line.trim()}\n` + - ` Fix: use os.environ['CHAT_UI_URL'] instead.`, + ` Fix: use os.environ['CHAT_UI_URL'] instead.` ); } if (inPythonRunBlock && !/\\\s*$/.test(line)) { @@ -172,10 +160,10 @@ describe("C-2 regression: Dockerfile must not interpolate build-args into Python inPythonRunBlock = true; } if (inPythonRunBlock && vulnerablePattern.test(line)) { - assert.fail( + expect.unreachable( `Dockerfile:${i + 1} interpolates NEMOCLAW_MODEL into a Python string literal.\n` + ` Line: ${line.trim()}\n` + - ` Fix: use os.environ['NEMOCLAW_MODEL'] instead.`, + ` Fix: use os.environ['NEMOCLAW_MODEL'] instead.` ); } if (inPythonRunBlock && !/\\\s*$/.test(line)) { @@ -210,17 +198,11 @@ describe("C-2 regression: Dockerfile must not interpolate build-args into Python } // Verify promotion happened before the python3 -c RUN layer if (/^\s*RUN\b.*python3\s+-c\b/.test(line)) { - assert.ok( - chatUiUrlPromoted, - `Dockerfile:${i + 1} has a python3 -c RUN layer but CHAT_UI_URL was not promoted via ENV before it`, - ); + expect(chatUiUrlPromoted).toBeTruthy(); return; // Found the RUN layer and verified — done } } - assert.ok( - chatUiUrlPromoted, - "Dockerfile must have ENV instruction that promotes CHAT_UI_URL from ARG to env var before the python3 -c RUN layer", - ); + expect(chatUiUrlPromoted).toBeTruthy(); }); it("Python script uses os.environ to read CHAT_UI_URL", () => { @@ -247,7 +229,7 @@ describe("C-2 regression: Dockerfile must not interpolate build-args into Python inPythonRunBlock = false; } } - assert.ok(hasEnvRead, "Python script in the python3 -c RUN block must read CHAT_UI_URL via os.environ"); + expect(hasEnvRead).toBeTruthy(); }); it("Dockerfile promotes NEMOCLAW_MODEL to ENV before the RUN layer", () => { @@ -276,17 +258,11 @@ describe("C-2 regression: Dockerfile must not interpolate build-args into Python } // Verify promotion happened before the python3 -c RUN layer if (/^\s*RUN\b.*python3\s+-c\b/.test(line)) { - assert.ok( - nemoModelPromoted, - `Dockerfile:${i + 1} has a python3 -c RUN layer but NEMOCLAW_MODEL was not promoted via ENV before it`, - ); + expect(nemoModelPromoted).toBeTruthy(); return; // Found the RUN layer and verified — done } } - assert.ok( - nemoModelPromoted, - "Dockerfile must have ENV instruction that promotes NEMOCLAW_MODEL from ARG to env var before the python3 -c RUN layer", - ); + expect(nemoModelPromoted).toBeTruthy(); }); it("Python script uses os.environ to read NEMOCLAW_MODEL", () => { @@ -313,6 +289,6 @@ describe("C-2 regression: Dockerfile must not interpolate build-args into Python inPythonRunBlock = false; } } - assert.ok(hasEnvRead, "Python script in the python3 -c RUN block must read NEMOCLAW_MODEL via os.environ"); + expect(hasEnvRead).toBeTruthy(); }); }); diff --git a/test/security-c4-manifest-traversal.test.js b/test/security-c4-manifest-traversal.test.js index b44a09b7..7deec47f 100644 --- a/test/security-c4-manifest-traversal.test.js +++ b/test/security-c4-manifest-traversal.test.js @@ -11,8 +11,6 @@ "use strict"; -const { describe, it } = require("node:test"); -const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); @@ -200,16 +198,10 @@ describe("C-4 PoC: vulnerable restoreSnapshotToHost allows path traversal", () = const { result, written } = restoreVulnerable(snapshotDir); // Vulnerable code writes to the traversal target - assert.ok(result, "vulnerable code should succeed (no validation)"); - assert.ok(written, "vulnerable code writes to disk"); - assert.ok( - fs.existsSync(path.join(traversalTarget, "sentinel.txt")), - "sentinel.txt must exist at traversal target — proves arbitrary write", - ); - assert.equal( - fs.readFileSync(path.join(traversalTarget, "sentinel.txt"), "utf-8"), - "attacker-controlled-content", - ); + expect(result).toBeTruthy(); + expect(written).toBeTruthy(); + expect(fs.existsSync(path.join(traversalTarget, "sentinel.txt"))).toBeTruthy(); + expect(fs.readFileSync(path.join(traversalTarget, "sentinel.txt"), "utf-8")).toBe("attacker-controlled-content"); } finally { fs.rmSync(workDir, { recursive: true, force: true }); } @@ -236,11 +228,8 @@ describe("C-4 PoC: vulnerable restoreSnapshotToHost allows path traversal", () = const { result } = restoreVulnerable(snapshotDir); - assert.ok(result, "vulnerable code should succeed"); - assert.ok( - fs.existsSync(evilConfigPath), - "config written to traversal target — proves arbitrary file write", - ); + expect(result).toBeTruthy(); + expect(fs.existsSync(evilConfigPath)).toBeTruthy(); } finally { fs.rmSync(workDir, { recursive: true, force: true }); } @@ -272,10 +261,10 @@ describe("C-4 fix: restoreSnapshotToHost rejects path traversal", () => { // Pass homeDir as trustedRoot to simulate resolveHostHome() const { result, errors, written } = restoreFixed(snapshotDir, homeDir); - assert.equal(result, false, "fixed code must reject traversal"); - assert.equal(written, false, "no files must be written"); - assert.ok(!fs.existsSync(traversalTarget), "traversal target must not be created"); - assert.ok(errors[0].includes("outside the trusted host root")); + expect(result).toBe(false); + expect(written).toBe(false); + expect(!fs.existsSync(traversalTarget)).toBeTruthy(); + expect(errors[0].includes("outside the trusted host root")).toBeTruthy(); } finally { fs.rmSync(workDir, { recursive: true, force: true }); } @@ -302,9 +291,9 @@ describe("C-4 fix: restoreSnapshotToHost rejects path traversal", () => { const { result, errors } = restoreFixed(snapshotDir, homeDir); - assert.equal(result, false, "fixed code must reject configPath traversal"); - assert.ok(!fs.existsSync(evilConfigPath), "evil config must not be written"); - assert.ok(errors[0].includes("outside the trusted host root")); + expect(result).toBe(false); + expect(!fs.existsSync(evilConfigPath)).toBeTruthy(); + expect(errors[0].includes("outside the trusted host root")).toBeTruthy(); } finally { fs.rmSync(workDir, { recursive: true, force: true }); } @@ -329,8 +318,8 @@ describe("C-4 fix: restoreSnapshotToHost rejects path traversal", () => { }); const { result } = restoreFixed(snapshotDir, homeDir); - assert.equal(result, false, "sibling path must be rejected"); - assert.ok(!fs.existsSync(siblingDir)); + expect(result).toBe(false); + expect(!fs.existsSync(siblingDir)).toBeTruthy(); } finally { fs.rmSync(workDir, { recursive: true, force: true }); } @@ -355,12 +344,9 @@ describe("C-4 fix: restoreSnapshotToHost rejects path traversal", () => { const { result, errors, written } = restoreFixed(snapshotDir, trustedRoot); - assert.equal(result, false, "homeDir=/ must be rejected"); - assert.equal(written, false, "no files must be written"); - assert.ok( - errors[0].includes("homeDir is outside the trusted host root"), - `expected homeDir rejection, got: ${errors[0]}`, - ); + expect(result).toBe(false); + expect(written).toBe(false); + expect(errors[0].includes("homeDir is outside the trusted host root")).toBeTruthy(); } finally { fs.rmSync(workDir, { recursive: true, force: true }); } @@ -387,10 +373,10 @@ describe("C-4 fix: restoreSnapshotToHost rejects path traversal", () => { // trustedRoot = homeDir (simulates resolveHostHome() returning this dir) const { result, errors, written } = restoreFixed(snapshotDir, homeDir); - assert.equal(result, true, "legitimate path must succeed"); - assert.equal(errors.length, 0); - assert.ok(written); - assert.ok(fs.existsSync(path.join(legitimateStateDir, "sentinel.txt"))); + expect(result).toBe(true); + expect(errors.length).toBe(0); + expect(written).toBeTruthy(); + expect(fs.existsSync(path.join(legitimateStateDir, "sentinel.txt"))).toBeTruthy(); } finally { fs.rmSync(workDir, { recursive: true, force: true }); } @@ -417,9 +403,9 @@ describe("C-4 fix: restoreSnapshotToHost rejects path traversal", () => { const { result, errors } = restoreFixed(snapshotDir, homeDir); - assert.equal(result, true, "legitimate config path must succeed"); - assert.equal(errors.length, 0); - assert.ok(fs.existsSync(legitimateConfigPath)); + expect(result).toBe(true); + expect(errors.length).toBe(0); + expect(fs.existsSync(legitimateConfigPath)).toBeTruthy(); } finally { fs.rmSync(workDir, { recursive: true, force: true }); } @@ -437,40 +423,28 @@ describe("C-4 regression: migration-state.ts contains path validation", () => { "utf-8", ); const fnStart = src.indexOf("function restoreSnapshotToHost"); - assert.ok(fnStart !== -1, "restoreSnapshotToHost must exist in migration-state.ts"); + expect(fnStart !== -1).toBeTruthy(); return src.slice(fnStart); } it("restoreSnapshotToHost calls isWithinRoot on manifest.stateDir", () => { const fnBody = getRestoreFnBody(); - assert.ok( - /isWithinRoot\s*\(\s*manifest\.stateDir/.test(fnBody), - "restoreSnapshotToHost must call isWithinRoot on manifest.stateDir", - ); + expect(/isWithinRoot\s*\(\s*manifest\.stateDir/.test(fnBody)).toBeTruthy(); }); it("restoreSnapshotToHost calls isWithinRoot on manifest.configPath", () => { const fnBody = getRestoreFnBody(); - assert.ok( - /isWithinRoot\s*\(\s*manifest\.configPath/.test(fnBody), - "restoreSnapshotToHost must call isWithinRoot on manifest.configPath", - ); + expect(/isWithinRoot\s*\(\s*manifest\.configPath/.test(fnBody)).toBeTruthy(); }); it("restoreSnapshotToHost validates manifest.homeDir against trusted root", () => { const fnBody = getRestoreFnBody(); - assert.ok( - /isWithinRoot\s*\(\s*manifest\.homeDir/.test(fnBody), - "restoreSnapshotToHost must call isWithinRoot on manifest.homeDir", - ); + expect(/isWithinRoot\s*\(\s*manifest\.homeDir/.test(fnBody)).toBeTruthy(); }); it("restoreSnapshotToHost fails closed when hasExternalConfig is true with missing configPath", () => { const fnBody = getRestoreFnBody(); - assert.ok( - /manifest\.hasExternalConfig\b/.test(fnBody) && - /typeof\s+manifest\.configPath\s*!==\s*["']string["']/.test(fnBody), - "restoreSnapshotToHost must type-check manifest.configPath when hasExternalConfig is true", - ); + expect(/manifest\.hasExternalConfig\b/.test(fnBody) && + /typeof\s+manifest\.configPath\s*!==\s*["']string["']/.test(fnBody)).toBeTruthy(); }); }); diff --git a/test/service-env.test.js b/test/service-env.test.js index cd822085..38f1f3f7 100644 --- a/test/service-env.test.js +++ b/test/service-env.test.js @@ -1,84 +1,62 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -const { describe, it } = require("node:test"); -const assert = require("node:assert/strict"); const { execSync } = require("child_process"); const { resolveOpenshell } = require("../bin/lib/resolve-openshell"); describe("service environment", () => { describe("resolveOpenshell logic", () => { it("returns command -v result when absolute path", () => { - assert.equal( - resolveOpenshell({ commandVResult: "/usr/bin/openshell" }), - "/usr/bin/openshell" - ); + expect(resolveOpenshell({ commandVResult: "/usr/bin/openshell" })).toBe("/usr/bin/openshell"); }); it("rejects non-absolute command -v result (alias)", () => { - assert.equal( - resolveOpenshell({ commandVResult: "openshell", checkExecutable: () => false }), - null - ); + expect( + resolveOpenshell({ commandVResult: "openshell", checkExecutable: () => false }) + ).toBe(null); }); it("rejects alias definition from command -v", () => { - assert.equal( - resolveOpenshell({ commandVResult: "alias openshell='echo pwned'", checkExecutable: () => false }), - null - ); + expect( + resolveOpenshell({ commandVResult: "alias openshell='echo pwned'", checkExecutable: () => false }) + ).toBe(null); }); it("falls back to ~/.local/bin when command -v fails", () => { - assert.equal( - resolveOpenshell({ - commandVResult: null, - checkExecutable: (p) => p === "/fakehome/.local/bin/openshell", - home: "/fakehome", - }), - "/fakehome/.local/bin/openshell" - ); + expect(resolveOpenshell({ + commandVResult: null, + checkExecutable: (p) => p === "/fakehome/.local/bin/openshell", + home: "/fakehome", + })).toBe("/fakehome/.local/bin/openshell"); }); it("falls back to /usr/local/bin", () => { - assert.equal( - resolveOpenshell({ - commandVResult: null, - checkExecutable: (p) => p === "/usr/local/bin/openshell", - }), - "/usr/local/bin/openshell" - ); + expect(resolveOpenshell({ + commandVResult: null, + checkExecutable: (p) => p === "/usr/local/bin/openshell", + })).toBe("/usr/local/bin/openshell"); }); it("falls back to /usr/bin", () => { - assert.equal( - resolveOpenshell({ - commandVResult: null, - checkExecutable: (p) => p === "/usr/bin/openshell", - }), - "/usr/bin/openshell" - ); + expect(resolveOpenshell({ + commandVResult: null, + checkExecutable: (p) => p === "/usr/bin/openshell", + })).toBe("/usr/bin/openshell"); }); it("prefers ~/.local/bin over /usr/local/bin", () => { - assert.equal( - resolveOpenshell({ - commandVResult: null, - checkExecutable: (p) => p === "/fakehome/.local/bin/openshell" || p === "/usr/local/bin/openshell", - home: "/fakehome", - }), - "/fakehome/.local/bin/openshell" - ); + expect(resolveOpenshell({ + commandVResult: null, + checkExecutable: (p) => p === "/fakehome/.local/bin/openshell" || p === "/usr/local/bin/openshell", + home: "/fakehome", + })).toBe("/fakehome/.local/bin/openshell"); }); it("returns null when openshell not found anywhere", () => { - assert.equal( - resolveOpenshell({ - commandVResult: null, - checkExecutable: () => false, - }), - null - ); + expect(resolveOpenshell({ + commandVResult: null, + checkExecutable: () => false, + })).toBe(null); }); }); @@ -91,7 +69,7 @@ describe("service environment", () => { env: { ...process.env, NEMOCLAW_SANDBOX: "", SANDBOX_NAME: "my-box" }, } ).trim(); - assert.equal(result, "my-box"); + expect(result).toBe("my-box"); }); it("start-services.sh uses NEMOCLAW_SANDBOX over SANDBOX_NAME", () => { @@ -102,7 +80,7 @@ describe("service environment", () => { env: { ...process.env, NEMOCLAW_SANDBOX: "from-env", SANDBOX_NAME: "old" }, } ).trim(); - assert.equal(result, "from-env"); + expect(result).toBe("from-env"); }); it("start-services.sh falls back to default when both unset", () => { @@ -113,7 +91,7 @@ describe("service environment", () => { env: { ...process.env, NEMOCLAW_SANDBOX: "", SANDBOX_NAME: "" }, } ).trim(); - assert.equal(result, "default"); + expect(result).toBe("default"); }); }); }); diff --git a/test/setup-sandbox-name.test.js b/test/setup-sandbox-name.test.js index c454c012..4054daba 100644 --- a/test/setup-sandbox-name.test.js +++ b/test/setup-sandbox-name.test.js @@ -6,8 +6,6 @@ // // See: https://github.com/NVIDIA/NemoClaw/issues/197 -const { describe, it } = require("node:test"); -const assert = require("node:assert/strict"); const fs = require("node:fs"); const path = require("node:path"); const { execSync } = require("node:child_process"); @@ -18,48 +16,36 @@ describe("setup.sh sandbox name parameterization (#197)", () => { const content = fs.readFileSync(path.join(ROOT, "scripts/setup.sh"), "utf-8"); it("accepts sandbox name as $1 with default", () => { - assert.ok( - content.includes('SANDBOX_NAME="${1:-nemoclaw}"'), - 'setup.sh must accept sandbox name as $1 with default "nemoclaw"' - ); + expect(content.includes('SANDBOX_NAME="${1:-nemoclaw}"')).toBeTruthy(); }); it("sandbox create uses $SANDBOX_NAME, not hardcoded", () => { const createLine = content.match(/openshell sandbox create.*--name\s+(\S+)/); - assert.ok(createLine, "Could not find openshell sandbox create --name"); - assert.ok( - createLine[1].includes("$SANDBOX_NAME") || createLine[1].includes('"$SANDBOX_NAME"'), - `sandbox create --name must use $SANDBOX_NAME, found: ${createLine[1]}` - ); + expect(createLine).toBeTruthy(); + expect( + createLine[1].includes("$SANDBOX_NAME") || createLine[1].includes('"$SANDBOX_NAME"') + ).toBeTruthy(); }); it("sandbox delete uses $SANDBOX_NAME, not hardcoded", () => { const deleteLine = content.match(/openshell sandbox delete\s+(\S+)/); - assert.ok(deleteLine, "Could not find openshell sandbox delete"); - assert.ok( - deleteLine[1].includes("$SANDBOX_NAME") || deleteLine[1].includes('"$SANDBOX_NAME"'), - `sandbox delete must use $SANDBOX_NAME, found: ${deleteLine[1]}` - ); + expect(deleteLine).toBeTruthy(); + expect( + deleteLine[1].includes("$SANDBOX_NAME") || deleteLine[1].includes('"$SANDBOX_NAME"') + ).toBeTruthy(); }); it("sandbox get uses $SANDBOX_NAME, not hardcoded", () => { const getLine = content.match(/openshell sandbox get\s+(\S+)/); - assert.ok(getLine, "Could not find openshell sandbox get"); - assert.ok( - getLine[1].includes("$SANDBOX_NAME") || getLine[1].includes('"$SANDBOX_NAME"'), - `sandbox get must use $SANDBOX_NAME, found: ${getLine[1]}` - ); + expect(getLine).toBeTruthy(); + expect( + getLine[1].includes("$SANDBOX_NAME") || getLine[1].includes('"$SANDBOX_NAME"') + ).toBeTruthy(); }); it("gateway name stays hardcoded to nemoclaw", () => { - assert.ok( - content.includes("gateway destroy -g nemoclaw"), - "gateway destroy must use hardcoded nemoclaw (gateway != sandbox)" - ); - assert.ok( - content.includes("--name nemoclaw"), - "gateway start --name must use hardcoded nemoclaw" - ); + expect(content.includes("gateway destroy -g nemoclaw")).toBeTruthy(); + expect(content.includes("--name nemoclaw")).toBeTruthy(); }); it("$1 arg actually sets SANDBOX_NAME in bash", () => { @@ -67,7 +53,7 @@ describe("setup.sh sandbox name parameterization (#197)", () => { 'bash -c \'SANDBOX_NAME="${1:-nemoclaw}"; echo "$SANDBOX_NAME"\' -- my-test-box', { encoding: "utf-8" } ).trim(); - assert.equal(result, "my-test-box"); + expect(result).toBe("my-test-box"); }); it("no arg defaults to nemoclaw in bash", () => { @@ -75,6 +61,6 @@ describe("setup.sh sandbox name parameterization (#197)", () => { 'bash -c \'SANDBOX_NAME="${1:-nemoclaw}"; echo "$SANDBOX_NAME"\'', { encoding: "utf-8" } ).trim(); - assert.equal(result, "nemoclaw"); + expect(result).toBe("nemoclaw"); }); }); diff --git a/test/smoke-macos-install.test.js b/test/smoke-macos-install.test.js index 6d6e44c5..71af2bfd 100644 --- a/test/smoke-macos-install.test.js +++ b/test/smoke-macos-install.test.js @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -const { describe, it } = require("node:test"); -const assert = require("node:assert/strict"); const path = require("node:path"); const { spawnSync } = require("node:child_process"); @@ -15,8 +13,8 @@ describe("macOS smoke install script guardrails", () => { encoding: "utf-8", }); - assert.equal(result.status, 0); - assert.match(result.stdout, /Usage: \.\/scripts\/smoke-macos-install\.sh/); + expect(result.status).toBe(0); + expect(result.stdout).toMatch(/Usage: \.\/scripts\/smoke-macos-install\.sh/); }); it("requires NVIDIA_API_KEY", () => { @@ -26,8 +24,8 @@ describe("macOS smoke install script guardrails", () => { env: { ...process.env, NVIDIA_API_KEY: "" }, }); - assert.notEqual(result.status, 0); - assert.match(`${result.stdout}${result.stderr}`, /NVIDIA_API_KEY must be set/); + expect(result.status).not.toBe(0); + expect(`${result.stdout}${result.stderr}`).toMatch(/NVIDIA_API_KEY must be set/); }); it("rejects invalid sandbox names", () => { @@ -37,8 +35,8 @@ describe("macOS smoke install script guardrails", () => { env: { ...process.env, NVIDIA_API_KEY: "nvapi-test" }, }); - assert.notEqual(result.status, 0); - assert.match(`${result.stdout}${result.stderr}`, /Invalid sandbox name/); + expect(result.status).not.toBe(0); + expect(`${result.stdout}${result.stderr}`).toMatch(/Invalid sandbox name/); }); it("rejects unsupported runtimes", () => { @@ -48,8 +46,8 @@ describe("macOS smoke install script guardrails", () => { env: { ...process.env, NVIDIA_API_KEY: "nvapi-test" }, }); - assert.notEqual(result.status, 0); - assert.match(`${result.stdout}${result.stderr}`, /Unsupported runtime 'podman'/); + expect(result.status).not.toBe(0); + expect(`${result.stdout}${result.stderr}`).toMatch(/Unsupported runtime 'podman'/); }); it("fails when a requested runtime socket is unavailable", () => { @@ -63,8 +61,8 @@ describe("macOS smoke install script guardrails", () => { }, }); - assert.notEqual(result.status, 0); - assert.match(`${result.stdout}${result.stderr}`, /no Docker Desktop socket was found/); + expect(result.status).not.toBe(0); + expect(`${result.stdout}${result.stderr}`).toMatch(/no Docker Desktop socket was found/); }); it("stages the policy preset no answer after sandbox setup", () => { @@ -94,7 +92,7 @@ describe("macOS smoke install script guardrails", () => { env: { ...process.env, NVIDIA_API_KEY: "nvapi-test" }, }); - assert.equal(result.status, 0); - assert.equal(result.stdout, "smoke-test\nn\n"); + expect(result.status).toBe(0); + expect(result.stdout).toBe("smoke-test\nn\n"); }); }); diff --git a/test/uninstall.test.js b/test/uninstall.test.js index d0cececb..10ef2b59 100644 --- a/test/uninstall.test.js +++ b/test/uninstall.test.js @@ -1,8 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -const { describe, it } = require("node:test"); -const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); @@ -17,12 +15,12 @@ describe("uninstall CLI flags", () => { encoding: "utf-8", }); - assert.equal(result.status, 0); + expect(result.status).toBe(0); const output = `${result.stdout}${result.stderr}`; - assert.match(output, /NemoClaw Uninstaller/); - assert.match(output, /--yes/); - assert.match(output, /--keep-openshell/); - assert.match(output, /--delete-models/); + expect(output).toMatch(/NemoClaw Uninstaller/); + expect(output).toMatch(/--yes/); + expect(output).toMatch(/--keep-openshell/); + expect(output).toMatch(/--delete-models/); }); it("--yes skips the confirmation prompt and completes successfully", () => { @@ -47,11 +45,11 @@ describe("uninstall CLI flags", () => { }, }); - assert.equal(result.status, 0); + expect(result.status).toBe(0); // Banner and bye statement should be present const output = `${result.stdout}${result.stderr}`; - assert.match(output, /NemoClaw/); - assert.match(output, /Claws retracted/); + expect(output).toMatch(/NemoClaw/); + expect(output).toMatch(/Claws retracted/); } finally { fs.rmSync(tmp, { recursive: true, force: true }); } @@ -69,8 +67,8 @@ describe("uninstall helpers", () => { }, ); - assert.equal(result.status, 0); - assert.equal(result.stdout.trim(), "openshell-cluster-nemoclaw"); + expect(result.status).toBe(0); + expect(result.stdout.trim()).toBe("openshell-cluster-nemoclaw"); }); it("removes the user-local nemoclaw shim", () => { @@ -89,7 +87,7 @@ describe("uninstall helpers", () => { }, ); - assert.equal(result.status, 0); - assert.equal(fs.existsSync(shimPath), false); + expect(result.status).toBe(0); + expect(fs.existsSync(shimPath)).toBe(false); }); }); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..75d34bdd --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["test/**/*.test.js"], + globals: true, + }, +});