From 5cc8c846c8c5fd6f65eaea292ef0015718541a46 Mon Sep 17 00:00:00 2001 From: sevenc Date: Fri, 27 Mar 2026 12:30:20 +0800 Subject: [PATCH 1/2] fix(nim): use published Docker port in status checks --- bin/lib/nim.js | 35 +++++++++------ test/nim.test.js | 113 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 134 insertions(+), 14 deletions(-) diff --git a/bin/lib/nim.js b/bin/lib/nim.js index f291a0967..b735dff96 100644 --- a/bin/lib/nim.js +++ b/bin/lib/nim.js @@ -150,13 +150,13 @@ function startNimContainerByName(name, model, port = 8000) { function waitForNimHealth(port = 8000, timeout = 300) { const start = Date.now(); - const _interval = 5000; - const safePort = Number(port); - console.log(` Waiting for NIM health on port ${safePort} (timeout: ${timeout}s)...`); + const intervalSec = 5; + const hostPort = Number(port); + console.log(` Waiting for NIM health on port ${hostPort} (timeout: ${timeout}s)...`); while ((Date.now() - start) / 1000 < timeout) { try { - const result = runCapture(`curl -sf http://localhost:${safePort}/v1/models`, { + const result = runCapture(`curl -sf http://localhost:${hostPort}/v1/models`, { ignoreError: true, }); if (result) { @@ -164,8 +164,7 @@ function waitForNimHealth(port = 8000, timeout = 300) { return true; } } catch { /* ignored */ } - // Synchronous sleep via spawnSync - require("child_process").spawnSync("sleep", ["5"]); + require("child_process").spawnSync("sleep", [String(intervalSec)]); } console.error(` NIM did not become healthy within ${timeout}s.`); return false; @@ -183,24 +182,34 @@ function stopNimContainerByName(name) { run(`docker rm ${qn} 2>/dev/null || true`, { ignoreError: true }); } -function nimStatus(sandboxName) { +function nimStatus(sandboxName, port) { const name = containerName(sandboxName); - return nimStatusByName(name); + return nimStatusByName(name, port); } -function nimStatusByName(name) { +function nimStatusByName(name, port) { try { + const qn = shellQuote(name); const state = runCapture( - `docker inspect --format '{{.State.Status}}' ${shellQuote(name)} 2>/dev/null`, + `docker inspect --format '{{.State.Status}}' ${qn} 2>/dev/null`, { ignoreError: true } ); if (!state) return { running: false, container: name }; let healthy = false; if (state === "running") { - const health = runCapture(`curl -sf http://localhost:8000/v1/models 2>/dev/null`, { - ignoreError: true, - }); + let resolvedHostPort = port != null ? Number(port) : 0; + if (!resolvedHostPort) { + const mapping = runCapture(`docker port ${qn} 8000 2>/dev/null`, { + ignoreError: true, + }); + const m = mapping && mapping.match(/:(\d+)\s*$/); + resolvedHostPort = m ? Number(m[1]) : 8000; + } + const health = runCapture( + `curl -sf http://localhost:${resolvedHostPort}/v1/models 2>/dev/null`, + { ignoreError: true } + ); healthy = !!health; } return { running: state === "running", healthy, container: name, state }; diff --git a/test/nim.test.js b/test/nim.test.js index cd4cf6cd4..89885919a 100644 --- a/test/nim.test.js +++ b/test/nim.test.js @@ -1,9 +1,34 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { describe, it, expect } from "vitest"; +import { createRequire } from "module"; +import { describe, it, expect, vi } from "vitest"; import nim from "../bin/lib/nim"; +const require = createRequire(import.meta.url); +const NIM_PATH = require.resolve("../bin/lib/nim"); +const RUNNER_PATH = require.resolve("../bin/lib/runner"); + +function loadNimWithMockedRunner(runCapture) { + const runner = require(RUNNER_PATH); + const originalRun = runner.run; + const originalRunCapture = runner.runCapture; + + delete require.cache[NIM_PATH]; + runner.run = vi.fn(); + runner.runCapture = runCapture; + const nimModule = require(NIM_PATH); + + return { + nimModule, + restore() { + delete require.cache[NIM_PATH]; + runner.run = originalRun; + runner.runCapture = originalRunCapture; + }, + }; +} + describe("nim", () => { describe("listModels", () => { it("returns 5 models", () => { @@ -69,4 +94,90 @@ describe("nim", () => { expect(st.running).toBe(false); }); }); + + describe("nimStatusByName", () => { + it("uses provided port directly", () => { + const runCapture = vi.fn((cmd) => { + if (cmd.includes("docker inspect")) return "running"; + if (cmd.includes("http://localhost:9000/v1/models")) return '{"data":[]}'; + return ""; + }); + const { nimModule, restore } = loadNimWithMockedRunner(runCapture); + + try { + const st = nimModule.nimStatusByName("foo", 9000); + const commands = runCapture.mock.calls.map(([cmd]) => cmd); + + expect(st).toMatchObject({ running: true, healthy: true, container: "foo", state: "running" }); + expect(commands.some((cmd) => cmd.includes("docker port"))).toBe(false); + expect(commands.some((cmd) => cmd.includes("http://localhost:9000/v1/models"))).toBe(true); + } finally { + restore(); + } + }); + + it("uses published docker port when no port is provided", () => { + for (const mapping of ["0.0.0.0:9000", ":::9000"]) { + const runCapture = vi.fn((cmd) => { + if (cmd.includes("docker inspect")) return "running"; + if (cmd.includes("docker port")) return mapping; + if (cmd.includes("http://localhost:9000/v1/models")) return '{"data":[]}'; + return ""; + }); + const { nimModule, restore } = loadNimWithMockedRunner(runCapture); + + try { + const st = nimModule.nimStatusByName("foo"); + const commands = runCapture.mock.calls.map(([cmd]) => cmd); + + expect(st).toMatchObject({ running: true, healthy: true, container: "foo", state: "running" }); + expect(commands.some((cmd) => cmd.includes("docker port"))).toBe(true); + expect(commands.some((cmd) => cmd.includes("http://localhost:9000/v1/models"))).toBe(true); + } finally { + restore(); + } + } + }); + + it("falls back to 8000 when docker port lookup fails", () => { + const runCapture = vi.fn((cmd) => { + if (cmd.includes("docker inspect")) return "running"; + if (cmd.includes("docker port")) return ""; + if (cmd.includes("http://localhost:8000/v1/models")) return '{"data":[]}'; + return ""; + }); + const { nimModule, restore } = loadNimWithMockedRunner(runCapture); + + try { + const st = nimModule.nimStatusByName("foo"); + const commands = runCapture.mock.calls.map(([cmd]) => cmd); + + expect(st).toMatchObject({ running: true, healthy: true, container: "foo", state: "running" }); + expect(commands.some((cmd) => cmd.includes("docker port"))).toBe(true); + expect(commands.some((cmd) => cmd.includes("http://localhost:8000/v1/models"))).toBe(true); + } finally { + restore(); + } + }); + + it("does not run health check when container is not running", () => { + const runCapture = vi.fn((cmd) => { + if (cmd.includes("docker inspect")) return "exited"; + return ""; + }); + const { nimModule, restore } = loadNimWithMockedRunner(runCapture); + + try { + const st = nimModule.nimStatusByName("foo"); + const commands = runCapture.mock.calls.map(([cmd]) => cmd); + + expect(st).toMatchObject({ running: false, healthy: false, container: "foo", state: "exited" }); + expect(commands).toHaveLength(1); + expect(commands.some((cmd) => cmd.includes("docker port"))).toBe(false); + expect(commands.some((cmd) => cmd.includes("http://localhost:"))).toBe(false); + } finally { + restore(); + } + }); + }); }); From b26692452d4979c3d4f840ad754f8f2470fc15a6 Mon Sep 17 00:00:00 2001 From: sevenc Date: Fri, 27 Mar 2026 12:40:15 +0800 Subject: [PATCH 2/2] test: update test case --- test/nim.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/nim.test.js b/test/nim.test.js index 89885919a..468a55c94 100644 --- a/test/nim.test.js +++ b/test/nim.test.js @@ -117,7 +117,7 @@ describe("nim", () => { }); it("uses published docker port when no port is provided", () => { - for (const mapping of ["0.0.0.0:9000", ":::9000"]) { + for (const mapping of ["0.0.0.0:9000", "127.0.0.1:9000", "[::]:9000", ":::9000"]) { const runCapture = vi.fn((cmd) => { if (cmd.includes("docker inspect")) return "running"; if (cmd.includes("docker port")) return mapping;