diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 40a7c921d..933443312 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,6 +19,7 @@ # # Priority groups (prek: same priority may run in parallel; stock pre-commit ignores priority): # 0 — General file fixers (whitespace, EOF, line endings) +# 4 — SPDX header insertion (--fix) # 5 — Shell / Python / TS formatters (shfmt, ruff format, prettier) # 6 — Fixes that should follow formatters (ruff check --fix, eslint --fix) # 10 — Linters and read-only checks @@ -38,6 +39,18 @@ repos: args: ["--fix=lf"] priority: 0 + # ── Priority 4: SPDX headers (insert if missing; runs before language formatters) ── + - repo: local + hooks: + - id: spdx-headers + name: SPDX license headers (insert if missing) + entry: bash scripts/check-spdx-headers.sh --fix + language: system + files: ^(nemoclaw/src/.*\.ts|nemoclaw-blueprint/.*\.py|.*\.sh|\.husky/.*)$ + exclude: ^nemoclaw-blueprint/.*__init__\.py$ + pass_filenames: true + priority: 4 + # ── Priority 5: formatters ──────────────────────────────────────────────── - repo: https://github.com/scop/pre-commit-shfmt rev: v3.12.0-2 @@ -111,9 +124,6 @@ repos: rev: v0.11.0.1 hooks: - id: shellcheck - args: - - --severity=warning - - --exclude=SC1091 priority: 10 - repo: https://github.com/hadolint/hadolint @@ -122,16 +132,6 @@ repos: - id: hadolint priority: 10 - - repo: local - hooks: - - id: spdx-headers - name: SPDX license headers - entry: bash scripts/check-spdx-headers.sh - language: system - files: ^(nemoclaw/src/.*\.ts|nemoclaw-blueprint/.*\.py|.*\.sh|\.husky/.*)$ - pass_filenames: true - priority: 10 - - repo: https://github.com/gitleaks/gitleaks rev: v8.30.1 hooks: diff --git a/package.json b/package.json index aacdee319..7c63784c0 100644 --- a/package.json +++ b/package.json @@ -33,20 +33,23 @@ }, "lint-staged": { "nemoclaw/src/**/*.ts": [ - "bash scripts/check-spdx-headers.sh", + "bash scripts/check-spdx-headers.sh --fix", "sh -c 'cd nemoclaw && npx eslint --fix \"$@\"' _", "sh -c 'cd nemoclaw && npx prettier --write \"$@\"' _" ], "nemoclaw-blueprint/**/*.py": [ - "bash scripts/check-spdx-headers.sh", + "bash scripts/check-spdx-headers.sh --fix", "uvx ruff check --fix", "uvx ruff format" ], "**/*.sh": [ - "bash scripts/check-spdx-headers.sh", + "bash scripts/check-spdx-headers.sh --fix", "npx shellcheck" ], - ".husky/{pre-commit,pre-push,commit-msg}": "npx shellcheck" + ".husky/{pre-commit,pre-push,commit-msg}": [ + "bash scripts/check-spdx-headers.sh --fix", + "npx shellcheck" + ] }, "devDependencies": { "@commitlint/cli": "^20.5.0", diff --git a/scripts/check-spdx-headers.sh b/scripts/check-spdx-headers.sh index e67d0d49c..4f9306924 100755 --- a/scripts/check-spdx-headers.sh +++ b/scripts/check-spdx-headers.sh @@ -2,25 +2,99 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# Checks that all given files contain the required SPDX license headers. -# Usage: check-spdx-headers.sh file1 file2 ... +# SPDX header check (and optional auto-insert), similar in spirit to DOCA's check_doca_license.py --fix. +# +# Usage: +# check-spdx-headers.sh FILE... # fail if header missing +# check-spdx-headers.sh --fix FILE... # insert NVIDIA Apache-2.0 SPDX block after shebang (if any) set -euo pipefail -COPYRIGHT="SPDX-FileCopyrightText: Copyright (c)" -LICENSE="SPDX-License-Identifier: Apache-2.0" +COPYRIGHT_SUBSTR="SPDX-FileCopyrightText: Copyright (c)" +LICENSE_SUBSTR="SPDX-License-Identifier: Apache-2.0" + +usage() { + echo "Usage: $0 [--fix] FILE..." >&2 + exit 2 +} + +FIX=false +if [[ "${1:-}" == "--fix" ]]; then + FIX=true + shift +fi + +[[ $# -gt 0 ]] || usage + +has_spdx() { + local file=$1 + local head + head=$(head -n 16 -- "$file" 2>/dev/null) || return 1 + grep -Fq "$COPYRIGHT_SUBSTR" <<<"$head" || return 1 + grep -Fq "$LICENSE_SUBSTR" <<<"$head" || return 1 + return 0 +} + +comment_style_for() { + local base + base=$(basename "$1") + case "$base" in + Dockerfile | *.dockerfile | *.Dockerfile) echo "#" ;; + *.ts | *.tsx | *.js | *.mjs | *.cjs) echo "//" ;; + *) echo "#" ;; + esac +} + +spdx_block() { + local style=$1 + local year + year=$(date +%Y) + if [[ "$style" == "//" ]]; then + printf '// SPDX-FileCopyrightText: Copyright (c) %s NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n' "$year" + printf '// SPDX-License-Identifier: Apache-2.0\n' + else + printf '# SPDX-FileCopyrightText: Copyright (c) %s NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n' "$year" + printf '# SPDX-License-Identifier: Apache-2.0\n' + fi +} + +insert_spdx() { + local file=$1 + local style + style=$(comment_style_for "$file") + local tmp + tmp="$(mktemp "${TMPDIR:-/tmp}/nemoclaw-spdx.XXXXXX")" + { + IFS= read -r first || true + if [[ "$first" == '#!'* ]]; then + printf '%s\n' "$first" + spdx_block "$style" + printf '\n' + cat + else + spdx_block "$style" + printf '\n' + if [[ -n "${first:-}" ]]; then + printf '%s\n' "$first" + fi + cat + fi + } <"$file" >"$tmp" && mv "$tmp" "$file" +} failed=0 for file in "$@"; do - file_head="$(head -n 5 -- "$file")" - if ! grep -Fq "$COPYRIGHT" <<< "$file_head"; then - echo "Missing SPDX-FileCopyrightText: $file" - failed=1 + [[ -f "$file" ]] || continue + if has_spdx "$file"; then + continue fi - if ! grep -Fq "$LICENSE" <<< "$file_head"; then - echo "Missing SPDX-License-Identifier: $file" + if [[ "$FIX" == true ]]; then + echo "Adding SPDX header: $file" + insert_spdx "$file" + else + echo "Missing SPDX-FileCopyrightText or SPDX-License-Identifier (first ~16 lines): $file" failed=1 fi done -exit $failed +exit "$failed" diff --git a/scripts/husky-env.sh b/scripts/husky-env.sh index 27c001f7e..86a8b8bd8 100644 --- a/scripts/husky-env.sh +++ b/scripts/husky-env.sh @@ -1,3 +1,4 @@ +# shellcheck shell=bash # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # diff --git a/scripts/test-inference-local.sh b/scripts/test-inference-local.sh index 93aea3625..ebdfbcb07 100755 --- a/scripts/test-inference-local.sh +++ b/scripts/test-inference-local.sh @@ -1,4 +1,7 @@ #!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + # Test inference.local routing through OpenShell provider (local vLLM) -echo '{"model":"nvidia/nemotron-3-nano-30b-a3b","messages":[{"role":"user","content":"say hello"}]}' > /tmp/req.json +echo '{"model":"nvidia/nemotron-3-nano-30b-a3b","messages":[{"role":"user","content":"say hello"}]}' >/tmp/req.json curl -s https://inference.local/v1/chat/completions -H "Content-Type: application/json" -d @/tmp/req.json diff --git a/scripts/test-inference.sh b/scripts/test-inference.sh index cbe599f95..5553eaed5 100755 --- a/scripts/test-inference.sh +++ b/scripts/test-inference.sh @@ -1,4 +1,7 @@ #!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + # Test inference.local routing through OpenShell provider -echo '{"model":"nvidia/nemotron-3-super-120b-a12b","messages":[{"role":"user","content":"say hello"}]}' > /tmp/req.json +echo '{"model":"nvidia/nemotron-3-super-120b-a12b","messages":[{"role":"user","content":"say hello"}]}' >/tmp/req.json curl -s https://inference.local/v1/chat/completions -H "Content-Type: application/json" -d @/tmp/req.json diff --git a/test/e2e/test-double-onboard.sh b/test/e2e/test-double-onboard.sh index 7ebdddcd5..8bf330b0f 100755 --- a/test/e2e/test-double-onboard.sh +++ b/test/e2e/test-double-onboard.sh @@ -1,4 +1,7 @@ #!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + # Double onboard: verify that consecutive `nemoclaw onboard` runs recover # automatically from stale state (gateway, port forward, registry entries) # left behind by a previous run. @@ -23,14 +26,23 @@ set -uo pipefail PASS=0 FAIL=0 -SKIP=0 TOTAL=0 -pass() { ((PASS++)); ((TOTAL++)); printf '\033[32m PASS: %s\033[0m\n' "$1"; } -fail() { ((FAIL++)); ((TOTAL++)); printf '\033[31m FAIL: %s\033[0m\n' "$1"; } -skip() { ((SKIP++)); ((TOTAL++)); printf '\033[33m SKIP: %s\033[0m\n' "$1"; } -section() { echo ""; printf '\033[1;36m=== %s ===\033[0m\n' "$1"; } -info() { printf '\033[1;34m [info]\033[0m %s\n' "$1"; } +pass() { + ((PASS++)) + ((TOTAL++)) + printf '\033[32m PASS: %s\033[0m\n' "$1" +} +fail() { + ((FAIL++)) + ((TOTAL++)) + printf '\033[31m FAIL: %s\033[0m\n' "$1" +} +section() { + echo "" + printf '\033[1;36m=== %s ===\033[0m\n' "$1" +} +info() { printf '\033[1;34m [info]\033[0m %s\n' "$1"; } SANDBOX_A="e2e-double-a" SANDBOX_B="e2e-double-b" @@ -45,7 +57,7 @@ info "Destroying any leftover test sandboxes/gateway from previous runs..." # the nemoclaw registry at ~/.nemoclaw/sandboxes.json. Stale registry # entries from a previous run would cause Phase 2 to exit with # "Sandbox already exists" before the test even starts. -if command -v nemoclaw > /dev/null 2>&1; then +if command -v nemoclaw >/dev/null 2>&1; then nemoclaw "$SANDBOX_A" destroy 2>/dev/null || true nemoclaw "$SANDBOX_B" destroy 2>/dev/null || true fi @@ -60,21 +72,21 @@ pass "Pre-cleanup complete" # ══════════════════════════════════════════════════════════════════ section "Phase 1: Prerequisites" -if docker info > /dev/null 2>&1; then +if docker info >/dev/null 2>&1; then pass "Docker is running" else fail "Docker is not running — cannot continue" exit 1 fi -if command -v openshell > /dev/null 2>&1; then +if command -v openshell >/dev/null 2>&1; then pass "openshell CLI installed" else fail "openshell CLI not found — cannot continue" exit 1 fi -if command -v nemoclaw > /dev/null 2>&1; then +if command -v nemoclaw >/dev/null 2>&1; then pass "nemoclaw CLI installed" else fail "nemoclaw CLI not found — cannot continue" @@ -99,7 +111,7 @@ ONBOARD_LOG="$(mktemp)" NEMOCLAW_NON_INTERACTIVE=1 \ NEMOCLAW_SANDBOX_NAME="$SANDBOX_A" \ NEMOCLAW_POLICY_MODE=skip \ - nemoclaw onboard --non-interactive > "$ONBOARD_LOG" 2>&1 + nemoclaw onboard --non-interactive >"$ONBOARD_LOG" 2>&1 exit1=$? output1="$(cat "$ONBOARD_LOG")" rm -f "$ONBOARD_LOG" @@ -110,22 +122,30 @@ else fail "First onboard exited $exit1 (expected 1)" fi -echo "$output1" | grep -q "Sandbox '${SANDBOX_A}' created" \ - && pass "Sandbox '$SANDBOX_A' created (step 3 completed)" \ - || fail "Sandbox creation not confirmed in output" +if echo "$output1" | grep -q "Sandbox '${SANDBOX_A}' created"; then + pass "Sandbox '$SANDBOX_A' created (step 3 completed)" +else + fail "Sandbox creation not confirmed in output" +fi # Verify stale state was left behind -openshell gateway info -g nemoclaw 2>/dev/null | grep -q "nemoclaw" \ - && pass "Gateway is still running (stale state)" \ - || fail "Gateway is not running after first onboard" +if openshell gateway info -g nemoclaw 2>/dev/null | grep -q "nemoclaw"; then + pass "Gateway is still running (stale state)" +else + fail "Gateway is not running after first onboard" +fi -openshell sandbox get "$SANDBOX_A" > /dev/null 2>&1 \ - && pass "Sandbox '$SANDBOX_A' exists in openshell" \ - || fail "Sandbox '$SANDBOX_A' not found in openshell" +if openshell sandbox get "$SANDBOX_A" >/dev/null 2>&1; then + pass "Sandbox '$SANDBOX_A' exists in openshell" +else + fail "Sandbox '$SANDBOX_A' not found in openshell" +fi -[ -f "$REGISTRY" ] && grep -q "$SANDBOX_A" "$REGISTRY" \ - && pass "Registry contains '$SANDBOX_A'" \ - || fail "Registry does not contain '$SANDBOX_A'" +if [ -f "$REGISTRY" ] && grep -q "$SANDBOX_A" "$REGISTRY"; then + pass "Registry contains '$SANDBOX_A'" +else + fail "Registry does not contain '$SANDBOX_A'" +fi info "Stale state confirmed — NOT cleaning up before next onboard" @@ -140,7 +160,7 @@ NEMOCLAW_NON_INTERACTIVE=1 \ NEMOCLAW_SANDBOX_NAME="$SANDBOX_A" \ NEMOCLAW_RECREATE_SANDBOX=1 \ NEMOCLAW_POLICY_MODE=skip \ - nemoclaw onboard --non-interactive > "$ONBOARD_LOG" 2>&1 + nemoclaw onboard --non-interactive >"$ONBOARD_LOG" 2>&1 exit2=$? output2="$(cat "$ONBOARD_LOG")" rm -f "$ONBOARD_LOG" @@ -152,25 +172,35 @@ else fail "Second onboard exited $exit2 (expected 1)" fi -echo "$output2" | grep -q "Cleaning up previous NemoClaw session" \ - && pass "Stale session cleanup fired on second onboard" \ - || fail "Stale session cleanup did NOT fire (regression: #397)" +if echo "$output2" | grep -q "Cleaning up previous NemoClaw session"; then + pass "Stale session cleanup fired on second onboard" +else + fail "Stale session cleanup did NOT fire (regression: #397)" +fi -echo "$output2" | grep -q "Port 8080 is not available" \ - && fail "Port 8080 conflict detected (regression: #21)" \ - || pass "No port 8080 conflict" +if echo "$output2" | grep -q "Port 8080 is not available"; then + fail "Port 8080 conflict detected (regression: #21)" +else + pass "No port 8080 conflict" +fi -echo "$output2" | grep -q "Port 18789 is not available" \ - && fail "Port 18789 conflict detected" \ - || pass "No port 18789 conflict" +if echo "$output2" | grep -q "Port 18789 is not available"; then + fail "Port 18789 conflict detected" +else + pass "No port 18789 conflict" +fi -echo "$output2" | grep -q "Sandbox '${SANDBOX_A}' created" \ - && pass "Sandbox '$SANDBOX_A' recreated" \ - || fail "Sandbox '$SANDBOX_A' was not recreated" +if echo "$output2" | grep -q "Sandbox '${SANDBOX_A}' created"; then + pass "Sandbox '$SANDBOX_A' recreated" +else + fail "Sandbox '$SANDBOX_A' was not recreated" +fi -openshell gateway info -g nemoclaw 2>/dev/null | grep -q "nemoclaw" \ - && pass "Gateway running after second onboard" \ - || fail "Gateway not running after second onboard" +if openshell gateway info -g nemoclaw 2>/dev/null | grep -q "nemoclaw"; then + pass "Gateway running after second onboard" +else + fail "Gateway not running after second onboard" +fi # ══════════════════════════════════════════════════════════════════ # Phase 4: Third onboard — DIFFERENT name (e2e-double-b) @@ -182,7 +212,7 @@ ONBOARD_LOG="$(mktemp)" NEMOCLAW_NON_INTERACTIVE=1 \ NEMOCLAW_SANDBOX_NAME="$SANDBOX_B" \ NEMOCLAW_POLICY_MODE=skip \ - nemoclaw onboard --non-interactive > "$ONBOARD_LOG" 2>&1 + nemoclaw onboard --non-interactive >"$ONBOARD_LOG" 2>&1 exit3=$? output3="$(cat "$ONBOARD_LOG")" rm -f "$ONBOARD_LOG" @@ -193,21 +223,29 @@ else fail "Third onboard exited $exit3 (expected 1)" fi -echo "$output3" | grep -q "Cleaning up previous NemoClaw session" \ - && pass "Stale session cleanup fired on third onboard" \ - || fail "Stale session cleanup did NOT fire on third onboard" +if echo "$output3" | grep -q "Cleaning up previous NemoClaw session"; then + pass "Stale session cleanup fired on third onboard" +else + fail "Stale session cleanup did NOT fire on third onboard" +fi -echo "$output3" | grep -q "Port 8080 is not available" \ - && fail "Port 8080 conflict on third onboard (regression)" \ - || pass "No port 8080 conflict on third onboard" +if echo "$output3" | grep -q "Port 8080 is not available"; then + fail "Port 8080 conflict on third onboard (regression)" +else + pass "No port 8080 conflict on third onboard" +fi -echo "$output3" | grep -q "Port 18789 is not available" \ - && fail "Port 18789 conflict on third onboard" \ - || pass "No port 18789 conflict on third onboard" +if echo "$output3" | grep -q "Port 18789 is not available"; then + fail "Port 18789 conflict on third onboard" +else + pass "No port 18789 conflict on third onboard" +fi -echo "$output3" | grep -q "Sandbox '${SANDBOX_B}' created" \ - && pass "Sandbox '$SANDBOX_B' created" \ - || fail "Sandbox '$SANDBOX_B' was not created" +if echo "$output3" | grep -q "Sandbox '${SANDBOX_B}' created"; then + pass "Sandbox '$SANDBOX_B' created" +else + fail "Sandbox '$SANDBOX_B' was not created" +fi # ══════════════════════════════════════════════════════════════════ # Phase 5: Final cleanup @@ -221,17 +259,23 @@ openshell sandbox delete "$SANDBOX_B" 2>/dev/null || true openshell forward stop 18789 2>/dev/null || true openshell gateway destroy -g nemoclaw 2>/dev/null || true -openshell sandbox get "$SANDBOX_A" > /dev/null 2>&1 \ - && fail "Sandbox '$SANDBOX_A' still exists after cleanup" \ - || pass "Sandbox '$SANDBOX_A' cleaned up" +if openshell sandbox get "$SANDBOX_A" >/dev/null 2>&1; then + fail "Sandbox '$SANDBOX_A' still exists after cleanup" +else + pass "Sandbox '$SANDBOX_A' cleaned up" +fi -openshell sandbox get "$SANDBOX_B" > /dev/null 2>&1 \ - && fail "Sandbox '$SANDBOX_B' still exists after cleanup" \ - || pass "Sandbox '$SANDBOX_B' cleaned up" +if openshell sandbox get "$SANDBOX_B" >/dev/null 2>&1; then + fail "Sandbox '$SANDBOX_B' still exists after cleanup" +else + pass "Sandbox '$SANDBOX_B' cleaned up" +fi -[ -f "$REGISTRY" ] && grep -q "$SANDBOX_A\|$SANDBOX_B" "$REGISTRY" \ - && fail "Registry still contains test sandbox entries" \ - || pass "Registry cleaned up" +if [ -f "$REGISTRY" ] && grep -q "$SANDBOX_A\|$SANDBOX_B" "$REGISTRY"; then + fail "Registry still contains test sandbox entries" +else + pass "Registry cleaned up" +fi pass "Final cleanup complete" @@ -243,7 +287,6 @@ echo "========================================" echo " Double Onboard E2E Results:" echo " Passed: $PASS" echo " Failed: $FAIL" -echo " Skipped: $SKIP" echo " Total: $TOTAL" echo "========================================" diff --git a/test/e2e/test-full-e2e.sh b/test/e2e/test-full-e2e.sh old mode 100644 new mode 100755 index 5441ca684..a05c221d9 --- a/test/e2e/test-full-e2e.sh +++ b/test/e2e/test-full-e2e.sh @@ -1,4 +1,7 @@ #!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + # Full E2E: install → onboard → verify inference (REAL services, no mocks) # # Proves the COMPLETE user journey including real inference against @@ -28,11 +31,26 @@ FAIL=0 SKIP=0 TOTAL=0 -pass() { ((PASS++)); ((TOTAL++)); printf '\033[32m PASS: %s\033[0m\n' "$1"; } -fail() { ((FAIL++)); ((TOTAL++)); printf '\033[31m FAIL: %s\033[0m\n' "$1"; } -skip() { ((SKIP++)); ((TOTAL++)); printf '\033[33m SKIP: %s\033[0m\n' "$1"; } -section() { echo ""; printf '\033[1;36m=== %s ===\033[0m\n' "$1"; } -info() { printf '\033[1;34m [info]\033[0m %s\n' "$1"; } +pass() { + ((PASS++)) + ((TOTAL++)) + printf '\033[32m PASS: %s\033[0m\n' "$1" +} +fail() { + ((FAIL++)) + ((TOTAL++)) + printf '\033[31m FAIL: %s\033[0m\n' "$1" +} +skip() { + ((SKIP++)) + ((TOTAL++)) + printf '\033[33m SKIP: %s\033[0m\n' "$1" +} +section() { + echo "" + printf '\033[1;36m=== %s ===\033[0m\n' "$1" +} +info() { printf '\033[1;34m [info]\033[0m %s\n' "$1"; } # Parse chat completion response — handles both content and reasoning_content # (nemotron-3-super is a reasoning model that may put output in reasoning_content) @@ -67,10 +85,10 @@ SANDBOX_NAME="${NEMOCLAW_SANDBOX_NAME:-e2e-nightly}" # ══════════════════════════════════════════════════════════════════ section "Phase 0: Pre-cleanup" info "Destroying any leftover sandbox/gateway from previous runs..." -if command -v nemoclaw > /dev/null 2>&1; then +if command -v nemoclaw >/dev/null 2>&1; then nemoclaw "$SANDBOX_NAME" destroy 2>/dev/null || true fi -if command -v openshell > /dev/null 2>&1; then +if command -v openshell >/dev/null 2>&1; then openshell sandbox delete "$SANDBOX_NAME" 2>/dev/null || true openshell gateway destroy -g nemoclaw 2>/dev/null || true fi @@ -81,7 +99,7 @@ pass "Pre-cleanup complete" # ══════════════════════════════════════════════════════════════════ section "Phase 1: Prerequisites" -if docker info > /dev/null 2>&1; then +if docker info >/dev/null 2>&1; then pass "Docker is running" else fail "Docker is not running — cannot continue" @@ -95,7 +113,7 @@ else exit 1 fi -if curl -sf --max-time 10 https://integrate.api.nvidia.com/v1/models > /dev/null 2>&1; then +if curl -sf --max-time 10 https://integrate.api.nvidia.com/v1/models >/dev/null 2>&1; then pass "Network access to integrate.api.nvidia.com" else fail "Cannot reach integrate.api.nvidia.com" @@ -112,7 +130,10 @@ fi # ══════════════════════════════════════════════════════════════════ section "Phase 2: Install nemoclaw (non-interactive mode)" -cd "$REPO" || { fail "Could not cd to repo root: $REPO"; exit 1; } +cd "$REPO" || { + fail "Could not cd to repo root: $REPO" + exit 1 +} info "Running install.sh --non-interactive..." info "This installs Node.js, openshell, NemoClaw, and runs onboard." @@ -122,7 +143,7 @@ INSTALL_LOG="/tmp/nemoclaw-e2e-install.log" # Write to a file instead of piping through tee. openshell's background # port-forward inherits pipe file descriptors, which prevents tee from exiting. # Use tail -f in the background for real-time output in CI logs. -bash install.sh --non-interactive > "$INSTALL_LOG" 2>&1 & +bash install.sh --non-interactive >"$INSTALL_LOG" 2>&1 & install_pid=$! tail -f "$INSTALL_LOG" --pid=$install_pid 2>/dev/null & tail_pid=$! @@ -133,11 +154,15 @@ wait $tail_pid 2>/dev/null || true # Source shell profile to pick up nvm/PATH changes from install.sh if [ -f "$HOME/.bashrc" ]; then + # shellcheck source=/dev/null source "$HOME/.bashrc" 2>/dev/null || true fi # Ensure nvm is loaded in current shell export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" -[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" +if [ -s "$NVM_DIR/nvm.sh" ]; then + # shellcheck source=/dev/null + . "$NVM_DIR/nvm.sh" +fi # Ensure ~/.local/bin is on PATH (openshell may be installed there in non-interactive mode) if [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then export PATH="$HOME/.local/bin:$PATH" @@ -151,7 +176,7 @@ else fi # Verify nemoclaw is on PATH -if command -v nemoclaw > /dev/null 2>&1; then +if command -v nemoclaw >/dev/null 2>&1; then pass "nemoclaw installed at $(command -v nemoclaw)" else fail "nemoclaw not found on PATH after install" @@ -159,16 +184,18 @@ else fi # Verify openshell was installed -if command -v openshell > /dev/null 2>&1; then +if command -v openshell >/dev/null 2>&1; then pass "openshell installed ($(openshell --version 2>&1 || echo unknown))" else fail "openshell not found on PATH after install" exit 1 fi -nemoclaw --help > /dev/null 2>&1 \ - && pass "nemoclaw --help exits 0" \ - || fail "nemoclaw --help failed" +if nemoclaw --help >/dev/null 2>&1; then + pass "nemoclaw --help exits 0" +else + fail "nemoclaw --help failed" +fi # ══════════════════════════════════════════════════════════════════ # Phase 3: Sandbox verification @@ -177,16 +204,17 @@ section "Phase 3: Sandbox verification" # 3a: nemoclaw list if list_output=$(nemoclaw list 2>&1); then - echo "$list_output" | grep -Fq -- "$SANDBOX_NAME" \ - && pass "nemoclaw list contains '${SANDBOX_NAME}'" \ - || fail "nemoclaw list does not contain '${SANDBOX_NAME}'" + if echo "$list_output" | grep -Fq -- "$SANDBOX_NAME"; then + pass "nemoclaw list contains '${SANDBOX_NAME}'" + else + fail "nemoclaw list does not contain '${SANDBOX_NAME}'" + fi else fail "nemoclaw list failed: ${list_output:0:200}" fi # 3b: nemoclaw status -status_output=$(nemoclaw "$SANDBOX_NAME" status 2>&1) -if [ $? -eq 0 ]; then +if status_output=$(nemoclaw "$SANDBOX_NAME" status 2>&1); then pass "nemoclaw ${SANDBOX_NAME} status exits 0" else fail "nemoclaw ${SANDBOX_NAME} status failed: ${status_output:0:200}" @@ -195,23 +223,29 @@ fi # 3c: Inference must be configured by onboard (no fallback — if onboard # failed to configure it, that's a bug we want to catch) if inf_check=$(openshell inference get 2>&1); then - echo "$inf_check" | grep -qi "nvidia-nim" \ - && pass "Inference configured via onboard" \ - || fail "Inference not configured — onboard did not set up nvidia-nim provider" + if echo "$inf_check" | grep -qi "nvidia-nim"; then + pass "Inference configured via onboard" + else + fail "Inference not configured — onboard did not set up nvidia-nim provider" + fi else fail "openshell inference get failed: ${inf_check:0:200}" fi # 3d: Policy presets applied if policy_output=$(openshell policy get --full "$SANDBOX_NAME" 2>&1); then - echo "$policy_output" | grep -qi "network_policies" \ - && pass "Policy applied to sandbox" \ - || fail "No network policy found on sandbox" + if echo "$policy_output" | grep -qi "network_policies"; then + pass "Policy applied to sandbox" + else + fail "No network policy found on sandbox" + fi # Check that at least npm or pypi preset endpoints are present (onboard auto-suggests these) - echo "$policy_output" | grep -qi "registry.npmjs.org\|pypi.org" \ - && pass "Policy presets (npm/pypi) detected in sandbox policy" \ - || skip "Could not confirm npm/pypi presets in policy (may vary by environment)" + if echo "$policy_output" | grep -qi "registry.npmjs.org\|pypi.org"; then + pass "Policy presets (npm/pypi) detected in sandbox policy" + else + skip "Could not confirm npm/pypi presets in policy (may vary by environment)" + fi else fail "openshell policy get failed: ${policy_output:0:200}" fi @@ -249,11 +283,11 @@ info "[LIVE] Sandbox inference test → user → sandbox → gateway → NVIDIA ssh_config="$(mktemp)" sandbox_response="" -if openshell sandbox ssh-config "$SANDBOX_NAME" > "$ssh_config" 2>/dev/null; then +if openshell sandbox ssh-config "$SANDBOX_NAME" >"$ssh_config" 2>/dev/null; then # Use timeout if available (Linux, Homebrew), fall back to plain ssh TIMEOUT_CMD="" - command -v timeout > /dev/null 2>&1 && TIMEOUT_CMD="timeout 90" - command -v gtimeout > /dev/null 2>&1 && TIMEOUT_CMD="gtimeout 90" + command -v timeout >/dev/null 2>&1 && TIMEOUT_CMD="timeout 90" + command -v gtimeout >/dev/null 2>&1 && TIMEOUT_CMD="gtimeout 90" sandbox_response=$($TIMEOUT_CMD ssh -F "$ssh_config" \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ @@ -263,7 +297,7 @@ if openshell sandbox ssh-config "$SANDBOX_NAME" > "$ssh_config" 2>/dev/null; the "curl -s --max-time 60 https://inference.local/v1/chat/completions \ -H 'Content-Type: application/json' \ -d '{\"model\":\"nvidia/nemotron-3-super-120b-a12b\",\"messages\":[{\"role\":\"user\",\"content\":\"Reply with exactly one word: PONG\"}],\"max_tokens\":100}'" \ - 2>&1) || true + 2>&1) || true fi rm -f "$ssh_config" @@ -308,9 +342,11 @@ nemoclaw "$SANDBOX_NAME" destroy 2>&1 | tail -3 || true openshell gateway destroy -g nemoclaw 2>/dev/null || true list_after=$(nemoclaw list 2>&1) -echo "$list_after" | grep -Fq -- "$SANDBOX_NAME" \ - && fail "Sandbox ${SANDBOX_NAME} still in list after destroy" \ - || pass "Sandbox ${SANDBOX_NAME} removed" +if echo "$list_after" | grep -Fq -- "$SANDBOX_NAME"; then + fail "Sandbox ${SANDBOX_NAME} still in list after destroy" +else + pass "Sandbox ${SANDBOX_NAME} removed" +fi # ══════════════════════════════════════════════════════════════════ # Summary