From c5133339e1f5319c427f31b574d9f754cbf77317 Mon Sep 17 00:00:00 2001 From: San Dang Date: Tue, 19 May 2026 13:43:39 +0530 Subject: [PATCH 1/7] test(e2e): cover onboard negative paths --- .github/workflows/nightly-e2e.yaml | 45 +- src/lib/onboard.ts | 6 +- src/lib/onboard/e2e-failure-injection.ts | 11 + test/e2e/docs/parity-map.yaml | 5 + test/e2e/test-onboard-negative-paths.sh | 521 +++++++++++++++++++++++ test/e2e/test-onboard-repair.sh | 24 +- test/e2e/test-onboard-resume.sh | 19 +- 7 files changed, 603 insertions(+), 28 deletions(-) create mode 100644 src/lib/onboard/e2e-failure-injection.ts create mode 100755 test/e2e/test-onboard-negative-paths.sh diff --git a/.github/workflows/nightly-e2e.yaml b/.github/workflows/nightly-e2e.yaml index 422c8eaefd..a1f945de9c 100644 --- a/.github/workflows/nightly-e2e.yaml +++ b/.github/workflows/nightly-e2e.yaml @@ -87,7 +87,8 @@ on: snapshot-commands-e2e, shields-config-e2e, rebuild-openclaw-e2e, upgrade-stale-sandbox-e2e, rebuild-hermes-e2e, rebuild-hermes-stale-base-e2e, double-onboard-e2e, - onboard-repair-e2e, onboard-resume-e2e, runtime-overrides-e2e, + onboard-repair-e2e, onboard-resume-e2e, onboard-negative-paths-e2e, + runtime-overrides-e2e, credential-sanitization-e2e, telegram-injection-e2e, overlayfs-autofix-e2e, device-auth-health-e2e, launchable-smoke-e2e, gpu-e2e, gpu-double-onboard-e2e, @@ -1756,6 +1757,45 @@ jobs: path: test-onboard-resume-*.log if-no-files-found: ignore + # -- Onboard Negative Paths E2E ------------------------------- + onboard-negative-paths-e2e: + if: >- + github.repository == 'NVIDIA/NemoClaw' && + (github.event_name != 'workflow_dispatch' || + inputs.jobs == '' || + contains(format(',{0},', inputs.jobs), ',onboard-negative-paths-e2e,')) + runs-on: ubuntu-latest + timeout-minutes: 75 + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ inputs.target_ref || github.ref }} + - name: Install NemoClaw + env: + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} + NEMOCLAW_NON_INTERACTIVE: "1" + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" + run: bash install.sh --non-interactive --yes-i-accept-third-party-software + - name: Run onboard negative-path E2E test + env: + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} + NEMOCLAW_NON_INTERACTIVE: "1" + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" + run: | + [ -f "$HOME/.bashrc" ] && source "$HOME/.bashrc" 2>/dev/null || true + export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" + [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]] && export PATH="$HOME/.local/bin:$PATH" + bash test/e2e/test-onboard-negative-paths.sh + - name: Upload test log on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: onboard-negative-paths-test-log + path: /tmp/nemoclaw-e2e-onboard-negative-paths.log + if-no-files-found: ignore + # ── Runtime Overrides E2E ────────────────────────────────── runtime-overrides-e2e: if: >- @@ -2171,6 +2211,7 @@ jobs: double-onboard-e2e, onboard-repair-e2e, onboard-resume-e2e, + onboard-negative-paths-e2e, runtime-overrides-e2e, credential-sanitization-e2e, telegram-injection-e2e, @@ -2266,6 +2307,7 @@ jobs: double-onboard-e2e, onboard-repair-e2e, onboard-resume-e2e, + onboard-negative-paths-e2e, runtime-overrides-e2e, credential-sanitization-e2e, telegram-injection-e2e, @@ -2418,6 +2460,7 @@ jobs: double-onboard-e2e, onboard-repair-e2e, onboard-resume-e2e, + onboard-negative-paths-e2e, runtime-overrides-e2e, credential-sanitization-e2e, telegram-injection-e2e, diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index f011992aa9..570ca3accd 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -74,9 +74,8 @@ const { isLinuxDockerDriverGatewayEnabled }: typeof import("./onboard/docker-dri const { reconcileGatewayGpuReuseForGpuIntent, }: typeof import("./onboard/gateway-gpu-passthrough") = require("./onboard/gateway-gpu-passthrough"); -const { - syncPresetSelection, -}: typeof import("./onboard/policy-preset-sync") = require("./onboard/policy-preset-sync"); +const { syncPresetSelection }: typeof import("./onboard/policy-preset-sync") = require("./onboard/policy-preset-sync"); +const { maybeForceE2eStepFailure }: typeof import("./onboard/e2e-failure-injection") = require("./onboard/e2e-failure-injection"); const { gatherWechatConfig, hasWechatConfigDrift, @@ -9037,6 +9036,7 @@ function startRecordedStep( return session; }); } + maybeForceE2eStepFailure(stepName); } const ONBOARD_STEP_INDEX: Record = { diff --git a/src/lib/onboard/e2e-failure-injection.ts b/src/lib/onboard/e2e-failure-injection.ts new file mode 100644 index 0000000000..eba9b9c435 --- /dev/null +++ b/src/lib/onboard/e2e-failure-injection.ts @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** Testing-only hook for deterministic E2E resume/repair fault injection. */ +export function maybeForceE2eStepFailure(stepName: string): void { + if (process.env.NEMOCLAW_E2E_FAILURE_INJECTION !== "1") return; + const forcedStep = (process.env.NEMOCLAW_E2E_FORCE_FAIL_AT_STEP || "").trim(); + if (!forcedStep || forcedStep !== stepName) return; + console.error(` [e2e] Forced onboarding failure at step '${stepName}'.`); + process.exit(1); +} diff --git a/test/e2e/docs/parity-map.yaml b/test/e2e/docs/parity-map.yaml index 80b412e602..8baae0e04c 100644 --- a/test/e2e/docs/parity-map.yaml +++ b/test/e2e/docs/parity-map.yaml @@ -298,6 +298,11 @@ scripts: reason: live legacy behavior requires Docker, sudo hosts edit, and OpenShell; retained for bucket parity tracking owner: e2e-maintainers runner_requirement: Docker + sudo hosts edit + OpenShell + test-onboard-negative-paths.sh: + scenario: ubuntu-repo-cloud-openclaw + status: deferred + bucket: onboarding-negative-paths + assertions: [] test-brave-search-e2e.sh: scenario: ubuntu-repo-cloud-openclaw status: migrated diff --git a/test/e2e/test-onboard-negative-paths.sh b/test/e2e/test-onboard-negative-paths.sh new file mode 100755 index 0000000000..bdae755657 --- /dev/null +++ b/test/e2e/test-onboard-negative-paths.sh @@ -0,0 +1,521 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# E2E: onboard negative and edge-case paths. +# +# Regression coverage for issue #2573. The nightly happy-path onboard test +# should not be the only place that exercises non-interactive validation. +# +# Scenarios: +# 1. NEMOCLAW_POLICY_MODE=restricted falls back to tier suggestions. +# 2. NEMOCLAW_POLICY_MODE=nonexistent falls back to tier suggestions. +# 3. Invalid NVIDIA API key format is rejected without a stack trace. +# 4. Non-NVIDIA provider keys are not forced to use nvapi-. +# 5. A host listener on the configured gateway port produces a friendly conflict. +# 6. Custom non-interactive policy presets are applied. +# 7. NEMOCLAW_PROVIDER=cloud and NEMOCLAW_MODEL are honored. + +set -uo pipefail + +export NEMOCLAW_E2E_DEFAULT_TIMEOUT=1800 +SCRIPT_DIR_TIMEOUT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" +# shellcheck source=test/e2e/e2e-timeout.sh +source "${SCRIPT_DIR_TIMEOUT}/e2e-timeout.sh" + +LOG_FILE="${NEMOCLAW_E2E_LOG:-/tmp/nemoclaw-e2e-onboard-negative-paths.log}" +exec > >(tee "$LOG_FILE") 2>&1 + +PASS=0 +FAIL=0 +SKIP=0 +TOTAL=0 +PORT_HOLDER_PID="" + +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"; } + +if [ -d /workspace ] && [ -f /workspace/install.sh ]; then + REPO="/workspace" +elif [ -f "$(cd "$(dirname "$0")/../.." && pwd)/install.sh" ]; then + REPO="$(cd "$(dirname "$0")/../.." && pwd)" +else + echo "ERROR: Cannot find repo root." + exit 1 +fi + +run_nemoclaw() { + node "$REPO/bin/nemoclaw.js" "$@" +} + +if ! command -v nemoclaw >/dev/null 2>&1; then + nemoclaw() { node "$REPO/bin/nemoclaw.js" "$@"; } +fi + +SANDBOX_NAME="${NEMOCLAW_SANDBOX_NAME:-e2e-onboard-negative}" +CLOUD_MODEL="${NEMOCLAW_ONBOARD_NEGATIVE_MODEL:-nvidia/nemotron-3-super-120b-a12b}" +PORT_CONFLICT_PORT="${NEMOCLAW_ONBOARD_NEGATIVE_CONFLICT_PORT:-18080}" +SESSION_FILE="$HOME/.nemoclaw/onboard-session.json" +REGISTRY_FILE="$HOME/.nemoclaw/sandboxes.json" +RESTORE_API_KEY="${NVIDIA_API_KEY:-}" + +# shellcheck source=test/e2e/lib/sandbox-teardown.sh +. "$(dirname "${BASH_SOURCE[0]}")/lib/sandbox-teardown.sh" +register_sandbox_for_teardown "$SANDBOX_NAME" +register_sandbox_for_teardown "${SANDBOX_NAME}-bad-key" +register_sandbox_for_teardown "${SANDBOX_NAME}-port" + +cleanup_extra() { + set +e + if [ -n "$PORT_HOLDER_PID" ]; then + kill "$PORT_HOLDER_PID" >/dev/null 2>&1 || true + wait "$PORT_HOLDER_PID" >/dev/null 2>&1 || true + fi + openshell sandbox delete "$SANDBOX_NAME" >/dev/null 2>&1 || true + openshell sandbox delete "${SANDBOX_NAME}-bad-key" >/dev/null 2>&1 || true + openshell sandbox delete "${SANDBOX_NAME}-port" >/dev/null 2>&1 || true + openshell forward stop 18789 >/dev/null 2>&1 || true + openshell gateway destroy -g nemoclaw >/dev/null 2>&1 || true + rm -f "$SESSION_FILE" +} +trap 'cleanup_extra; _nemoclaw_sandbox_teardown' EXIT + +print_summary() { + echo "" + echo "========================================" + echo " PASS: $PASS" + echo " FAIL: $FAIL" + echo " SKIP: $SKIP" + echo " TOTAL: $TOTAL" + echo "========================================" + echo "" +} + +assert_no_stack_trace() { + local output="$1" + if printf '%s\n' "$output" | grep -Eq '(^|[[:space:]])(TypeError|ReferenceError|SyntaxError):|^[[:space:]]+at '; then + return 1 + fi + return 0 +} + +ensure_cli_build() { + if [ -f "$REPO/dist/lib/onboard.js" ] && [ -f "$REPO/dist/lib/validation.js" ]; then + return 0 + fi + info "dist/ is missing; building CLI..." + (cd "$REPO" && npm run build:cli) +} + +run_policy_fallback_check() { + local mode="$1" + node - "$REPO" "$mode" <<'NODE' +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); + +const repo = process.argv[2]; +const mode = process.argv[3]; +const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-negative-policy-")); +process.env.HOME = home; +process.env.NEMOCLAW_NON_INTERACTIVE = "1"; +process.env.NEMOCLAW_POLICY_TIER = "balanced"; +process.env.NEMOCLAW_POLICY_MODE = mode; +process.env.NEMOCLAW_POLICY_PRESETS = ""; + +try { + Object.defineProperty(process, "platform", { value: "darwin" }); +} catch {} + +const credentials = require(path.join(repo, "dist", "lib", "credentials", "store.js")); +const runner = require(path.join(repo, "dist", "lib", "runner.js")); +const registry = require(path.join(repo, "dist", "lib", "state", "registry.js")); +const policies = require(path.join(repo, "dist", "lib", "policy", "index.js")); +const resolveOpenshell = require(path.join(repo, "dist", "lib", "adapters", "openshell", "resolve.js")); + +credentials.prompt = async (msg) => { throw new Error(`unexpected prompt: ${msg}`); }; +credentials.ensureApiKey = async () => {}; +credentials.getCredential = () => null; +runner.run = () => ({ status: 0, stdout: "", stderr: "" }); +runner.runCapture = (command) => { + const text = Array.isArray(command) ? command.join(" ") : String(command); + if (text.includes("sandbox list")) return "test-sb Ready"; + return ""; +}; +registry.registerSandbox = () => true; +registry.updateSandbox = () => true; +registry.getSandbox = () => ({ name: "test-sb", model: null, provider: null }); +resolveOpenshell.resolveOpenshell = () => "/usr/bin/true"; + +const appliedCalls = []; +policies.applyPreset = (_sandbox, name) => { appliedCalls.push(name); return true; }; +policies.applyPresets = (_sandbox, names) => { + for (const name of names) appliedCalls.push(name); + return true; +}; +policies.getAppliedPresets = () => []; + +const warnings = []; +console.log = () => {}; +console.warn = (msg) => warnings.push(String(msg)); + +(async () => { + const { setupPoliciesWithSelection } = require(path.join(repo, "dist", "lib", "onboard.js")); + const applied = await setupPoliciesWithSelection("test-sb", {}); + if (!Array.isArray(applied) || applied.length === 0) { + throw new Error(`expected fallback presets for ${mode}, got ${JSON.stringify(applied)}`); + } + if (appliedCalls.length === 0) { + throw new Error(`expected preset application calls for ${mode}`); + } + if (!warnings.some((line) => line.includes(`Unsupported NEMOCLAW_POLICY_MODE: ${mode}`))) { + throw new Error(`missing unsupported-mode warning for ${mode}: ${warnings.join(" | ")}`); + } + if (!warnings.some((line) => line.includes("Falling back to suggested presets"))) { + throw new Error(`missing fallback warning for ${mode}: ${warnings.join(" | ")}`); + } + const hasTierHint = warnings.some((line) => line.includes("NEMOCLAW_POLICY_TIER=restricted")); + if (mode === "restricted" && !hasTierHint) { + throw new Error(`missing tier hint for restricted mode: ${warnings.join(" | ")}`); + } + if (mode !== "restricted" && hasTierHint) { + throw new Error(`unexpected tier hint for ${mode}: ${warnings.join(" | ")}`); + } +})() + .then(() => fs.rmSync(home, { recursive: true, force: true })) + .catch((err) => { + fs.rmSync(home, { recursive: true, force: true }); + console.error(err && err.stack ? err.stack : err); + process.exit(1); + }); +NODE +} + +run_validation_check() { + node - "$REPO" <<'NODE' +const path = require("node:path"); +const repo = process.argv[2]; +const { validateNvidiaApiKeyValue } = require(path.join(repo, "dist", "lib", "validation.js")); + +const nvidiaError = validateNvidiaApiKeyValue("not-a-nvidia-key", "NVIDIA_API_KEY"); +if (!nvidiaError || !nvidiaError.includes("Must start with nvapi-")) { + throw new Error(`expected NVIDIA key prefix rejection, got: ${nvidiaError}`); +} + +const anthropicError = validateNvidiaApiKeyValue("sk-ant-test-key-without-nvapi-prefix", "ANTHROPIC_API_KEY"); +if (anthropicError !== null) { + throw new Error(`expected Anthropic key to bypass nvapi- prefix enforcement, got: ${anthropicError}`); +} +NODE +} + +start_port_holder() { + local port="$1" + PORT_HOLDER_PID="" + node - "$port" <<'NODE' >/tmp/nemoclaw-e2e-port-holder.log 2>&1 & +const net = require("node:net"); +const port = Number(process.argv[2]); +const server = net.createServer((socket) => socket.end()); +server.on("error", (err) => { + console.error(err && err.message ? err.message : err); + process.exit(2); +}); +server.listen(port, "127.0.0.1", () => { + console.log("ready"); +}); +setInterval(() => {}, 1000); +NODE + PORT_HOLDER_PID=$! + local i + for i in $(seq 1 40); do + if node -e 'const net=require("node:net"); const port=Number(process.argv[1]); const s=net.connect(port,"127.0.0.1"); s.once("connect",()=>{s.destroy(); process.exit(0);}); s.once("error",()=>process.exit(1)); setTimeout(()=>process.exit(1),250);' "$port" >/dev/null 2>&1; then + return 0 + fi + if ! kill -0 "$PORT_HOLDER_PID" >/dev/null 2>&1; then + PORT_HOLDER_PID="" + return 1 + fi + sleep 0.25 + done + return 1 +} + +section "Phase 0: Prerequisites" + +if command -v node >/dev/null 2>&1; then + pass "Node.js available" +else + fail "Node.js not found" + print_summary + exit 1 +fi + +if ensure_cli_build; then + pass "CLI build output available" +else + fail "Could not build CLI" + print_summary + exit 1 +fi + +if docker info >/dev/null 2>&1; then + pass "Docker is running" +else + fail "Docker is not running" + print_summary + exit 1 +fi + +if command -v openshell >/dev/null 2>&1; then + pass "openshell CLI installed" +else + fail "openshell CLI not found" + print_summary + exit 1 +fi + +if [[ -n "$RESTORE_API_KEY" && "$RESTORE_API_KEY" == nvapi-* ]]; then + pass "NVIDIA_API_KEY is set" +else + fail "NVIDIA_API_KEY not set or invalid; required for live onboard scenarios" + print_summary + exit 1 +fi + +section "Phase 1: Pre-cleanup" +info "Destroying leftover test sandboxes and gateway state..." +run_nemoclaw "$SANDBOX_NAME" destroy --yes >/dev/null 2>&1 || true +run_nemoclaw "${SANDBOX_NAME}-bad-key" destroy --yes >/dev/null 2>&1 || true +run_nemoclaw "${SANDBOX_NAME}-port" destroy --yes >/dev/null 2>&1 || true +openshell sandbox delete "$SANDBOX_NAME" >/dev/null 2>&1 || true +openshell sandbox delete "${SANDBOX_NAME}-bad-key" >/dev/null 2>&1 || true +openshell sandbox delete "${SANDBOX_NAME}-port" >/dev/null 2>&1 || true +openshell forward stop 18789 >/dev/null 2>&1 || true +openshell gateway destroy -g nemoclaw >/dev/null 2>&1 || true +rm -f "$SESSION_FILE" +pass "Pre-cleanup complete" + +section "Phase 2: Policy-mode fallback validation" + +if run_policy_fallback_check restricted; then + pass "NEMOCLAW_POLICY_MODE=restricted falls back to suggested presets" +else + fail "NEMOCLAW_POLICY_MODE=restricted did not fall back cleanly" +fi + +if run_policy_fallback_check nonexistent; then + pass "NEMOCLAW_POLICY_MODE=nonexistent falls back to suggested presets" +else + fail "NEMOCLAW_POLICY_MODE=nonexistent did not fall back cleanly" +fi + +section "Phase 3: Provider credential validation" + +INVALID_KEY_LOG="$(mktemp)" +NEMOCLAW_NON_INTERACTIVE=1 \ + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 \ + NEMOCLAW_SANDBOX_NAME="${SANDBOX_NAME}-bad-key" \ + NEMOCLAW_RECREATE_SANDBOX=1 \ + NEMOCLAW_PROVIDER=cloud \ + NEMOCLAW_POLICY_MODE=skip \ + NVIDIA_API_KEY=not-a-nvidia-key \ + node "$REPO/bin/nemoclaw.js" onboard --non-interactive >"$INVALID_KEY_LOG" 2>&1 +invalid_key_exit=$? +invalid_key_output="$(cat "$INVALID_KEY_LOG")" +rm -f "$INVALID_KEY_LOG" +openshell gateway destroy -g nemoclaw >/dev/null 2>&1 || true +rm -f "$SESSION_FILE" + +if [ "$invalid_key_exit" -eq 1 ]; then + pass "Invalid NVIDIA API key exited 1" +else + fail "Invalid NVIDIA API key exited $invalid_key_exit (expected 1)" +fi + +if printf '%s\n' "$invalid_key_output" | grep -q "Invalid NVIDIA API key. Must start with nvapi-"; then + pass "Invalid NVIDIA API key message is explicit" +else + fail "Invalid NVIDIA API key message missing" +fi + +if assert_no_stack_trace "$invalid_key_output"; then + pass "Invalid NVIDIA API key path did not print a stack trace" +else + fail "Invalid NVIDIA API key path printed a stack trace" +fi + +if run_validation_check; then + pass "Provider-aware credential validation accepts non-NVIDIA key prefixes" +else + fail "Provider-aware credential validation rejected a non-NVIDIA key prefix" +fi + +section "Phase 4: Gateway port conflict" + +if start_port_holder "$PORT_CONFLICT_PORT"; then + pass "Held gateway port ${PORT_CONFLICT_PORT} with a host listener" +else + skip "Could not start a local holder on port ${PORT_CONFLICT_PORT}; attempting conflict assertion against any existing listener" +fi + +PORT_CONFLICT_LOG="$(mktemp)" +NEMOCLAW_NON_INTERACTIVE=1 \ + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 \ + NEMOCLAW_SANDBOX_NAME="${SANDBOX_NAME}-port" \ + NEMOCLAW_RECREATE_SANDBOX=1 \ + NEMOCLAW_GATEWAY_PORT="$PORT_CONFLICT_PORT" \ + NEMOCLAW_PROVIDER=cloud \ + NEMOCLAW_POLICY_MODE=skip \ + NVIDIA_API_KEY="$RESTORE_API_KEY" \ + node "$REPO/bin/nemoclaw.js" onboard --non-interactive >"$PORT_CONFLICT_LOG" 2>&1 +port_conflict_exit=$? +port_conflict_output="$(cat "$PORT_CONFLICT_LOG")" +rm -f "$PORT_CONFLICT_LOG" + +if [ -n "$PORT_HOLDER_PID" ]; then + kill "$PORT_HOLDER_PID" >/dev/null 2>&1 || true + wait "$PORT_HOLDER_PID" >/dev/null 2>&1 || true + PORT_HOLDER_PID="" +fi +rm -f "$SESSION_FILE" + +if [ "$port_conflict_exit" -eq 1 ]; then + pass "Onboard rejected occupied gateway port" +else + fail "Occupied gateway port exited $port_conflict_exit (expected 1)" +fi + +if printf '%s\n' "$port_conflict_output" | grep -q "Port ${PORT_CONFLICT_PORT} is not available"; then + pass "Port conflict message is user-friendly" +else + fail "Port conflict message missing" +fi + +if assert_no_stack_trace "$port_conflict_output"; then + pass "Port conflict path did not print a stack trace" +else + fail "Port conflict path printed a stack trace" +fi + +section "Phase 5: Live non-interactive onboard honors presets and model" + +LIVE_LOG="$(mktemp)" +NEMOCLAW_NON_INTERACTIVE=1 \ + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 \ + NEMOCLAW_SANDBOX_NAME="$SANDBOX_NAME" \ + NEMOCLAW_RECREATE_SANDBOX=1 \ + NEMOCLAW_PROVIDER=cloud \ + NEMOCLAW_MODEL="$CLOUD_MODEL" \ + NEMOCLAW_POLICY_MODE=custom \ + NEMOCLAW_POLICY_PRESETS=npm,pypi \ + NVIDIA_API_KEY="$RESTORE_API_KEY" \ + node "$REPO/bin/nemoclaw.js" onboard --non-interactive >"$LIVE_LOG" 2>&1 +live_exit=$? +live_output="$(cat "$LIVE_LOG")" +rm -f "$LIVE_LOG" + +if [ "$live_exit" -eq 0 ]; then + pass "Live non-interactive onboard completed" +else + fail "Live non-interactive onboard exited $live_exit" + printf '%s\n' "$live_output" | tail -120 + print_summary + exit 1 +fi + +if printf '%s\n' "$live_output" | grep -q "Using NVIDIA Endpoints with model: ${CLOUD_MODEL}"; then + pass "Live onboard selected requested cloud model" +else + fail "Live onboard output did not confirm requested cloud model" +fi + +if node - "$REGISTRY_FILE" "$SANDBOX_NAME" "$CLOUD_MODEL" <<'NODE'; then +const fs = require("node:fs"); +const [registryPath, sandboxName, expectedModel] = process.argv.slice(2); +const registry = JSON.parse(fs.readFileSync(registryPath, "utf8")); +const sandbox = registry.sandboxes && registry.sandboxes[sandboxName]; +if (!sandbox) throw new Error(`missing sandbox registry entry: ${sandboxName}`); +if (sandbox.provider !== "nvidia-prod") { + throw new Error(`expected provider nvidia-prod, got ${sandbox.provider}`); +} +if (sandbox.model !== expectedModel) { + throw new Error(`expected model ${expectedModel}, got ${sandbox.model}`); +} +const policies = Array.isArray(sandbox.policies) ? sandbox.policies : []; +for (const preset of ["npm", "pypi"]) { + if (!policies.includes(preset)) { + throw new Error(`missing policy preset ${preset}; policies=${JSON.stringify(policies)}`); + } +} +NODE + pass "Registry recorded requested provider, model, and policy presets" +else + fail "Registry did not record requested provider, model, and policy presets" +fi + +if node - "$SESSION_FILE" "$SANDBOX_NAME" "$CLOUD_MODEL" <<'NODE'; then +const fs = require("node:fs"); +const [sessionPath, sandboxName, expectedModel] = process.argv.slice(2); +const session = JSON.parse(fs.readFileSync(sessionPath, "utf8")); +if (session.status !== "complete") throw new Error(`session status ${session.status}`); +if (session.sandboxName !== sandboxName) throw new Error(`session sandbox ${session.sandboxName}`); +if (session.provider !== "nvidia-prod") throw new Error(`session provider ${session.provider}`); +if (session.model !== expectedModel) throw new Error(`session model ${session.model}`); +const presets = Array.isArray(session.policyPresets) ? session.policyPresets : []; +for (const preset of ["npm", "pypi"]) { + if (!presets.includes(preset)) { + throw new Error(`missing session policy preset ${preset}; presets=${JSON.stringify(presets)}`); + } +} +NODE + pass "Session recorded requested provider, model, and policy presets" +else + fail "Session did not record requested provider, model, and policy presets" +fi + +section "Phase 6: Final cleanup" + +if [[ "${NEMOCLAW_E2E_KEEP_SANDBOX:-}" != "1" ]]; then + run_nemoclaw "$SANDBOX_NAME" destroy --yes >/dev/null 2>&1 || true +fi +openshell sandbox delete "$SANDBOX_NAME" >/dev/null 2>&1 || true +openshell forward stop 18789 >/dev/null 2>&1 || true +openshell gateway destroy -g nemoclaw >/dev/null 2>&1 || true +rm -f "$SESSION_FILE" + +if openshell sandbox get "$SANDBOX_NAME" >/dev/null 2>&1; then + fail "Sandbox '$SANDBOX_NAME' still exists after cleanup" +else + pass "Sandbox '$SANDBOX_NAME' cleaned up" +fi + +if [ -f "$SESSION_FILE" ]; then + fail "Onboard session file still exists after cleanup" +else + pass "Onboard session file cleaned up" +fi + +pass "Final cleanup complete" +print_summary + +if [ "$FAIL" -ne 0 ]; then + exit 1 +fi diff --git a/test/e2e/test-onboard-repair.sh b/test/e2e/test-onboard-repair.sh index 052dafa4c5..8351b74878 100755 --- a/test/e2e/test-onboard-repair.sh +++ b/test/e2e/test-onboard-repair.sh @@ -162,22 +162,19 @@ pass "Exported NVIDIA_API_KEY for the repair run (host writes nothing to disk; O # Phase 2: Create interrupted resumable state # ══════════════════════════════════════════════════════════════════ section "Phase 2: Create interrupted state" -info "Running onboard with POLICY_MODE=custom but no POLICY_PRESETS to force a policy-step failure..." +info "Running onboard with E2E failure injection at the policy step..." -# Use NEMOCLAW_POLICY_MODE=custom without NEMOCLAW_POLICY_PRESETS — this is a -# real validation path that exits 1 at the policy step after the sandbox is -# already created, leaving resumable session state. -# -# Note: the previous approach (NEMOCLAW_POLICY_MODE=invalid) stopped working -# after PR #2434 changed invalid modes from process.exit(1) to a graceful -# fallback with console.warn(). See #2573 for details. +# Force a deterministic interruption after the sandbox and OpenClaw setup +# complete, but before policy setup completes. This keeps repair coverage +# independent of product validation behavior such as policy-mode parsing. FIRST_LOG="$(mktemp)" NEMOCLAW_NON_INTERACTIVE=1 \ NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 \ NEMOCLAW_SANDBOX_NAME="$SANDBOX_NAME" \ NEMOCLAW_RECREATE_SANDBOX=1 \ - NEMOCLAW_POLICY_MODE=custom \ - NEMOCLAW_POLICY_PRESETS="" \ + NEMOCLAW_POLICY_MODE=suggested \ + NEMOCLAW_E2E_FAILURE_INJECTION=1 \ + NEMOCLAW_E2E_FORCE_FAIL_AT_STEP=policies \ node "$REPO/bin/nemoclaw.js" onboard --non-interactive >"$FIRST_LOG" 2>&1 first_exit=$? first_output="$(cat "$FIRST_LOG")" @@ -197,7 +194,7 @@ else fail "Onboard session file missing after interrupted run" fi -if echo "$first_output" | grep -q "NEMOCLAW_POLICY_PRESETS is required when NEMOCLAW_POLICY_MODE=custom"; then +if echo "$first_output" | grep -q "\[e2e\] Forced onboarding failure at step 'policies'."; then pass "First run failed at policy setup as intended" else fail "First run did not fail at the expected policy step" @@ -288,8 +285,9 @@ NEMOCLAW_NON_INTERACTIVE=1 \ NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 \ NEMOCLAW_SANDBOX_NAME="$SANDBOX_NAME" \ NEMOCLAW_RECREATE_SANDBOX=1 \ - NEMOCLAW_POLICY_MODE=custom \ - NEMOCLAW_POLICY_PRESETS="" \ + NEMOCLAW_POLICY_MODE=suggested \ + NEMOCLAW_E2E_FAILURE_INJECTION=1 \ + NEMOCLAW_E2E_FORCE_FAIL_AT_STEP=policies \ node "$REPO/bin/nemoclaw.js" onboard --non-interactive >"$REINJECT_LOG" 2>&1 || true rm -f "$REINJECT_LOG" pass "Re-created interrupted session for conflict tests" diff --git a/test/e2e/test-onboard-resume.sh b/test/e2e/test-onboard-resume.sh index d1a38cc7c3..129f121e53 100755 --- a/test/e2e/test-onboard-resume.sh +++ b/test/e2e/test-onboard-resume.sh @@ -142,22 +142,19 @@ pass "Exported NVIDIA_API_KEY for the resume run (host writes nothing to disk; O # Phase 2: First onboard (forced failure after sandbox creation) # ══════════════════════════════════════════════════════════════════ section "Phase 2: First onboard (interrupted)" -info "Running onboard with POLICY_MODE=custom but no POLICY_PRESETS to force a policy-step failure..." +info "Running onboard with E2E failure injection at the policy step..." -# Use NEMOCLAW_POLICY_MODE=custom without NEMOCLAW_POLICY_PRESETS — this is a -# real validation path that exits 1 at the policy step after the sandbox is -# already created, leaving resumable session state. -# -# Note: the previous approach (NEMOCLAW_POLICY_MODE=invalid) stopped working -# after PR #2434 changed invalid modes from process.exit(1) to a graceful -# fallback with console.warn(). See #2573 for details. +# Force a deterministic interruption after the sandbox and OpenClaw setup +# complete, but before policy setup completes. This keeps resume coverage +# independent of product validation behavior such as policy-mode parsing. FIRST_LOG="$(mktemp)" NEMOCLAW_NON_INTERACTIVE=1 \ NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 \ NEMOCLAW_SANDBOX_NAME="$SANDBOX_NAME" \ NEMOCLAW_RECREATE_SANDBOX=1 \ - NEMOCLAW_POLICY_MODE=custom \ - NEMOCLAW_POLICY_PRESETS="" \ + NEMOCLAW_POLICY_MODE=suggested \ + NEMOCLAW_E2E_FAILURE_INJECTION=1 \ + NEMOCLAW_E2E_FORCE_FAIL_AT_STEP=policies \ node "$REPO/bin/nemoclaw.js" onboard --non-interactive >"$FIRST_LOG" 2>&1 first_exit=$? first_output="$(cat "$FIRST_LOG")" @@ -177,7 +174,7 @@ else fail "Sandbox creation not confirmed in first run output" fi -if echo "$first_output" | grep -q "NEMOCLAW_POLICY_PRESETS is required when NEMOCLAW_POLICY_MODE=custom"; then +if echo "$first_output" | grep -q "\[e2e\] Forced onboarding failure at step 'policies'."; then pass "First run failed at policy setup as intended" else fail "First run did not fail at the expected policy step" From 221961807f685340ce4868eb71938dbd98a4bbc0 Mon Sep 17 00:00:00 2001 From: San Dang Date: Tue, 19 May 2026 14:10:05 +0530 Subject: [PATCH 2/7] test(e2e): satisfy onboard hook gates --- ci/env-var-doc-allowlist.json | 8 ++++++++ test/e2e/test-onboard-negative-paths.sh | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/ci/env-var-doc-allowlist.json b/ci/env-var-doc-allowlist.json index fe63e5daa5..6722d10f6e 100644 --- a/ci/env-var-doc-allowlist.json +++ b/ci/env-var-doc-allowlist.json @@ -38,5 +38,13 @@ { "name": "NEMOCLAW_TEST_NO_SLEEP", "reason": "Test sentinel that bypasses real-time sleep() calls in onboard inference probes. Set to '1' only by Vitest tests; never user-set." + }, + { + "name": "NEMOCLAW_E2E_FAILURE_INJECTION", + "reason": "Internal E2E-only sentinel that enables deterministic onboarding fault injection for resume/repair scripts. Never user-set in production." + }, + { + "name": "NEMOCLAW_E2E_FORCE_FAIL_AT_STEP", + "reason": "Internal E2E-only selector naming the onboarding step where deterministic fault injection should exit. Used only with NEMOCLAW_E2E_FAILURE_INJECTION in test scripts." } ] diff --git a/test/e2e/test-onboard-negative-paths.sh b/test/e2e/test-onboard-negative-paths.sh index bdae755657..3a5c29410d 100755 --- a/test/e2e/test-onboard-negative-paths.sh +++ b/test/e2e/test-onboard-negative-paths.sh @@ -244,8 +244,8 @@ server.listen(port, "127.0.0.1", () => { setInterval(() => {}, 1000); NODE PORT_HOLDER_PID=$! - local i - for i in $(seq 1 40); do + local _i + for _i in $(seq 1 40); do if node -e 'const net=require("node:net"); const port=Number(process.argv[1]); const s=net.connect(port,"127.0.0.1"); s.once("connect",()=>{s.destroy(); process.exit(0);}); s.once("error",()=>process.exit(1)); setTimeout(()=>process.exit(1),250);' "$port" >/dev/null 2>&1; then return 0 fi From 048da39ff259706e839b39b48e42190c32ede71e Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Tue, 19 May 2026 14:46:48 -0400 Subject: [PATCH 3/7] test(e2e): migrate onboard negatives to scenarios --- .github/workflows/nightly-e2e.yaml | 7 ++- test/e2e/docs/parity-map.yaml | 37 +++++++++++- .../nemoclaw_scenarios/expected-states.yaml | 56 +++++++++++++++++++ .../nemoclaw_scenarios/onboard/dispatch.sh | 11 ++++ test/e2e/nemoclaw_scenarios/scenarios.yaml | 49 ++++++++++++++++ test/e2e/runtime/lib/negative.sh | 34 +++++++++++ test/e2e/runtime/lib/onboard-state.sh | 47 ++++++++++++++++ test/e2e/runtime/lib/port-holder.sh | 46 +++++++++++++++ test/e2e/runtime/run-scenario.sh | 52 +++++++++++++++++ .../e2e-scenario-resolver.test.ts | 26 +++++++++ .../00-registry-provider-model-policies.sh | 23 ++++++++ .../01-session-provider-model-policies.sh | 23 ++++++++ test/e2e/validation_suites/suites.yaml | 9 +++ 13 files changed, 416 insertions(+), 4 deletions(-) create mode 100755 test/e2e/runtime/lib/negative.sh create mode 100755 test/e2e/runtime/lib/onboard-state.sh create mode 100755 test/e2e/runtime/lib/port-holder.sh create mode 100755 test/e2e/validation_suites/onboarding/state/00-registry-provider-model-policies.sh create mode 100755 test/e2e/validation_suites/onboarding/state/01-session-provider-model-policies.sh diff --git a/.github/workflows/nightly-e2e.yaml b/.github/workflows/nightly-e2e.yaml index a1f945de9c..930de22756 100644 --- a/.github/workflows/nightly-e2e.yaml +++ b/.github/workflows/nightly-e2e.yaml @@ -1787,7 +1787,12 @@ jobs: export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]] && export PATH="$HOME/.local/bin:$PATH" - bash test/e2e/test-onboard-negative-paths.sh + for scenario in \ + ubuntu-repo-cloud-openclaw-custom-policies \ + ubuntu-invalid-nvidia-key-negative \ + ubuntu-gateway-port-conflict-negative; do + bash test/e2e/runtime/run-scenario.sh "$scenario" + done - name: Upload test log on failure if: failure() uses: actions/upload-artifact@v4 diff --git a/test/e2e/docs/parity-map.yaml b/test/e2e/docs/parity-map.yaml index 8baae0e04c..83782f936f 100644 --- a/test/e2e/docs/parity-map.yaml +++ b/test/e2e/docs/parity-map.yaml @@ -299,10 +299,41 @@ scripts: owner: e2e-maintainers runner_requirement: Docker + sudo hosts edit + OpenShell test-onboard-negative-paths.sh: - scenario: ubuntu-repo-cloud-openclaw - status: deferred + scenario: ubuntu-repo-cloud-openclaw-custom-policies + status: migrated bucket: onboarding-negative-paths - assertions: [] + assertions: + - legacy: NEMOCLAW_POLICY_MODE=restricted falls back to suggested presets + status: deferred + reason: pure policy fallback behavior belongs in focused TypeScript behavior tests + owner: e2e-maintainers + runner_requirement: local CLI build + - legacy: NEMOCLAW_POLICY_MODE=nonexistent falls back to suggested presets + status: deferred + reason: pure policy fallback behavior belongs in focused TypeScript behavior tests + owner: e2e-maintainers + runner_requirement: local CLI build + - legacy: Invalid NVIDIA API key exits cleanly with an explicit message and no stack trace + status: migrated + scenario: ubuntu-invalid-nvidia-key-negative + assertion_id: expected-state.failure.invalid-nvidia-api-key + - legacy: Non-NVIDIA provider key prefixes are accepted by provider-aware credential validation + status: deferred + reason: provider-aware prefix validation is pure validation logic and does not require live scenario coverage + owner: e2e-maintainers + runner_requirement: local CLI build + - legacy: Occupied onboard gateway port returns a user-friendly conflict error and no stack trace + status: migrated + scenario: ubuntu-gateway-port-conflict-negative + assertion_id: expected-state.failure.gateway-port-conflict + - legacy: Non-interactive onboard honors explicit NEMOCLAW_POLICY_PRESETS + status: migrated + scenario: ubuntu-repo-cloud-openclaw-custom-policies + assertion_id: onboarding-state.registry-provider-model-policies + - legacy: Non-interactive onboard honors NEMOCLAW_PROVIDER=cloud plus the requested model + status: migrated + scenario: ubuntu-repo-cloud-openclaw-custom-policies + assertion_id: onboarding-state.session-provider-model-policies test-brave-search-e2e.sh: scenario: ubuntu-repo-cloud-openclaw status: migrated diff --git a/test/e2e/nemoclaw_scenarios/expected-states.yaml b/test/e2e/nemoclaw_scenarios/expected-states.yaml index e5a93c4aba..24a2b9181d 100644 --- a/test/e2e/nemoclaw_scenarios/expected-states.yaml +++ b/test/e2e/nemoclaw_scenarios/expected-states.yaml @@ -60,6 +60,32 @@ expected_states: policy_engine: supported shields: supported + cloud-openclaw-custom-policies-ready: + cli: + installed: true + gateway: + expected: present + health: healthy + sandbox: + expected: present + status: running + agent: openclaw + inference: + expected: available + provider: nvidia + route: inference-local + mode: gateway-routed + credentials: + expected: present + storage: gateway-managed + onboarding_state: + provider: nvidia-prod + model: nvidia/nemotron-3-super-120b-a12b + policy_presets: npm,pypi + security: + policy_engine: supported + shields: supported + cloud-hermes-ready: cli: installed: true @@ -118,3 +144,33 @@ expected_states: failure: expected: true stage: preflight + + onboarding-failure-invalid-nvidia-key: + cli: + installed: true + gateway: + expected: absent + sandbox: + expected: absent + failure: + expected: true + stage: onboarding + reason: invalid-nvidia-api-key + exit_code: 1 + message_contains: Invalid NVIDIA API key. Must start with nvapi- + no_stack_trace: true + + onboarding-failure-gateway-port-conflict: + cli: + installed: true + gateway: + expected: absent + sandbox: + expected: absent + failure: + expected: true + stage: onboarding + reason: gateway-port-conflict + exit_code: 1 + message_contains: Port 18080 is not available + no_stack_trace: true diff --git a/test/e2e/nemoclaw_scenarios/onboard/dispatch.sh b/test/e2e/nemoclaw_scenarios/onboard/dispatch.sh index 1c9a561bca..2baf698986 100755 --- a/test/e2e/nemoclaw_scenarios/onboard/dispatch.sh +++ b/test/e2e/nemoclaw_scenarios/onboard/dispatch.sh @@ -34,6 +34,17 @@ e2e_onboard() { cloud-openclaw) e2e_onboard_cloud_openclaw ;; + cloud-openclaw-custom-policies) + E2E_ONBOARDING_MODEL="${E2E_ONBOARDING_MODEL:-nvidia/nemotron-3-super-120b-a12b}" + E2E_ONBOARDING_POLICY_PRESETS="${E2E_ONBOARDING_POLICY_PRESETS:-npm,pypi}" + e2e_context_set E2E_ONBOARDING_MODEL "${E2E_ONBOARDING_MODEL}" + e2e_context_set E2E_ONBOARDING_POLICY_PRESETS "${E2E_ONBOARDING_POLICY_PRESETS}" + e2e_context_set E2E_ONBOARDING_REGISTRY_PROVIDER "nvidia-prod" + NEMOCLAW_MODEL="${E2E_ONBOARDING_MODEL}" NEMOCLAW_POLICY_MODE=custom NEMOCLAW_POLICY_PRESETS="${E2E_ONBOARDING_POLICY_PRESETS}" e2e_onboard_cloud_openclaw + ;; + cloud-openclaw-invalid-nvidia-key | cloud-openclaw-gateway-port-conflict) + e2e_onboard_cloud_openclaw + ;; cloud-hermes) e2e_onboard_cloud_hermes ;; diff --git a/test/e2e/nemoclaw_scenarios/scenarios.yaml b/test/e2e/nemoclaw_scenarios/scenarios.yaml index 31a8beaeff..dc898a746f 100644 --- a/test/e2e/nemoclaw_scenarios/scenarios.yaml +++ b/test/e2e/nemoclaw_scenarios/scenarios.yaml @@ -57,6 +57,27 @@ onboarding: agent: openclaw provider: nvidia inference_route: inference-local + cloud-openclaw-custom-policies: + path: cloud + agent: openclaw + provider: nvidia + inference_route: inference-local + model: nvidia/nemotron-3-super-120b-a12b + policy_presets: + - npm + - pypi + cloud-openclaw-invalid-nvidia-key: + path: cloud + agent: openclaw + provider: nvidia + inference_route: inference-local + invalid_api_key: not-a-nvidia-key + cloud-openclaw-gateway-port-conflict: + path: cloud + agent: openclaw + provider: nvidia + inference_route: inference-local + gateway_port: 18080 cloud-hermes: &id002 path: cloud agent: hermes @@ -173,6 +194,34 @@ setup_scenarios: onboarding: cloud-openclaw expected_state: preflight-failure-no-sandbox suites: [] + ubuntu-repo-cloud-openclaw-custom-policies: + dimensions: + platform: ubuntu-local + install: repo-current + runtime: docker-running + onboarding: cloud-openclaw-custom-policies + expected_state: cloud-openclaw-custom-policies-ready + suites: + - smoke + - inference + - credentials + - onboarding-state + ubuntu-invalid-nvidia-key-negative: + dimensions: + platform: ubuntu-local + install: repo-current + runtime: docker-running + onboarding: cloud-openclaw-invalid-nvidia-key + expected_state: onboarding-failure-invalid-nvidia-key + suites: [] + ubuntu-gateway-port-conflict-negative: + dimensions: + platform: ubuntu-local + install: repo-current + runtime: docker-running + onboarding: cloud-openclaw-gateway-port-conflict + expected_state: onboarding-failure-gateway-port-conflict + suites: [] base_scenarios: ubuntu-repo-docker: platform: ubuntu-local diff --git a/test/e2e/runtime/lib/negative.sh b/test/e2e/runtime/lib/negative.sh new file mode 100755 index 0000000000..95de65808d --- /dev/null +++ b/test/e2e/runtime/lib/negative.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Helpers for expected-failure E2E flows. + +e2e_negative_output_has_stack_trace() { + local output="$1" + printf '%s\n' "${output}" | grep -Eq '(^|[[:space:]])(TypeError|ReferenceError|SyntaxError):|^[[:space:]]+at ' +} + +e2e_negative_assert_failure() { + local log_file="$1" + local actual_exit="$2" + local expected_exit="$3" + local message_contains="$4" + local no_stack_trace="${5:-0}" + + if [[ "${actual_exit}" -ne "${expected_exit}" ]]; then + echo "expected failure exit ${expected_exit}, got ${actual_exit}" >&2 + cat "${log_file}" >&2 + return 1 + fi + if [[ -n "${message_contains}" ]] && ! grep -Fq "${message_contains}" "${log_file}"; then + echo "expected failure output to contain: ${message_contains}" >&2 + cat "${log_file}" >&2 + return 1 + fi + if [[ "${no_stack_trace}" == "1" ]] && e2e_negative_output_has_stack_trace "$(cat "${log_file}")"; then + echo "expected failure output not to contain a JavaScript stack trace" >&2 + cat "${log_file}" >&2 + return 1 + fi +} diff --git a/test/e2e/runtime/lib/onboard-state.sh b/test/e2e/runtime/lib/onboard-state.sh new file mode 100755 index 0000000000..5b82dbc1aa --- /dev/null +++ b/test/e2e/runtime/lib/onboard-state.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Assertions for onboard registry/session provider, model, and policy state. + +e2e_onboard_state_assert_registry() { + local registry_file="$1" + local sandbox_name="$2" + local expected_provider="$3" + local expected_model="$4" + local expected_presets_csv="$5" + node - "${registry_file}" "${sandbox_name}" "${expected_provider}" "${expected_model}" "${expected_presets_csv}" <<'NODE' +const fs = require("node:fs"); +const [registryPath, sandboxName, expectedProvider, expectedModel, csv] = process.argv.slice(2); +const registry = JSON.parse(fs.readFileSync(registryPath, "utf8")); +const sandbox = registry.sandboxes && registry.sandboxes[sandboxName]; +if (!sandbox) throw new Error(`missing sandbox registry entry: ${sandboxName}`); +if (sandbox.provider !== expectedProvider) throw new Error(`expected provider ${expectedProvider}, got ${sandbox.provider}`); +if (sandbox.model !== expectedModel) throw new Error(`expected model ${expectedModel}, got ${sandbox.model}`); +const policies = Array.isArray(sandbox.policies) ? sandbox.policies : []; +for (const preset of csv.split(",").filter(Boolean)) { + if (!policies.includes(preset)) throw new Error(`missing policy preset ${preset}; policies=${JSON.stringify(policies)}`); +} +NODE +} + +e2e_onboard_state_assert_session() { + local session_file="$1" + local sandbox_name="$2" + local expected_provider="$3" + local expected_model="$4" + local expected_presets_csv="$5" + node - "${session_file}" "${sandbox_name}" "${expected_provider}" "${expected_model}" "${expected_presets_csv}" <<'NODE' +const fs = require("node:fs"); +const [sessionPath, sandboxName, expectedProvider, expectedModel, csv] = process.argv.slice(2); +const session = JSON.parse(fs.readFileSync(sessionPath, "utf8")); +if (session.status !== "complete") throw new Error(`session status ${session.status}`); +if (session.sandboxName !== sandboxName) throw new Error(`session sandbox ${session.sandboxName}`); +if (session.provider !== expectedProvider) throw new Error(`session provider ${session.provider}`); +if (session.model !== expectedModel) throw new Error(`session model ${session.model}`); +const presets = Array.isArray(session.policyPresets) ? session.policyPresets : []; +for (const preset of csv.split(",").filter(Boolean)) { + if (!presets.includes(preset)) throw new Error(`missing session policy preset ${preset}; presets=${JSON.stringify(presets)}`); +} +NODE +} diff --git a/test/e2e/runtime/lib/port-holder.sh b/test/e2e/runtime/lib/port-holder.sh new file mode 100755 index 0000000000..32209dc534 --- /dev/null +++ b/test/e2e/runtime/lib/port-holder.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Local TCP listener helper for deterministic gateway port-conflict tests. + +E2E_PORT_HOLDER_PID="${E2E_PORT_HOLDER_PID:-}" + +e2e_port_holder_start() { + local port="$1" + E2E_PORT_HOLDER_PID="" + node - "${port}" <<'NODE' >/tmp/nemoclaw-e2e-port-holder.log 2>&1 & +const net = require("node:net"); +const port = Number(process.argv[2]); +const server = net.createServer((socket) => socket.end()); +server.on("error", (err) => { + console.error(err && err.message ? err.message : err); + process.exit(2); +}); +server.listen(port, "127.0.0.1", () => { + console.log("ready"); +}); +setInterval(() => {}, 1000); +NODE + E2E_PORT_HOLDER_PID=$! + local _i + for _i in $(seq 1 40); do + if node -e 'const net=require("node:net"); const port=Number(process.argv[1]); const s=net.connect(port,"127.0.0.1"); s.once("connect",()=>{s.destroy(); process.exit(0);}); s.once("error",()=>process.exit(1)); setTimeout(()=>process.exit(1),250);' "${port}" >/dev/null 2>&1; then + return 0 + fi + if ! kill -0 "${E2E_PORT_HOLDER_PID}" >/dev/null 2>&1; then + E2E_PORT_HOLDER_PID="" + return 1 + fi + sleep 0.25 + done + return 1 +} + +e2e_port_holder_stop() { + if [[ -n "${E2E_PORT_HOLDER_PID}" ]]; then + kill "${E2E_PORT_HOLDER_PID}" >/dev/null 2>&1 || true + wait "${E2E_PORT_HOLDER_PID}" >/dev/null 2>&1 || true + E2E_PORT_HOLDER_PID="" + fi +} diff --git a/test/e2e/runtime/run-scenario.sh b/test/e2e/runtime/run-scenario.sh index 26c28a395e..ba8bcb0571 100755 --- a/test/e2e/runtime/run-scenario.sh +++ b/test/e2e/runtime/run-scenario.sh @@ -178,6 +178,10 @@ ONBOARDING_ID="$(read_plan_string dimensions.onboarding.id)" RUNTIME_ID="$(read_plan_string dimensions.runtime.id)" RUNTIME_CONTAINER_DAEMON="$(read_plan_string dimensions.runtime.profile.container_daemon)" EXPECTED_STATE_ID="$(read_plan_string expected_state.id)" +FAILURE_STAGE="$(read_plan_string expected_state.config.failure.stage)" +FAILURE_EXIT_CODE="$(read_plan_string expected_state.config.failure.exit_code)" +FAILURE_MESSAGE_CONTAINS="$(read_plan_string expected_state.config.failure.message_contains)" +FAILURE_NO_STACK_TRACE="$(read_plan_string expected_state.config.failure.no_stack_trace)" # Trace the dimension id so scenario-level assertions can identify the # configured install (e.g. repo-current); e2e_install internally traces @@ -235,6 +239,54 @@ if [[ "${EXPECTED_STATE_ID}" == "preflight-failure-no-sandbox" ]]; then exit 0 fi + +if [[ "${FAILURE_STAGE}" == "onboarding" ]]; then + negative_log="${E2E_CONTEXT_DIR}/negative-onboarding.log" + sandbox_name="$(e2e_context_get E2E_SANDBOX_NAME)" + port_holder_started=0 + onboard_env=(NEMOCLAW_SANDBOX_NAME="${sandbox_name}" NEMOCLAW_RECREATE_SANDBOX=1 NEMOCLAW_POLICY_MODE=skip) + case "${ONBOARDING_ID}" in + cloud-openclaw-invalid-nvidia-key) + onboard_env+=(NVIDIA_API_KEY=not-a-nvidia-key) + ;; + cloud-openclaw-gateway-port-conflict) + conflict_port="$(read_plan_string dimensions.onboarding.profile.gateway_port)" + : "${conflict_port:=18080}" + if e2e_port_holder_start "${conflict_port}"; then + port_holder_started=1 + else + echo "run-scenario: could not start port holder on ${conflict_port}; continuing against any existing listener" >&2 + fi + onboard_env+=(NEMOCLAW_GATEWAY_PORT="${conflict_port}") + ;; + esac + if [[ "${DRY_RUN}" -eq 1 ]]; then + printf '%s +' "${FAILURE_MESSAGE_CONTAINS}" >"${negative_log}" + negative_status="${FAILURE_EXIT_CODE:-1}" + else + set +e + ( + export "${onboard_env[@]}" + e2e_onboard "${ONBOARDING_ID}" + ) >"${negative_log}" 2>&1 + negative_status=$? + set -e + fi + if [[ "${port_holder_started}" -eq 1 ]]; then + e2e_port_holder_stop + fi + if ! e2e_negative_assert_failure "${negative_log}" "${negative_status}" "${FAILURE_EXIT_CODE:-1}" "${FAILURE_MESSAGE_CONTAINS}" "$([[ "${FAILURE_NO_STACK_TRACE}" == "true" ]] && echo 1 || echo 0)"; then + exit 4 + fi + if openshell sandbox list 2>/dev/null | grep -Fq "${sandbox_name}"; then + echo "run-scenario: negative onboarding left behind sandbox ${sandbox_name}" >&2 + exit 4 + fi + echo "run-scenario: negative onboarding ${ONBOARDING_ID} passed" + exit 0 +fi + DOCKER_OPTIONAL_UNAVAILABLE=0 if [[ "${RUNTIME_CONTAINER_DAEMON}" == "optional" ]] && ! docker info >/dev/null 2>&1; then DOCKER_OPTIONAL_UNAVAILABLE=1 diff --git a/test/e2e/scenario-framework-tests/e2e-scenario-resolver.test.ts b/test/e2e/scenario-framework-tests/e2e-scenario-resolver.test.ts index 8c6cf4929a..7caffd649a 100644 --- a/test/e2e/scenario-framework-tests/e2e-scenario-resolver.test.ts +++ b/test/e2e/scenario-framework-tests/e2e-scenario-resolver.test.ts @@ -40,6 +40,32 @@ describe("E2E scenario resolver", () => { } }); + it("should_resolve_onboard_negative_path_migration_scenarios", () => { + const meta = realMetadata(); + const custom = resolveScenario("ubuntu-repo-cloud-openclaw-custom-policies", meta); + expect(custom.dimensions.onboarding.id).toBe("cloud-openclaw-custom-policies"); + expect(custom.expected_state.id).toBe("cloud-openclaw-custom-policies-ready"); + expect(custom.suites.map((s) => s.id)).toContain("onboarding-state"); + + const invalidKey = resolveScenario("ubuntu-invalid-nvidia-key-negative", meta); + expect(invalidKey.expected_state.config.failure).toMatchObject({ + expected: true, + stage: "onboarding", + reason: "invalid-nvidia-api-key", + exit_code: 1, + no_stack_trace: true, + }); + + const portConflict = resolveScenario("ubuntu-gateway-port-conflict-negative", meta); + expect(portConflict.expected_state.config.failure).toMatchObject({ + expected: true, + stage: "onboarding", + reason: "gateway-port-conflict", + exit_code: 1, + no_stack_trace: true, + }); + }); + it("should_fail_for_unknown_scenario", () => { const meta = realMetadata(); expect(() => resolveScenario("does-not-exist", meta)).toThrow(/does-not-exist/); diff --git a/test/e2e/validation_suites/onboarding/state/00-registry-provider-model-policies.sh b/test/e2e/validation_suites/onboarding/state/00-registry-provider-model-policies.sh new file mode 100755 index 0000000000..2d900ed5b8 --- /dev/null +++ b/test/e2e/validation_suites/onboarding/state/00-registry-provider-model-policies.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../../../runtime/lib/context.sh +. "${SCRIPT_DIR}/../../../runtime/lib/context.sh" +# shellcheck source=../../../runtime/lib/onboard-state.sh +. "${SCRIPT_DIR}/../../../runtime/lib/onboard-state.sh" + +CTX_FILE="$(e2e_context_path)" +# shellcheck disable=SC1090 +. "${CTX_FILE}" + +registry_file="${HOME}/.nemoclaw/sandboxes.json" +model="${E2E_ONBOARDING_MODEL:-nvidia/nemotron-3-super-120b-a12b}" +provider="${E2E_ONBOARDING_REGISTRY_PROVIDER:-nvidia-prod}" +presets="${E2E_ONBOARDING_POLICY_PRESETS:-npm,pypi}" + +e2e_onboard_state_assert_registry "${registry_file}" "${E2E_SANDBOX_NAME}" "${provider}" "${model}" "${presets}" +echo "PASS: onboarding-state.registry-provider-model-policies" diff --git a/test/e2e/validation_suites/onboarding/state/01-session-provider-model-policies.sh b/test/e2e/validation_suites/onboarding/state/01-session-provider-model-policies.sh new file mode 100755 index 0000000000..f604d3a760 --- /dev/null +++ b/test/e2e/validation_suites/onboarding/state/01-session-provider-model-policies.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../../../runtime/lib/context.sh +. "${SCRIPT_DIR}/../../../runtime/lib/context.sh" +# shellcheck source=../../../runtime/lib/onboard-state.sh +. "${SCRIPT_DIR}/../../../runtime/lib/onboard-state.sh" + +CTX_FILE="$(e2e_context_path)" +# shellcheck disable=SC1090 +. "${CTX_FILE}" + +session_file="${HOME}/.nemoclaw/onboard-session.json" +model="${E2E_ONBOARDING_MODEL:-nvidia/nemotron-3-super-120b-a12b}" +provider="${E2E_ONBOARDING_REGISTRY_PROVIDER:-nvidia-prod}" +presets="${E2E_ONBOARDING_POLICY_PRESETS:-npm,pypi}" + +e2e_onboard_state_assert_session "${session_file}" "${E2E_SANDBOX_NAME}" "${provider}" "${model}" "${presets}" +echo "PASS: onboarding-state.session-provider-model-policies" diff --git a/test/e2e/validation_suites/suites.yaml b/test/e2e/validation_suites/suites.yaml index 2bebbdc374..6ec38c4a3f 100644 --- a/test/e2e/validation_suites/suites.yaml +++ b/test/e2e/validation_suites/suites.yaml @@ -30,6 +30,15 @@ suites: steps: &id008 - id: credentials-present script: security/credentials/00-credentials-present.sh + onboarding-state: + requires_state: + sandbox.status: running + onboarding_state.provider: nvidia-prod + steps: + - id: registry-provider-model-policies + script: onboarding/state/00-registry-provider-model-policies.sh + - id: session-provider-model-policies + script: onboarding/state/01-session-provider-model-policies.sh local-ollama-inference: requires_state: gateway.health: healthy From 5f7802d35eab818383e245cf3b9da1fc69ebf716 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Tue, 19 May 2026 15:17:18 -0400 Subject: [PATCH 4/7] ci(e2e): install scenario runner deps in nightly --- .github/workflows/nightly-e2e.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/nightly-e2e.yaml b/.github/workflows/nightly-e2e.yaml index 930de22756..d8b6e2841a 100644 --- a/.github/workflows/nightly-e2e.yaml +++ b/.github/workflows/nightly-e2e.yaml @@ -1787,6 +1787,7 @@ jobs: export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]] && export PATH="$HOME/.local/bin:$PATH" + npm ci --ignore-scripts for scenario in \ ubuntu-repo-cloud-openclaw-custom-policies \ ubuntu-invalid-nvidia-key-negative \ From 127aae3857303504551669c0873d780b1e01a696 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Tue, 19 May 2026 16:02:33 -0400 Subject: [PATCH 5/7] style(e2e): apply pre-push formatting --- test/e2e/runtime/run-scenario.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/test/e2e/runtime/run-scenario.sh b/test/e2e/runtime/run-scenario.sh index ba8bcb0571..27cc5b85c6 100755 --- a/test/e2e/runtime/run-scenario.sh +++ b/test/e2e/runtime/run-scenario.sh @@ -239,7 +239,6 @@ if [[ "${EXPECTED_STATE_ID}" == "preflight-failure-no-sandbox" ]]; then exit 0 fi - if [[ "${FAILURE_STAGE}" == "onboarding" ]]; then negative_log="${E2E_CONTEXT_DIR}/negative-onboarding.log" sandbox_name="$(e2e_context_get E2E_SANDBOX_NAME)" From d89919f312129b9e56c6109f2c4d7c651a22e808 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Tue, 19 May 2026 16:08:19 -0400 Subject: [PATCH 6/7] fix(e2e): address negative path review findings --- .github/workflows/nightly-e2e.yaml | 114 +++++++++++++++------------- test/e2e/runtime/lib/port-holder.sh | 8 ++ 2 files changed, 69 insertions(+), 53 deletions(-) diff --git a/.github/workflows/nightly-e2e.yaml b/.github/workflows/nightly-e2e.yaml index d8b6e2841a..4a3ef52826 100644 --- a/.github/workflows/nightly-e2e.yaml +++ b/.github/workflows/nightly-e2e.yaml @@ -155,7 +155,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: install-log path: /tmp/nemoclaw-e2e-install.log @@ -194,7 +194,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: install-log-cloud-onboard path: /tmp/nemoclaw-e2e-cloud-onboard-install.log @@ -228,7 +228,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: install-log-cloud-inference path: /tmp/nemoclaw-e2e-cloud-inference-install.log @@ -262,7 +262,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: install-log-skill-agent path: /tmp/nemoclaw-e2e-skill-agent-install.log @@ -338,7 +338,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: install-log-messaging-providers path: | @@ -377,7 +377,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: install-log-openclaw-slack-pairing path: /tmp/nemoclaw-e2e-openclaw-slack-pairing-install.log @@ -413,7 +413,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: install-log-messaging-compatible-endpoint path: /tmp/nemoclaw-e2e-messaging-compatible-endpoint-install.log @@ -464,7 +464,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: install-log-channels-stop-start path: | @@ -510,7 +510,7 @@ jobs: - name: Upload onboard log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: install-log-brave-search # The script scrubs $BRAVE_API_KEY from this log in place @@ -546,7 +546,7 @@ jobs: - name: Upload onboard log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: install-log-kimi-inference-compat path: /tmp/nemoclaw-e2e-kimi-inference-compat-onboard.log @@ -554,7 +554,7 @@ jobs: - name: Upload build/setup log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: build-log-kimi-inference-compat path: /tmp/nemoclaw-e2e-kimi-inference-compat-build.log @@ -562,7 +562,7 @@ jobs: - name: Upload agent log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: agent-log-kimi-inference-compat path: /tmp/nemoclaw-e2e-kimi-inference-compat-agent.log @@ -671,7 +671,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: install-log-token-rotation path: /tmp/nemoclaw-e2e-install.log @@ -703,7 +703,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: sandbox-survival-install-log path: /tmp/nemoclaw-e2e-install.log @@ -738,7 +738,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: issue-2478-crash-loop-recovery-install-log path: /tmp/nemoclaw-e2e-install.log @@ -775,7 +775,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: hermes-e2e-install-log path: /tmp/nemoclaw-e2e-hermes-install.log @@ -812,7 +812,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: hermes-inference-switch-install-log path: /tmp/nemoclaw-e2e-hermes-inference-switch-install.log @@ -854,7 +854,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: hermes-discord-e2e-install-log path: /tmp/nemoclaw-e2e-hermes-discord-install.log @@ -894,7 +894,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: hermes-slack-e2e-install-log path: /tmp/nemoclaw-e2e-hermes-slack-install.log @@ -1134,7 +1134,7 @@ jobs: - name: Upload sandbox gateway logs on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: sandbox-operations-docker-logs path: docker-logs/ @@ -1142,7 +1142,7 @@ jobs: - name: Upload test log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: sandbox-operations-test-log path: test-sandbox-operations-*.log @@ -1176,7 +1176,7 @@ jobs: - name: Upload test log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: inference-routing-test-log path: test-inference-routing-*.log @@ -1212,7 +1212,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: openclaw-inference-switch-install-log path: /tmp/nemoclaw-e2e-openclaw-inference-switch-install.log @@ -1246,7 +1246,7 @@ jobs: - name: Upload test log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: network-policy-test-log path: test-network-policy-*.log @@ -1277,7 +1277,7 @@ jobs: - name: Upload test log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: state-backup-restore-test-log path: test-state-backup-restore-*.log @@ -1308,7 +1308,7 @@ jobs: - name: Upload test log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: tunnel-lifecycle-test-log path: test-tunnel-lifecycle-*.log @@ -1342,7 +1342,7 @@ jobs: - name: Upload test log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: diagnostics-test-log path: test-diagnostics-*.log @@ -1380,7 +1380,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: install-log-credential-migration path: /tmp/nemoclaw-e2e-install.log @@ -1414,7 +1414,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: snapshot-commands-install-log path: /tmp/nemoclaw-e2e-install.log @@ -1448,7 +1448,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: shields-config-install-log path: /tmp/nemoclaw-e2e-shields-install.log @@ -1482,7 +1482,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: rebuild-openclaw-install-log path: /tmp/nemoclaw-e2e-install.log @@ -1517,7 +1517,7 @@ jobs: - name: Upload install logs on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: upgrade-stale-sandbox-logs path: | @@ -1557,7 +1557,7 @@ jobs: - name: Upload gateway upgrade logs on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: openshell-gateway-upgrade-logs path: | @@ -1598,7 +1598,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: rebuild-hermes-install-log path: /tmp/nemoclaw-e2e-install.log @@ -1634,7 +1634,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: rebuild-hermes-stale-base-install-log path: /tmp/nemoclaw-e2e-install.log @@ -1654,6 +1654,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.target_ref || github.ref }} + persist-credentials: false - name: Install NemoClaw env: NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} @@ -1673,7 +1674,7 @@ jobs: bash test/e2e/test-double-onboard.sh - name: Upload test log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: double-onboard-test-log path: test-double-onboard-*.log @@ -1693,6 +1694,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.target_ref || github.ref }} + persist-credentials: false - name: Install NemoClaw env: NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} @@ -1712,7 +1714,7 @@ jobs: bash test/e2e/test-onboard-repair.sh - name: Upload test log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: onboard-repair-test-log path: test-onboard-repair-*.log @@ -1732,6 +1734,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.target_ref || github.ref }} + persist-credentials: false - name: Install NemoClaw env: NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} @@ -1751,7 +1754,7 @@ jobs: bash test/e2e/test-onboard-resume.sh - name: Upload test log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: onboard-resume-test-log path: test-onboard-resume-*.log @@ -1771,6 +1774,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.target_ref || github.ref }} + persist-credentials: false - name: Install NemoClaw env: NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} @@ -1783,20 +1787,23 @@ jobs: NEMOCLAW_NON_INTERACTIVE: "1" NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" run: | + set -euo pipefail [ -f "$HOME/.bashrc" ] && source "$HOME/.bashrc" 2>/dev/null || true export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]] && export PATH="$HOME/.local/bin:$PATH" npm ci --ignore-scripts + log_file="/tmp/nemoclaw-e2e-onboard-negative-paths.log" + : >"${log_file}" for scenario in \ ubuntu-repo-cloud-openclaw-custom-policies \ ubuntu-invalid-nvidia-key-negative \ ubuntu-gateway-port-conflict-negative; do - bash test/e2e/runtime/run-scenario.sh "$scenario" + bash test/e2e/runtime/run-scenario.sh "$scenario" 2>&1 | tee -a "${log_file}" done - name: Upload test log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: onboard-negative-paths-test-log path: /tmp/nemoclaw-e2e-onboard-negative-paths.log @@ -1816,6 +1823,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.target_ref || github.ref }} + persist-credentials: false - name: Install NemoClaw env: NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} @@ -1835,7 +1843,7 @@ jobs: bash test/e2e/test-runtime-overrides.sh - name: Upload test log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: runtime-overrides-test-log path: test-runtime-overrides-*.log @@ -1878,7 +1886,7 @@ jobs: bash test/e2e/test-credential-sanitization.sh - name: Upload test log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: credential-sanitization-test-log path: test-credential-sanitization-*.log @@ -1921,7 +1929,7 @@ jobs: bash test/e2e/test-telegram-injection.sh - name: Upload test log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: telegram-injection-test-log path: test-telegram-injection-*.log @@ -1957,7 +1965,7 @@ jobs: - name: Upload onboard logs on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: overlayfs-autofix-logs path: | @@ -1998,7 +2006,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: device-auth-health-install-log path: /tmp/nemoclaw-e2e-health-install.log @@ -2037,7 +2045,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: launchable-smoke-install-log path: /tmp/nemoclaw-launchable-install.log @@ -2045,7 +2053,7 @@ jobs: - name: Upload onboard log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: launchable-smoke-onboard-log path: /tmp/nemoclaw-launchable-onboard.log @@ -2053,7 +2061,7 @@ jobs: - name: Upload test log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: launchable-smoke-test-log path: /tmp/nemoclaw-launchable-test.log @@ -2099,7 +2107,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: gpu-e2e-install-log path: /tmp/nemoclaw-gpu-e2e-install.log @@ -2107,7 +2115,7 @@ jobs: - name: Upload test log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: gpu-e2e-test-log path: /tmp/nemoclaw-gpu-e2e-test.log @@ -2154,7 +2162,7 @@ jobs: - name: Upload install log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: gpu-double-onboard-install-log path: /tmp/nemoclaw-gpu-double-onboard-install.log @@ -2162,7 +2170,7 @@ jobs: - name: Upload re-onboard log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: gpu-double-onboard-reonboard-log path: /tmp/nemoclaw-gpu-double-onboard-reonboard.log @@ -2170,7 +2178,7 @@ jobs: - name: Upload test log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: gpu-double-onboard-test-log path: /tmp/nemoclaw-gpu-double-onboard-test.log diff --git a/test/e2e/runtime/lib/port-holder.sh b/test/e2e/runtime/lib/port-holder.sh index 32209dc534..cbffcd275a 100755 --- a/test/e2e/runtime/lib/port-holder.sh +++ b/test/e2e/runtime/lib/port-holder.sh @@ -8,6 +8,9 @@ E2E_PORT_HOLDER_PID="${E2E_PORT_HOLDER_PID:-}" e2e_port_holder_start() { local port="$1" + if [[ -n "${E2E_PORT_HOLDER_PID}" ]]; then + e2e_port_holder_stop + fi E2E_PORT_HOLDER_PID="" node - "${port}" <<'NODE' >/tmp/nemoclaw-e2e-port-holder.log 2>&1 & const net = require("node:net"); @@ -34,6 +37,11 @@ NODE fi sleep 0.25 done + if [[ -n "${E2E_PORT_HOLDER_PID}" ]]; then + kill "${E2E_PORT_HOLDER_PID}" >/dev/null 2>&1 || true + wait "${E2E_PORT_HOLDER_PID}" >/dev/null 2>&1 || true + E2E_PORT_HOLDER_PID="" + fi return 1 } From 38f9506a0a34e3e1754b290d9b35fb2f48fdca8d Mon Sep 17 00:00:00 2001 From: Aaron Erickson Date: Tue, 19 May 2026 18:31:28 -0700 Subject: [PATCH 7/7] test(e2e): source negative-path helpers Signed-off-by: Aaron Erickson --- test/e2e/runtime/run-scenario.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/e2e/runtime/run-scenario.sh b/test/e2e/runtime/run-scenario.sh index 27cc5b85c6..e0258256c1 100755 --- a/test/e2e/runtime/run-scenario.sh +++ b/test/e2e/runtime/run-scenario.sh @@ -142,6 +142,10 @@ fi . "${SCRIPT_DIR}/lib/env.sh" # shellcheck source=lib/context.sh . "${SCRIPT_DIR}/lib/context.sh" +# shellcheck source=lib/negative.sh +. "${SCRIPT_DIR}/lib/negative.sh" +# shellcheck source=lib/port-holder.sh +. "${SCRIPT_DIR}/lib/port-holder.sh" # shellcheck source=../nemoclaw_scenarios/install/dispatch.sh . "${E2E_ROOT}/nemoclaw_scenarios/install/dispatch.sh" # shellcheck source=../nemoclaw_scenarios/onboard/dispatch.sh