Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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: 2 additions & 0 deletions scripts/checks/vitest-project-overlap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ const INSTALLER_INTEGRATION_TESTS = new Set([
"test/install-preflight-docker-bootstrap.test.ts",
"test/install-preflight.test.ts",
"test/install-station-dgx-os.test.ts",
"test/install-station-docker-repository.test.ts",
"test/install-station-host-preparation.test.ts",
"test/install-station-package-transaction.test.ts",
]);

function normalizeRepoPath(file: string): string {
Expand Down
20 changes: 19 additions & 1 deletion scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3499,7 +3499,25 @@ run_station_host_preparation() {
if [ "${FORCE_STATION_INSTALL:-}" = "1" ]; then
helper_args+=(--force-station-install)
fi
bash "$helper" "${helper_args[@]}"
bash "$helper" "${helper_args[@]}" 2>&1 | filter_station_host_preparation_output
}

filter_station_host_preparation_output() {
local line detail
while IFS= read -r line; do
case "$line" in
*" version="*" log="*)
info "DGX Station host preparation log: ${line##* log=}"
;;
*" WARNING: "*)
detail="${line#* WARNING: }"
warn "$detail"
;;
*" ERROR: "*)
printf '%s\n' "$line" >&2
;;
esac
done
}

ensure_station_express_host() {
Expand Down
418 changes: 371 additions & 47 deletions scripts/prepare-dgx-station-host.sh

Large diffs are not rendered by default.

57 changes: 57 additions & 0 deletions test/install-express-prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ import { describe, expect, it } from "vitest";
import { INSTALLER_PAYLOAD, TEST_SYSTEM_PATH } from "./helpers/installer-sourced-env";

describe("installer express install prompt (sourced)", () => {
function runInstallerSourced(body: string) {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-express-sourced-"));
const result = spawnSync(
"bash",
["--noprofile", "--norc", "-c", `source "$INSTALLER_UNDER_TEST" >/dev/null\n${body}`],
{
cwd: path.resolve(import.meta.dirname, ".."),
encoding: "utf-8",
env: {
HOME: home,
PATH: TEST_SYSTEM_PATH,
INSTALLER_UNDER_TEST: INSTALLER_PAYLOAD,
},
},
);
return { result, output: `${result.stdout}${result.stderr}` };
}

function runExpressPromptWithTty(
answer: string,
stdinMode: "pipe" | "tty",
Expand Down Expand Up @@ -361,6 +379,45 @@ detect_express_platform
expect(output).toMatch(/STATION_EXPRESS=1/);
});

it("keeps Station preparation details in the log while showing warnings and errors", () => {
const { result, output } = runInstallerSourced(`
printf '%s\n' \
'[station-prepare] 2026-07-17T07:59:07Z version=2026-07-17.4 mode=--apply log=/tmp/station-prepare.log' \
'[station-prepare] 2026-07-17T07:59:07Z platform=Dell Pro Max with Station GB300 profile=generic-ubuntu' \
'[station-prepare] 2026-07-17T07:59:08Z WARNING: condition-qualified generic-image failed unit: cloud-init.service' \
'NVIDIA-SMI 610.43.02' \
'[station-prepare] 2026-07-17T07:59:20Z ERROR: example failure' \
| filter_station_host_preparation_output
`);

expect(result.status, output).toBe(0);
expect(output).toContain("DGX Station host preparation log: /tmp/station-prepare.log");
expect(output).toContain("condition-qualified generic-image failed unit: cloud-init.service");
expect(output).toContain("ERROR: example failure");
expect(output).not.toMatch(/platform=Dell Pro Max|NVIDIA-SMI/);
});

it("preserves the Station helper exit status while filtering installer output", () => {
const { result, output } = runInstallerSourced(`
bash() {
printf '%s\n' \
'[station-prepare] 2026-07-17T07:59:07Z version=2026-07-17.4 mode=--apply log=/tmp/station-prepare.log' \
'[station-prepare] 2026-07-17T07:59:08Z runtime_setup=complete'
return 10
}
if run_station_host_preparation; then
printf 'STATUS=0\n'
else
printf 'STATUS=%s\n' "$?"
fi
`);

expect(result.status, output).toBe(0);
expect(output).toContain("STATUS=10");
expect(output).toContain("DGX Station host preparation log: /tmp/station-prepare.log");
expect(output).not.toContain("runtime_setup=complete");
});

it.each([
[
"supported-colossus-baseos",
Expand Down
59 changes: 39 additions & 20 deletions test/install-station-dgx-os.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,16 +522,17 @@ describe("DGX Station forced metadata installer handoff", () => {
INSTALLER_PAYLOAD,
`
SCRIPT_DIR="$HOME"
touch "$SCRIPT_DIR/prepare-dgx-station-host.sh"
bash() { printf 'HELPER_ARGS=%s\n' "$*"; }
cat >"$SCRIPT_DIR/prepare-dgx-station-host.sh" <<'HELPER'
printf '%s\n' "$*" >"$HOME/helper-args"
HELPER
FORCE_STATION_INSTALL=1
run_station_host_preparation
`,
);

expect(result.status, output).toBe(0);
expect(output).toContain(
`HELPER_ARGS=${path.join(home, "prepare-dgx-station-host.sh")} --apply --force-station-install`,
expect(fs.readFileSync(path.join(home, "helper-args"), "utf8")).toBe(
"--apply --force-station-install\n",
);
});

Expand Down Expand Up @@ -813,7 +814,8 @@ run_apply
STATION_PREPARE,
`
STATION_HOST_PROFILE=stock-dgx-os
nvidia-smi() { printf 'NVIDIA GB300, 595.71.05, 0, 0\n'; }
station_pci_device_is_gb300() { return 0; }
nvidia-smi() { printf '00000000:01:00.0, NVIDIA GB300, 595.71.05, 0, 0\n'; }
verify_gpu
`,
);
Expand All @@ -824,7 +826,8 @@ verify_gpu
STATION_PREPARE,
`
STATION_HOST_PROFILE=generic-ubuntu
nvidia-smi() { printf 'NVIDIA GB300, 595.71.05, 0, 0\n'; }
station_pci_device_is_gb300() { return 0; }
nvidia-smi() { printf '00000000:01:00.0, NVIDIA GB300, 595.71.05, 0, 0\n'; }
verify_gpu
`,
);
Expand All @@ -837,56 +840,60 @@ verify_gpu
STATION_PREPARE,
`
STATION_HOST_PROFILE=ai-developer-tools
station_pci_device_is_gb300() { [[ "$1" == "0000:01:00.0" ]]; }
nvidia-smi() {
printf 'NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition, 610.43.03, [N/A], [N/A]\n'
printf 'NVIDIA GB300, 610.43.03, 0, 0\n'
printf '00000000:02:00.0, NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition, 610.43.03, [N/A], [N/A]\n'
printf '00000000:01:00.0, NVIDIA GB300, 610.43.03, 0, 0\n'
}
verify_gpu
`,
);

expect(result.status, output).toBe(0);
expect(output).toContain("gpu_index=0 gpu=NVIDIA RTX PRO 6000");
expect(output).toContain("gpu_bdf=0000:02:00.0 gpu=NVIDIA RTX PRO 6000");
expect(output).toContain("role=auxiliary validation=skipped");
expect(output).toContain("gpu_index=1 gpu=NVIDIA GB300 role=inference");
expect(output).toContain("gpu_bdf=0000:01:00.0 gpu=NVIDIA GB300 role=inference");
});

it("requires both factory container probes to expose the GB300 on a mixed-GPU host", () => {
const mixed = runSourced(
STATION_PREPARE,
`
STATION_HOST_PROFILE=ai-developer-tools
station_pci_device_is_gb300() { [[ "$1" == "0000:01:00.0" ]]; }
station_sudo_local_default_docker() {
printf 'NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition, 610.43.03, [N/A], [N/A]\n'
printf 'NVIDIA GB300, 610.43.03, 0, 0\n'
printf '00000000:02:00.0, NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition, 610.43.03, [N/A], [N/A]\n'
printf '00000000:01:00.0, NVIDIA GB300, 610.43.03, 0, 0\n'
}
run_dgx_os_cdi_test_sudo
run_dgx_os_gpus_test_sudo
`,
);
expect(mixed.result.status, mixed.output).toBe(0);
expect(mixed.output).toContain("gpu_index=1 gpu=NVIDIA GB300 role=inference");
expect(mixed.output).toContain("gpu_bdf=0000:01:00.0 gpu=NVIDIA GB300 role=inference");

const rtxOnly = runSourced(
STATION_PREPARE,
`
STATION_HOST_PROFILE=ai-developer-tools
station_pci_device_is_gb300() { [[ "$1" == "0000:01:00.0" ]]; }
station_sudo_local_default_docker() {
printf 'NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition, 610.43.03, [N/A], [N/A]\n'
printf '00000000:02:00.0, NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition, 610.43.03, [N/A], [N/A]\n'
}
run_dgx_os_cdi_test_sudo
`,
);
expect(rtxOnly.result.status, rtxOnly.output).not.toBe(0);
expect(rtxOnly.output).toContain("Expected exactly one NVIDIA GB300, found 0");
expect(rtxOnly.output).toContain("Expected exactly one NVIDIA GB300 PCI device, found 0");
});

it("requires the qualified BaseOS driver to be loaded", () => {
const { result, output } = runSourced(
STATION_PREPARE,
`
STATION_HOST_PROFILE=colossus-baseos
nvidia-smi() { printf 'NVIDIA GB300, 595.71.05, 0, 0\n'; }
station_pci_device_is_gb300() { return 0; }
nvidia-smi() { printf '00000000:01:00.0, NVIDIA GB300, 595.71.05, 0, 0\n'; }
verify_gpu
`,
);
Expand Down Expand Up @@ -986,18 +993,30 @@ verify_dgx_os_runtime_sudo
});

it.each([
["wrong GPU", "NVIDIA GB200, 595.71.05, 0, 0", /Expected exactly one NVIDIA GB300, found 0/],
["non-zero volatile ECC", "NVIDIA GB300, 595.71.05, 1, 0", /ECC must be 0\/0/],
[
"a missing GB300 PCI identity",
"00000000:01:00.0, NVIDIA GB300, 595.71.05, 0, 0",
"return 1",
/Expected exactly one NVIDIA GB300 PCI device, found 0/,
],
[
"non-zero volatile ECC",
"00000000:01:00.0, NVIDIA GB300, 595.71.05, 1, 0",
"return 0",
/ECC must be 0\/0/,
],
[
"a failing second GPU row",
"NVIDIA GB300, 595.71.05, 0, 0\nNVIDIA GB300, 595.71.05, 0, 1",
"00000000:01:00.0, NVIDIA GB300, 595.71.05, 0, 0\n00000000:02:00.0, NVIDIA GB300, 595.71.05, 0, 1",
"return 0",
/ECC must be 0\/0/,
],
])("fails stock validation for %s", (_scenario, row, message) => {
])("fails stock validation for %s", (_scenario, row, pciResult, message) => {
const { result, output } = runSourced(
STATION_PREPARE,
`
STATION_HOST_PROFILE=stock-dgx-os
station_pci_device_is_gb300() { ${pciResult}; }
nvidia-smi() { printf '%s\n' "$GPU_ROW"; }
verify_gpu
`,
Expand Down
Loading
Loading