Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
bb53acb
fix(onboard): color preflight WARN/ERROR check lines (#6004)
jason-ma-nv Jun 30, 2026
b1096dd
fix(onboard): apply warnLine to bridge container start probe inconclu…
prekshivyas Jul 1, 2026
6f58de2
fix(onboard): route image-pull DNS warning through warnLine (#6004)
jason-ma-nv Jul 2, 2026
a7fbec4
Merge remote-tracking branch 'origin/main' into fix/6004-preflight-wa…
jason-ma-nv Jul 3, 2026
3c9cb0c
fix(onboard): color preflight WARN/ERROR by target stream via styleTe…
jason-ma-nv Jul 3, 2026
61a3ecc
fix(onboard): keep onboard.ts net-neutral under growth guardrail (#6004)
jason-ma-nv Jul 3, 2026
d004a26
fix(onboard): extract preflight severity messages so onboard.ts lines…
jason-ma-nv Jul 3, 2026
a3bbfe4
test(onboard): drop unused beforeEach import in terminal-style test (…
jason-ma-nv Jul 3, 2026
1bd7555
fix(onboard): color Docker-unreachable preflight
cv Jul 3, 2026
210c6a0
fix(onboard): tighten preflight severity helpers
cv Jul 3, 2026
7ee50d1
test(onboard): isolate severity regressions
cv Jul 3, 2026
bed601a
merge: refresh preflight severity fixes with main
cv Jul 3, 2026
e54e9c4
fix(onboard): complete stderr severity coverage
cv Jul 3, 2026
dc83915
merge: refresh preflight severity fixes with main
cv Jul 3, 2026
e074fd7
test(onboard): linearize stream restoration
cv Jul 3, 2026
5054368
fix(onboard): harden preflight severity output
apurvvkumaria Jul 4, 2026
ce84032
fix(onboard): color fatal CDI preflight
apurvvkumaria Jul 4, 2026
9e06a53
fix(onboard): color deferred gateway warning
apurvvkumaria Jul 4, 2026
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
43 changes: 42 additions & 1 deletion src/lib/cli/terminal-style.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";

import { B, D, G, R, RD, YW } from "./terminal-style";

Expand All @@ -12,3 +12,44 @@ describe("terminal-style", () => {
}
});
});

const ORIGINAL_TTY = process.stdout.isTTY;

function setTTY(value: boolean): void {
Object.defineProperty(process.stdout, "isTTY", { value, configurable: true });
}

// #6004: warnLine/failLine read NO_COLOR + stdout.isTTY at module load, so each
// case reloads the module under a stubbed environment.
async function loadStyle(opts: { tty: boolean; noColor?: string }) {
vi.resetModules();
setTTY(opts.tty);
vi.stubEnv("NO_COLOR", opts.noColor ?? "");
return import("./terminal-style");
}

describe("preflight line helpers (#6004)", () => {
afterEach(() => {
vi.unstubAllEnvs();
vi.resetModules();
setTTY(ORIGINAL_TTY);
});

it("renders warn ⚠ in yellow and fail ✗ in red on a color-capable TTY", async () => {
const { warnLine, failLine } = await loadStyle({ tty: true, noColor: "" });
expect(warnLine("disk low")).toBe(" \x1b[1;33m⚠ disk low\x1b[0m");
expect(failLine("docker down")).toBe(" \x1b[1;31m✗ docker down\x1b[0m");
});

it("emits plain text (no ANSI) under NO_COLOR=1", async () => {
const { warnLine, failLine } = await loadStyle({ tty: true, noColor: "1" });
expect(warnLine("disk low")).toBe(" ⚠ disk low");
expect(failLine("docker down")).toBe(" ✗ docker down");
});

it("emits plain text (no ANSI) when stdout is not a TTY", async () => {
const { warnLine, failLine } = await loadStyle({ tty: false });
expect(warnLine("x")).toBe(" ⚠ x");
expect(failLine("y")).toBe(" ✗ y");
});
});
15 changes: 15 additions & 0 deletions src/lib/cli/terminal-style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,18 @@ export const D = useColor ? "\x1b[2m" : "";
export const R = useColor ? "\x1b[0m" : "";
export const RD = useColor ? "\x1b[1;31m" : "";
export const YW = useColor ? "\x1b[1;33m" : "";

/**
* Preflight result line helpers (#6004). Render a warning (`⚠`) line in yellow
* and a failure (`✗`) line in red so they stand out from the default-colored
* `✓`/INFO lines in the lengthy onboard preflight output. Color is suppressed
* automatically when `NO_COLOR` is set or stdout is not a TTY (via `YW`/`RD`/`R`
* being empty strings), so CI output stays plain text.
*/
export function warnLine(message: string): string {
return ` ${YW}⚠ ${message}${R}`;
}

export function failLine(message: string): string {
return ` ${RD}✗ ${message}${R}`;
}
26 changes: 17 additions & 9 deletions src/lib/onboard/bridge-dns-preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* on the fatal reasons described in `[[isFatalContainerDnsProbeFailure]]`.
*/

import { failLine, warnLine } from "../cli/terminal-style";
import { cliDisplayName, cliName } from "./branding";

interface DaemonJsonDnsPatchOpts {
Expand Down Expand Up @@ -75,6 +76,7 @@ function printDaemonJsonDnsPatch(opts: DaemonJsonDnsPatchOpts): void {
].join(" ");
console.error(`${indent}${sudoPrefix}sh -c '${shBody.replace(/'/g, "'\"'\"'")}'`);
}

import {
BUSYBOX_PROBE_IMAGE,
DEFAULT_HOST_DNS_PROBE_HOSTNAME,
Expand All @@ -95,7 +97,7 @@ export function printDockerBridgeContainerStartFailure(
result: DockerBridgeContainerStartProbeResult,
host?: Pick<Host, "isWsl">,
): void {
console.error(" ✗ Docker could not start a bridge-network test container.");
console.error(failLine("Docker could not start a bridge-network test container."));
if (result.details) {
for (const line of String(result.details).split("\n").slice(-4)) {
if (line.trim()) console.error(` ${line.trim()}`);
Expand Down Expand Up @@ -209,7 +211,9 @@ export function assertDockerBridgeAndContainerDnsHealthy(host: Host, nonInteract
" but doesn't prove container DNS is broken — the sandbox build may still succeed.",
);
} else {
console.warn(` ⚠ Container DNS probe inconclusive (reason: ${dns.reason ?? "unknown"}).`);
console.warn(
warnLine(`Container DNS probe inconclusive (reason: ${dns.reason ?? "unknown"}).`),
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
if (dns.details) {
for (const line of String(dns.details).split("\n").slice(-3)) {
Expand Down Expand Up @@ -249,11 +253,11 @@ export function assertDockerBridgeAndContainerDnsHealthy(host: Host, nonInteract
process.exit(1);
}
if (dns.reason === "timeout" || dns.reason === "killed") {
console.error(" ✗ Container DNS probe did not complete.");
console.error(failLine("Container DNS probe did not complete."));
} else if (dns.reason === "image_pull_failed") {
console.error(" ✗ Docker could not resolve or pull the DNS probe image.");
console.error(failLine("Docker could not resolve or pull the DNS probe image."));
} else {
console.error(" ✗ DNS resolution from inside a docker container failed.");
console.error(failLine("DNS resolution from inside a docker container failed."));
}
if (dns.details) {
for (const line of String(dns.details).split("\n").slice(-4)) {
Expand Down Expand Up @@ -383,7 +387,7 @@ export function assertHostDnsHealthy(host: Host, opts: AssertHostDnsHealthyOpts
return;
}
if (!isFatalHostDnsProbeFailure(result)) {
console.warn(` ⚠ Host DNS probe inconclusive (reason: ${result.reason ?? "unknown"}).`);
console.warn(warnLine(`Host DNS probe inconclusive (reason: ${result.reason ?? "unknown"}).`));
if (result.details) {
console.warn(` ${String(result.details).trim()}`);
}
Expand All @@ -394,11 +398,15 @@ export function assertHostDnsHealthy(host: Host, opts: AssertHostDnsHealthyOpts
}

if (result.reason === "timeout" || result.reason === "killed") {
console.error(` ✗ Host DNS probe did not complete (could not resolve ${result.hostname}).`);
console.error(
failLine(`Host DNS probe did not complete (could not resolve ${result.hostname}).`),
);
} else if (result.reason === "resolution_failed") {
console.error(` ✗ Host could not resolve ${result.hostname} (resolver answered, no record).`);
console.error(
failLine(`Host could not resolve ${result.hostname} (resolver answered, no record).`),
);
} else {
console.error(` ✗ Host DNS resolution failed (could not resolve ${result.hostname}).`);
console.error(failLine(`Host DNS resolution failed (could not resolve ${result.hostname}).`));
}
if (result.details) {
console.error(` ${String(result.details).trim()}`);
Expand Down
17 changes: 10 additions & 7 deletions src/lib/onboard/sandbox-gpu-preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import { dockerInfoFormat } from "../adapters/docker";
import { failLine, warnLine } from "../cli/terminal-style";
import type { GpuDetection } from "../inference/nim";
import type { SandboxGpuProofResult } from "../state/registry";
import { findReadableNvidiaCdiSpecFiles, getDockerCdiSpecDirs } from "./docker-cdi";
Expand Down Expand Up @@ -83,7 +84,7 @@ export function sandboxGpuRemediationLines(
export function exitOnSandboxGpuConfigErrors(config: SandboxGpuConfig): void {
if (config.errors.length > 0) {
console.error("");
for (const error of config.errors) console.error(` ✗ ${error}`);
for (const error of config.errors) console.error(failLine(error));
process.exit(1);
}
}
Expand Down Expand Up @@ -126,7 +127,7 @@ export function dockerNvidiaRuntimeAvailable(deps: SandboxGpuPreflightDeps = {})
function validateJetsonSandboxGpuPreflight(deps: SandboxGpuPreflightDeps): void {
if (!dockerNvidiaRuntimeAvailable(deps)) {
console.error("");
console.error(" ✗ Docker NVIDIA runtime was not detected for Jetson/Tegra sandbox GPU.");
console.error(failLine("Docker NVIDIA runtime was not detected for Jetson/Tegra sandbox GPU."));
console.error(" Jetson sandbox GPU uses NVIDIA Container Runtime semantics, not CDI.");
console.error(
" Install/configure NVIDIA Container Toolkit for Docker, then restart Docker:",
Expand Down Expand Up @@ -228,7 +229,7 @@ export function createDirectSandboxGpuVerifier(
if (proof.optional !== true) {
// Required proof (e.g. the sandbox-exec wrapper itself): keep the
// historical hard-fail so onboarding aborts and rolls back.
console.error(` ✗ GPU proof failed: ${proof.label}`);
console.error(failLine(`GPU proof failed: ${proof.label}`));
if (diagnostic) console.error(` ${diagnostic}`);
for (const line of sandboxGpuRemediationLines({
wslDockerDesktopStatus: detectWslDockerDesktopStatus(deps),
Expand All @@ -248,7 +249,7 @@ export function createDirectSandboxGpuVerifier(
if (proof.id === CUDA_USABILITY_PROOF_ID && cudaInitRan) {
cudaFailure = { label: proof.label, detail: diagnostic };
}
console.warn(` ⚠ GPU proof inconclusive: ${proof.label}`);
console.warn(warnLine(`GPU proof inconclusive: ${proof.label}`));
if (diagnostic) console.warn(` ${diagnostic}`);
}
const status: SandboxGpuProofResult["status"] = cudaVerified
Expand All @@ -259,7 +260,7 @@ export function createDirectSandboxGpuVerifier(
if (status === "verified") {
console.log(" ✓ Sandbox CUDA usability proven (cuInit succeeded).");
} else if (status === "failed") {
console.warn(` ⚠ Sandbox CUDA proof failed: ${cudaFailure?.label}`);
console.warn(warnLine(`Sandbox CUDA proof failed: ${cudaFailure?.label}`));
const lines =
resolvedPlatform === "jetson"
? jetsonGpuProofRemediationLines()
Expand All @@ -268,7 +269,9 @@ export function createDirectSandboxGpuVerifier(
});
for (const line of lines) console.warn(` ${line}`);
} else {
console.warn(" ⚠ Sandbox GPU enabled but CUDA usability is unverified (no CUDA proof ran).");
console.warn(
warnLine("Sandbox GPU enabled but CUDA usability is unverified (no CUDA proof ran)."),
);
}
return {
status,
Expand Down Expand Up @@ -308,7 +311,7 @@ export function validateSandboxGpuPreflight(
);
if (cdiSpecFiles.length === 0) {
console.error("");
console.error(" ✗ Docker CDI GPU support was not detected.");
console.error(failLine("Docker CDI GPU support was not detected."));
for (const line of sandboxGpuRemediationLines({
wslDockerDesktopStatus,
})) {
Expand Down
Loading