Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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 @@ -3384,7 +3384,25 @@ run_station_host_preparation() {
# fail-closed check so Station preparation cannot drift from that ref.
local helper="${SCRIPT_DIR}/prepare-dgx-station-host.sh"
[[ -f "$helper" ]] || error "DGX Station host preparation helper is missing: ${helper}"
bash "$helper" --apply
bash "$helper" --apply 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
402 changes: 364 additions & 38 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 @@ -317,6 +335,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("normalizes the canonical Ultra served alias to the registered model slug", () => {
const result = runExpressPromptWithTty("\n", "pipe", "DGX Station", {
NEMOCLAW_VLLM_MODEL: "nvidia/nemotron-3-ultra-550b-a55b",
Expand Down
26 changes: 20 additions & 6 deletions test/install-station-dgx-os.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,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 @@ -334,7 +335,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 Down Expand Up @@ -430,18 +432,30 @@ verify_dgx_os_runtime_sudo
});

it.each([
["wrong GPU", "NVIDIA GB200, 595.71.05, 0, 0", /Expected NVIDIA GB300/],
["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",
/no GB300 GPU matching the PCI identity/,
],
[
"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
182 changes: 182 additions & 0 deletions test/install-station-docker-repository.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { TEST_SYSTEM_PATH } from "./helpers/installer-sourced-env";

const REPO_ROOT = path.resolve(import.meta.dirname, "..");
const STATION_PREPARE = path.join(REPO_ROOT, "scripts", "prepare-dgx-station-host.sh");

function runSourced(body: string, extraEnv: Record<string, string> = {}) {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-station-docker-repository-"));
const result = spawnSync(
"bash",
["--noprofile", "--norc", "-c", `source "$SCRIPT_UNDER_TEST" >/dev/null\n${body}`],
{
cwd: REPO_ROOT,
encoding: "utf-8",
env: {
HOME: home,
PATH: TEST_SYSTEM_PATH,
SCRIPT_UNDER_TEST: STATION_PREPARE,
...extraEnv,
},
timeout: 15_000,
killSignal: "SIGKILL",
},
);
return { result, output: `${result.stdout}${result.stderr}` };
}

const DOCKER_REPOSITORY_FIXTURE = `
prepare_docker_repository_fixture() {
mkdir -p "$HOME/root/etc/apt/keyrings" "$HOME/root/etc/apt/sources.list.d"
printf 'verified ascii key\n' >"$HOME/docker.asc"
printf 'verified dearmored key\n' >"$HOME/docker.gpg"
printf '%s\n' \\
'deb [arch=arm64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu noble stable' \\
>"$HOME/docker-gpg.list"
printf '%s\n' \\
'deb [arch=arm64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable' \\
>"$HOME/docker-asc.list"
}
assert_root_regular_file_safe() { printf 'ASSERT_SAFE %s\n' "$1"; }
sudo() {
if [[ "$*" == 'test ! -L /etc/apt/sources.list.d/docker.list' ]]; then
test ! -L "$HOME/root/etc/apt/sources.list.d/docker.list"
return
fi
if [[ "$*" == 'test -e /etc/apt/sources.list.d/docker.list' ]]; then
test -e "$HOME/root/etc/apt/sources.list.d/docker.list"
return
fi
if [[ "$1" == 'cmp' && "$2" == '-s' ]]; then
case "$4" in
/etc/apt/sources.list.d/docker.list)
cmp -s "$3" "$HOME/root/etc/apt/sources.list.d/docker.list"
;;
/etc/apt/keyrings/docker.gpg)
cmp -s "$3" "$HOME/root/etc/apt/keyrings/docker.gpg"
;;
/etc/apt/keyrings/docker.asc)
cmp -s "$3" "$HOME/root/etc/apt/keyrings/docker.asc"
;;
*) return 1 ;;
esac
return
fi
return 1
}
`;

const VERIFY_REPOSITORY = `
ensure_docker_repository_source \
"$HOME/docker.asc" \
"$HOME/docker.gpg" \
"$HOME/docker-gpg.list" \
"$HOME/docker-asc.list"
`;

describe("DGX Station Docker repository compatibility", () => {
it("reuses the exact .gpg source with its verified key", () => {
const { result, output } = runSourced(`
${DOCKER_REPOSITORY_FIXTURE}
prepare_docker_repository_fixture
cp "$HOME/docker.gpg" "$HOME/root/etc/apt/keyrings/docker.gpg"
cp "$HOME/docker-gpg.list" "$HOME/root/etc/apt/sources.list.d/docker.list"
${VERIFY_REPOSITORY}
`);

expect(result.status, output).toBe(0);
expect(output).toContain("ASSERT_SAFE /etc/apt/sources.list.d/docker.list");
expect(output).toContain("ASSERT_SAFE /etc/apt/keyrings/docker.gpg");
expect(output).toContain("docker_repository_source=exact");
});

it("reuses the equivalent .asc source with its verified key", () => {
const { result, output } = runSourced(`
${DOCKER_REPOSITORY_FIXTURE}
prepare_docker_repository_fixture
cp "$HOME/docker.asc" "$HOME/root/etc/apt/keyrings/docker.asc"
cp "$HOME/docker-asc.list" "$HOME/root/etc/apt/sources.list.d/docker.list"
${VERIFY_REPOSITORY}
`);

expect(result.status, output).toBe(0);
expect(output).toContain("ASSERT_SAFE /etc/apt/sources.list.d/docker.list");
expect(output).toContain("ASSERT_SAFE /etc/apt/keyrings/docker.asc");
expect(output).toContain("docker_repository_source=verified_compatible");
});

it("rejects an .asc source when its installed key differs", () => {
const { result, output } = runSourced(`
${DOCKER_REPOSITORY_FIXTURE}
prepare_docker_repository_fixture
printf 'different ascii key\n' >"$HOME/root/etc/apt/keyrings/docker.asc"
cp "$HOME/docker-asc.list" "$HOME/root/etc/apt/sources.list.d/docker.list"
${VERIFY_REPOSITORY}
`);

expect(result.status, output).not.toBe(0);
expect(output).toMatch(/ASCII key differs from the verified key/);
expect(output).not.toContain("docker_repository_source=verified_compatible");
});

it.each([
[
"a changed URL",
"deb [arch=arm64 signed-by=/etc/apt/keyrings/docker.asc] https://mirror.invalid/linux/ubuntu noble stable\n",
],
[
"an extra source line",
"deb [arch=arm64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable\ndeb https://mirror.invalid/linux/ubuntu noble stable\n",
],
])("rejects a source with %s", (_case, sourceContent) => {
const { result, output } = runSourced(
`
${DOCKER_REPOSITORY_FIXTURE}
prepare_docker_repository_fixture
cp "$HOME/docker.asc" "$HOME/root/etc/apt/keyrings/docker.asc"
printf '%s' "$SOURCE_CONTENT" >"$HOME/root/etc/apt/sources.list.d/docker.list"
${VERIFY_REPOSITORY}
`,
{ SOURCE_CONTENT: sourceContent },
);

expect(result.status, output).not.toBe(0);
expect(output).toMatch(/differs from the validated \.gpg and \.asc forms/);
});

it("rejects a symlinked source", () => {
const { result, output } = runSourced(`
${DOCKER_REPOSITORY_FIXTURE}
prepare_docker_repository_fixture
cp "$HOME/docker.asc" "$HOME/root/etc/apt/keyrings/docker.asc"
ln -s "$HOME/docker-asc.list" "$HOME/root/etc/apt/sources.list.d/docker.list"
${VERIFY_REPOSITORY}
`);

expect(result.status, output).not.toBe(0);
expect(output).toMatch(/Docker repository source must not be a symbolic link/);
});

it("uses the dearmored key for a new source", () => {
const { result, output } = runSourced(`
${DOCKER_REPOSITORY_FIXTURE}
prepare_docker_repository_fixture
install_exact_file_or_reuse() { printf 'INSTALL %s -> %s\n' "$1" "$2"; }
${VERIFY_REPOSITORY}
`);

expect(result.status, output).toBe(0);
expect(output).toMatch(/INSTALL .+\/docker\.gpg -> \/etc\/apt\/keyrings\/docker\.gpg/);
expect(output).toMatch(
/INSTALL .+\/docker-gpg\.list -> \/etc\/apt\/sources\.list\.d\/docker\.list/,
);
expect(output).not.toContain("docker.asc -> /etc/apt/keyrings");
});
});
Loading
Loading