Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/commit-lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down Expand Up @@ -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"
}
}
56 changes: 27 additions & 29 deletions test/cli.test.js
Original file line number Diff line number Diff line change
@@ -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");

Expand All @@ -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();
});
});
4 changes: 1 addition & 3 deletions test/credentials.test.js
Original file line number Diff line number Diff line change
@@ -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");

Expand All @@ -27,6 +25,6 @@ describe("credential prompts", () => {
timeout: 5000,
});

assert.equal(result.status, 0);
expect(result.status).toBe(0);
});
});
33 changes: 13 additions & 20 deletions test/inference-config.test.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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`);
});
});
76 changes: 37 additions & 39 deletions test/install-preflight.test.js
Original file line number Diff line number Diff line change
@@ -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");
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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", () => {
Expand All @@ -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", () => {
Expand All @@ -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", () => {
Expand All @@ -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)", () => {
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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/);
});
});
Loading
Loading