Skip to content
Closed
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
7 changes: 5 additions & 2 deletions bin/lib/nim.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ function waitForNimHealth(port = 8000, timeout = 300) {

while ((Date.now() - start) / 1000 < timeout) {
try {
const result = runCapture(`curl -sf http://localhost:${safePort}/v1/models`, {
const result = runCapture(`curl -sf --connect-timeout 5 --max-time 5 http://localhost:${safePort}/v1/models`, {
ignoreError: true,
});
if (result) {
Expand Down Expand Up @@ -190,6 +190,9 @@ function nimStatus(sandboxName) {

function nimStatusByName(name) {
try {
// Guard against docker not being installed
runCapture("command -v docker", { ignoreError: false });

const state = runCapture(
`docker inspect --format '{{.State.Status}}' ${shellQuote(name)} 2>/dev/null`,
{ ignoreError: true }
Expand All @@ -198,7 +201,7 @@ function nimStatusByName(name) {

let healthy = false;
if (state === "running") {
const health = runCapture(`curl -sf http://localhost:8000/v1/models 2>/dev/null`, {
const health = runCapture(`curl -sf --connect-timeout 5 --max-time 5 http://localhost:8000/v1/models 2>/dev/null`, {
ignoreError: true,
});
healthy = !!health;
Expand Down
142 changes: 142 additions & 0 deletions test/nim.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,57 @@
import { describe, it, expect } from "vitest";
import nim from "../bin/lib/nim";

function withMockedRunner({ runResult, runCaptureResults = [], runCaptureImpl }, callback) {
const nimPath = require.resolve("../bin/lib/nim");
const runnerPath = require.resolve("../bin/lib/runner");
const savedNim = require.cache[nimPath];
const savedRunner = require.cache[runnerPath];
const childProcess = require("node:child_process");
const savedSpawnSync = childProcess.spawnSync;
const calls = { run: [], runCapture: [], spawnSync: [] };

const realRunner = require("../bin/lib/runner");
require.cache[runnerPath] = {
id: runnerPath,
filename: runnerPath,
loaded: true,
exports: {
...realRunner,
run(command, options) {
calls.run.push({ command, options });
return runResult;
},
runCapture(command, options) {
calls.runCapture.push({ command, options });
if (runCaptureImpl) {
return runCaptureImpl(command, options, calls);
}
return runCaptureResults.shift() ?? "";
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
};
delete require.cache[nimPath];
childProcess.spawnSync = (...args) => {
calls.spawnSync.push(args);
return { status: 0 };
};

try {
callback(require("../bin/lib/nim"), calls);
} finally {
delete require.cache[nimPath];
if (savedNim) {
require.cache[nimPath] = savedNim;
}
if (savedRunner) {
require.cache[runnerPath] = savedRunner;
} else {
delete require.cache[runnerPath];
}
childProcess.spawnSync = savedSpawnSync;
}
}

describe("nim", () => {
describe("listModels", () => {
it("returns 5 models", () => {
Expand Down Expand Up @@ -68,5 +119,96 @@ describe("nim", () => {
const st = nim.nimStatus("nonexistent-test-xyz");
expect(st.running).toBe(false);
});

it("probes health with a connect timeout and quoted container name", () => {
withMockedRunner(
{
runCaptureResults: ["/usr/bin/docker", "running", '{"data":[]}'],
},
(mockedNim, calls) => {
const st = mockedNim.nimStatus("my-sandbox");
assert.equal(st.running, true);
assert.equal(st.healthy, true);
assert.equal(
calls.runCapture[1].command,
`docker inspect --format '{{.State.Status}}' 'nemoclaw-nim-my-sandbox' 2>/dev/null`,
);
assert.equal(
calls.runCapture[2].command,
"curl -sf --connect-timeout 5 --max-time 5 http://localhost:8000/v1/models 2>/dev/null",
);
},
);
});

it("returns not running when docker is unavailable", () => {
withMockedRunner(
{
runCaptureImpl(command) {
if (command === "command -v docker") {
throw new Error("docker missing");
}
return "";
},
},
(mockedNim, calls) => {
const st = mockedNim.nimStatus("my-sandbox");
expect(st).toEqual({
running: false,
container: "nemoclaw-nim-my-sandbox",
});
expect(calls.runCapture).toHaveLength(1);
},
);
});
});

describe("shell command construction", () => {
it("quotes docker image pulls", () => {
withMockedRunner({}, (mockedNim, calls) => {
mockedNim.pullNimImage("nvidia/nemotron-3-nano-30b-a3b");
assert.equal(
calls.run[0].command,
"docker pull 'nvcr.io/nim/nvidia/nemotron-3-nano-30b-a3b:latest'",
);
});
});

it("quotes docker run and cleanup commands", () => {
withMockedRunner({}, (mockedNim, calls) => {
mockedNim.startNimContainer("my-sandbox", "nvidia/nemotron-3-nano-30b-a3b", 9000);
mockedNim.stopNimContainer("my-sandbox");

assert.equal(
calls.run[0].command,
"docker rm -f 'nemoclaw-nim-my-sandbox' 2>/dev/null || true",
);
assert.equal(
calls.run[1].command,
"docker run -d --gpus all -p 9000:8000 --name 'nemoclaw-nim-my-sandbox' --shm-size 16g 'nvcr.io/nim/nvidia/nemotron-3-nano-30b-a3b:latest'",
);
assert.equal(
calls.run[2].command,
"docker stop 'nemoclaw-nim-my-sandbox' 2>/dev/null || true",
);
assert.equal(
calls.run[3].command,
"docker rm 'nemoclaw-nim-my-sandbox' 2>/dev/null || true",
);
});
});
});

describe("waitForNimHealth", () => {
it("uses curl connect timeout for readiness probes", () => {
withMockedRunner({ runCaptureResults: ['{"data":[]}'] }, (mockedNim, calls) => {
assert.equal(mockedNim.waitForNimHealth(9000, 1), true);
assert.equal(
calls.runCapture[0].command,
"curl -sf --connect-timeout 5 --max-time 5 http://localhost:9000/v1/models",
);
assert.equal(calls.spawnSync.length, 0);
});
});
});
});