diff --git a/.github/workflows/regression-e2e.yaml b/.github/workflows/regression-e2e.yaml index ec7108b59bc..400ded2bae2 100644 --- a/.github/workflows/regression-e2e.yaml +++ b/.github/workflows/regression-e2e.yaml @@ -21,7 +21,7 @@ on: jobs: description: >- Comma-separated regression job names to run (empty = all). - Valid: dashboard-remote-bind-e2e,gateway-health-honest-e2e,docker-unreachable-gateway-start-e2e,gateway-drift-preflight-e2e,openshell-version-pin-e2e,onboard-inference-smoke-e2e,strict-tool-call-probe-e2e,model-router-provider-routed-inference-e2e,openclaw-plugin-runtime-exdev-e2e,whatsapp-qr-compact-e2e + Valid: dashboard-remote-bind-e2e,gateway-health-honest-e2e,gateway-drift-preflight-e2e,openshell-version-pin-e2e,onboard-inference-smoke-e2e,strict-tool-call-probe-e2e,model-router-provider-routed-inference-e2e,openclaw-plugin-runtime-exdev-e2e,whatsapp-qr-compact-e2e required: false type: string default: "" @@ -46,7 +46,6 @@ jobs: outputs: dashboard: ${{ steps.select.outputs.dashboard }} gateway: ${{ steps.select.outputs.gateway }} - docker_unreachable_gateway_start: ${{ steps.select.outputs.docker_unreachable_gateway_start }} gateway_drift_preflight: ${{ steps.select.outputs.gateway_drift_preflight }} openshell_version_pin: ${{ steps.select.outputs.openshell_version_pin }} onboard_inference_smoke: ${{ steps.select.outputs.onboard_inference_smoke }} @@ -81,12 +80,6 @@ jobs: echo "gateway=false" >> "$GITHUB_OUTPUT" fi - if [ -z "$normalized" ] || includes_job "docker-unreachable-gateway-start-e2e"; then - echo "docker_unreachable_gateway_start=true" >> "$GITHUB_OUTPUT" - else - echo "docker_unreachable_gateway_start=false" >> "$GITHUB_OUTPUT" - fi - if [ -z "$normalized" ] || includes_job "gateway-drift-preflight-e2e"; then echo "gateway_drift_preflight=true" >> "$GITHUB_OUTPUT" else @@ -194,44 +187,6 @@ jobs: /tmp/nemoclaw-e2e-gateway-health-honest-process.log if-no-files-found: ignore - # ── Docker-unreachable gateway-start E2E ─────────────────── - # Coverage guard for #2347's fallback path. Preflight catches Docker - # daemon outages before step [2/8] on the normal path; this focused - # regression simulates Docker dying during `openshell gateway start` and - # verifies onboard exits before health polling with Docker recovery advice. - docker-unreachable-gateway-start-e2e: - needs: select_regression_jobs - if: >- - github.repository == 'NVIDIA/NemoClaw' && - needs.select_regression_jobs.outputs.docker_unreachable_gateway_start == 'true' - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - - - name: Setup Node - uses: actions/setup-node@v6 - with: - node-version: "22" - - - name: Run Docker-unreachable gateway-start E2E test - env: - NEMOCLAW_NON_INTERACTIVE: "1" - NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" - run: bash test/e2e/test-docker-unreachable-gateway-start.sh - - - name: Upload Docker-unreachable gateway-start logs on failure - if: failure() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: docker-unreachable-gateway-start-logs - path: | - /tmp/nemoclaw-e2e-docker-unreachable-gateway-start.log - /tmp/nemoclaw-e2e-docker-unreachable-gateway-start-node.log - /tmp/nemoclaw-e2e-docker-unreachable-gateway-start-openshell.log - if-no-files-found: ignore - # ── OpenShell version-pin E2E ────────────────────────────── # Coverage guard for #3474. If a host has sticky OpenShell 0.0.45 on PATH # but this NemoClaw release supports only <=0.0.44, install-openshell.sh diff --git a/ci/test-file-size-budget.json b/ci/test-file-size-budget.json index d087e32d37c..d8d95f88c5c 100644 --- a/ci/test-file-size-budget.json +++ b/ci/test-file-size-budget.json @@ -11,7 +11,7 @@ "test/nemoclaw-start.test.ts": 5289, "test/onboard-messaging.test.ts": 2097, "test/onboard-selection.test.ts": 6922, - "test/onboard.test.ts": 4874, + "test/onboard.test.ts": 4783, "test/policies.test.ts": 2763 } } diff --git a/src/lib/onboard/gateway-start-failure-integration.test.ts b/src/lib/onboard/gateway-start-failure-integration.test.ts index 60410ebee50..3201807cfff 100644 --- a/src/lib/onboard/gateway-start-failure-integration.test.ts +++ b/src/lib/onboard/gateway-start-failure-integration.test.ts @@ -5,31 +5,24 @@ // startGatewayWithOptions() takes when `openshell gateway start` reports // the Docker daemon is not reachable. See src/lib/onboard.ts:2233. // -// This file replaces test/e2e/test-docker-unreachable-gateway-start.sh, -// which was structurally a Node-process unit test of startGateway() with a -// PATH-shimmed openshell binary, not a sandbox-lifecycle e2e. +// This helper-level suite preserves coverage from the former +// test/e2e/test-docker-unreachable-gateway-start.sh, which was structurally +// a Node-process unit test of startGateway() with a PATH-shimmed openshell +// binary, not a sandbox-lifecycle e2e. // // Original regression: NemoClaw #2347. // Owning migration issue: NemoClaw #4355. // -// Coverage strategy: prove the contract through two layers: +// Coverage strategy: prove the helper-level contract through two layers: // // 1. Unit tests of the already-exported helpers (printDockerDaemonRecovery, // handleFinalGatewayStartFailure with dockerUnreachable=true). // 2. A composition test that runs the same helper sequence the call site // uses (classify → handleFinal → exitProcess(1)). // -// What this file deliberately DOES NOT cover (gap documented as it.todo -// at the bottom of the suite, with a follow-up issue tracking the -// refactor): direct executable proof that startGatewayWithOptions, on a -// docker-unreachable streamGatewayStart() result, (a) throws -// pRetry.AbortError instead of retrying, (b) never logs "Waiting for -// gateway health...", and (c) never calls openshell `status` or -// `gateway info` probes. Closing that gap requires either mocking the -// ~10 module-internal closures `startGatewayWithOptions` touches before -// reaching streamGatewayStart, or extracting the inner pRetry async body -// into an exported helper that takes streamGatewayStart as a DI -// parameter. Both changes are out of scope for the retirement PR. +// The caller-level process regression that drives startGateway() through a +// PATH-shimmed openshell binary lives in +// test/onboard-gateway-docker-unreachable.test.ts. import { describe, expect, it, vi } from "vitest"; // `handleFinalGatewayStartFailure` is exposed via `module.exports = {...}` at @@ -310,45 +303,6 @@ describe("startGatewayWithOptions docker-unreachable abort (#2347)", () => { }); }); - // ── Documented coverage gap: caller-level contracts that need a - // behavior-level seam (out of scope for this retirement PR) ───────── - // - // The legacy bash script directly executed `startGateway()` with a - // PATH-shimmed openshell binary and asserted runtime behavior of the - // call site. The unit + composition tests above cover the same ground - // for everything reachable through already-exported helpers, but three - // call-site contracts cannot be proven without driving the actual - // `startGatewayWithOptions` orchestrator past its ~200 lines of - // gateway-reuse / ssh-keygen / known_hosts / docker-driver-detect - // preamble. Doing that in a focused way requires either: - // - // (a) ~10 vi.mock() calls on module-internal closures inside - // onboard.ts (brittle), or - // (b) a small refactor extracting the inner pRetry async body of - // startGatewayWithOptions into an exported helper that takes - // streamGatewayStart as a DI parameter. - // - // Both are out of scope for this retirement PR. Follow-up issue - // [#5113](https://github.com/NVIDIA/NemoClaw/issues/5113) tracks - // landing option (b) and converting these `it.todo` placeholders into - // real assertions. Until then, the - // primary safety net for these contracts is code review on - // src/lib/onboard.ts:startGatewayWithOptions plus the existing - // classifyGatewayStartFailure / printDockerDaemonRecovery / - // handleFinalGatewayStartFailure unit tests above. - - describe("call-site contracts (caller-level coverage gap)", () => { - it.todo( - "startGateway aborts via pRetry.AbortError without entering health-poll loop on docker-unreachable streamGatewayStart output", - ); - it.todo( - "startGateway never invokes openshell `status` or `gateway info` after docker-unreachable streamGatewayStart output", - ); - it.todo( - "startGateway forwards dockerUnreachable=true to handleFinalGatewayStartFailure (no doctor logs collection, no destroyGateway cleanup) when streamGatewayStart returns docker-unreachable signature", - ); - }); - // ── Sanity: classifyGatewayStartFailure recognises both signatures ───── // (Already covered in gateway-start-failure.test.ts; this is a pinning // assertion for the two strings the legacy script generated, kept here so diff --git a/test/e2e-scenario/migration/legacy-inventory.json b/test/e2e-scenario/migration/legacy-inventory.json index 33968004853..37c7ec55e30 100644 --- a/test/e2e-scenario/migration/legacy-inventory.json +++ b/test/e2e-scenario/migration/legacy-inventory.json @@ -266,17 +266,6 @@ "deletionReady": false, "notes": "Initial completeness row; classify detailed coverage and deletion evidence in the owning migration issue before deleting." }, - { - "legacyScript": "test/e2e/test-docker-unreachable-gateway-start.sh", - "domain": "sandbox-lifecycle", - "ownerIssue": "#4355", - "status": "not-migrated", - "targetVitestScenarios": [], - "bridgeProbes": [], - "retiredReason": "", - "deletionReady": false, - "notes": "PR #5109 added Vitest helper coverage at src/lib/onboard/gateway-start-failure-integration.test.ts (two-layer strategy: unit tests of printDockerDaemonRecovery + handleFinalGatewayStartFailure, plus a composition test running the same classify -> handleFinal -> exitProcess(1) sequence the call site uses). Full retirement (delete this script + remove the docker-unreachable-gateway-start-e2e job in regression-e2e.yaml) was deferred per PR Review Advisor feedback because three caller-level contracts (no health-poll loop entry, no openshell status/gateway-info probes after gateway-start failure, dockerUnreachable forwarded to handleFinalGatewayStartFailure) are not directly proven without a behavior-level seam in startGatewayWithOptions. Follow-up issue #5113 tracks extracting attemptGatewayStart and converting the three it.todo placeholders into real assertions; deletion + retirement happen in a follow-up PR after that lands. Refs: #2347, #4355, #5113." - }, { "legacyScript": "test/e2e/test-docs-validation.sh", "domain": "support-diagnostics", diff --git a/test/e2e/test-docker-unreachable-gateway-start.sh b/test/e2e/test-docker-unreachable-gateway-start.sh deleted file mode 100755 index bcfee8d3256..00000000000 --- a/test/e2e/test-docker-unreachable-gateway-start.sh +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env bash -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Regression E2E for #2347's gateway-start fallback: if `openshell gateway -# start` reports that Docker/Colima is unreachable, onboard must abort before -# gateway health polling and print Docker recovery guidance. - -set -euo pipefail - -LOG_FILE="/tmp/nemoclaw-e2e-docker-unreachable-gateway-start.log" -START_LOG="/tmp/nemoclaw-e2e-docker-unreachable-gateway-start-node.log" -TRACE_LOG="/tmp/nemoclaw-e2e-docker-unreachable-gateway-start-openshell.log" -exec > >(tee "$LOG_FILE") 2>&1 - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' - -pass() { echo -e "${GREEN}[PASS]${NC} $1"; } -info() { echo -e "${YELLOW}[INFO]${NC} $1"; } -diag() { echo -e "${YELLOW}[DIAG]${NC} $1"; } -fail() { - echo -e "${RED}[FAIL]${NC} $1" >&2 - diag "node log tail:" - tail -120 "$START_LOG" 2>/dev/null || true - diag "fake openshell trace:" - cat "$TRACE_LOG" 2>/dev/null || true - exit 1 -} - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" -REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" -TMP_DIR="$(mktemp -d)" -FAKE_BIN="${TMP_DIR}/bin" -mkdir -p "$FAKE_BIN" -: >"$TRACE_LOG" - -cleanup() { - rm -rf "$TMP_DIR" -} -trap cleanup EXIT - -cd "$REPO_ROOT" - -info "Preparing CLI build" -if [ ! -d node_modules ]; then - npm ci --ignore-scripts -fi -npm run build:cli - -info "Installing fake openshell that reports Docker socket-not-found during gateway start" -cat >"${FAKE_BIN}/openshell" <<'SHIM' -#!/usr/bin/env bash -set -euo pipefail -TRACE_LOG="${NEMOCLAW_FAKE_OPENSHELL_TRACE:?}" -printf '%s\n' "$*" >>"$TRACE_LOG" - -if [[ "$*" == "--version" ]]; then - printf 'openshell 0.0.44\n' - exit 0 -fi - -if [[ "$*" == "gateway --help" ]]; then - printf 'Commands: start select info destroy remove\n' - exit 0 -fi - -if [[ "$*" == *"gateway"*"start"* ]]; then - printf '%s\n' "__GATEWAY_START__" >>"$TRACE_LOG" - printf 'Error: Failed to create Docker client.\n' - printf 'Socket not found: /var/run/docker.sock\n' - exit 1 -fi - -# These probes are allowed before gateway start when startGateway() checks -# reusable gateway state. After __GATEWAY_START__, they prove the regression: -# onboard fell through into health/status polling instead of aborting. -if [[ "$*" == "status" || "$*" == *"gateway"*"info"* ]]; then - printf 'HEALTH POLL REACHED\n' - exit 0 -fi - -if [[ "$*" == *"gateway"*"select"* || "$*" == *"gateway"*"destroy"* || "$*" == *"gateway"*"remove"* ]]; then - exit 0 -fi - -exit 0 -SHIM -chmod 755 "${FAKE_BIN}/openshell" - -info "Invoking legacy startGateway() path with macOS/x64 platform semantics" -set +e -PATH="${FAKE_BIN}:$PATH" \ - HOME="${TMP_DIR}/home" \ - NEMOCLAW_FAKE_OPENSHELL_TRACE="$TRACE_LOG" \ - NEMOCLAW_NON_INTERACTIVE="1" \ - NEMOCLAW_HEALTH_POLL_COUNT="5" \ - node <<'NODE' >"$START_LOG" 2>&1 -Object.defineProperty(process, "platform", { value: "darwin" }); -Object.defineProperty(process, "arch", { value: "x64" }); - -const { startGateway } = require("./dist/lib/onboard"); - -startGateway(null) - .then(() => { - console.log("__startGateway_returned_successfully__"); - process.exit(0); - }) - .catch((error) => { - console.error("__startGateway_threw__"); - console.error(error && error.stack ? error.stack : error); - process.exit(3); - }); -NODE -NODE_EXIT=$? -set -e - -info "node exit code: ${NODE_EXIT}" - -if [ "$NODE_EXIT" -ne 1 ]; then - fail "startGateway should exit 1 when Docker is unreachable, got ${NODE_EXIT}" -fi -pass "startGateway exited 1 on Docker-unreachable gateway-start output" - -if ! grep -q "Docker daemon is not running" "$START_LOG"; then - fail "Docker recovery guidance was not printed" -fi -pass "Docker recovery guidance was printed" - -if ! grep -q "colima start" "$START_LOG"; then - fail "macOS/Colima recovery hint was not printed" -fi -pass "macOS/Colima recovery hint was printed" - -if grep -q "Waiting for gateway health" "$START_LOG"; then - fail "onboard entered gateway health polling after Docker-unreachable gateway-start output" -fi -pass "gateway health polling message was not printed after Docker-unreachable output" - -if grep -q "HEALTH POLL REACHED" "$START_LOG"; then - fail "gateway status/info probe output appeared after Docker-unreachable gateway-start output" -fi -pass "gateway status/info probe output did not appear in user output" - -POST_START_PROBES="$(awk ' - /__GATEWAY_START__/ { seen = 1; next } - seen && ($0 == "status" || $0 ~ /gateway.*info/) { print } -' "$TRACE_LOG")" -if [ -n "$POST_START_PROBES" ]; then - fail "status/gateway-info probes ran after Docker-unreachable gateway start: ${POST_START_PROBES}" -fi -pass "no status/gateway-info probes ran after Docker-unreachable gateway start" - -if grep -q "openshell doctor logs" "$START_LOG"; then - fail "generic OpenShell diagnostics were printed instead of Docker-specific recovery guidance" -fi -pass "generic OpenShell diagnostics were skipped for Docker-unreachable gateway start" - -echo "" -pass "#2347 gateway-start fallback E2E green" diff --git a/test/onboard-gateway-docker-unreachable.test.ts b/test/onboard-gateway-docker-unreachable.test.ts new file mode 100644 index 00000000000..ec21fc33f40 --- /dev/null +++ b/test/onboard-gateway-docker-unreachable.test.ts @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { describe, it } from "vitest"; +import { testTimeoutOptions } from "./helpers/timeouts"; + +describe("startGateway Docker-unreachable fallback (#2347)", () => { + it("fast-fails before health polling or generic cleanup", testTimeoutOptions(20_000), () => { + const repoRoot = path.join(import.meta.dirname, ".."); + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-gateway-docker-down-")); + const fakeBin = path.join(tmpDir, "bin"); + const scriptPath = path.join(tmpDir, "gateway-docker-down.cjs"); + const tracePath = path.join(tmpDir, "openshell.trace"); + const onboardPath = JSON.stringify(path.join(repoRoot, "dist", "lib", "onboard.js")); + + fs.mkdirSync(fakeBin, { recursive: true }); + fs.writeFileSync(tracePath, ""); + fs.writeFileSync( + path.join(fakeBin, "openshell"), + `#!/usr/bin/env bash +set -euo pipefail +trace="\${NEMOCLAW_FAKE_OPENSHELL_TRACE:?}" +printf "%s\\n" "$*" >> "$trace" + +if [[ "$*" == "--version" ]]; then + printf "openshell 0.0.44\\n" + exit 0 +fi +if [[ "$*" == "gateway --help" ]]; then + printf "Commands: start select info destroy remove\\n" + exit 0 +fi +if [[ "$*" == *"gateway"*"start"* ]]; then + printf "__GATEWAY_START__\\n" >> "$trace" + printf "Error: Failed to create Docker client.\\n" + printf "Socket not found: /var/run/docker.sock\\n" + exit 1 +fi +if [[ "$*" == *"status"* || "$*" == *"gateway"*"info"* ]]; then + printf "HEALTH POLL REACHED\\n" + exit 0 +fi +if [[ "$*" == *"doctor"*"logs"* ]]; then + printf "DOCTOR LOGS REACHED\\n" + exit 0 +fi +if [[ "$*" == *"gateway"*"select"* || "$*" == *"gateway"*"destroy"* || "$*" == *"gateway"*"remove"* ]]; then + exit 0 +fi +exit 0 +`, + { mode: 0o755 }, + ); + + const script = ` +const mod = require("module"); +const origLoad = mod._load; +mod._load = function(req, parent, isMain) { + if (req === "p-retry") { + const pRetry = async (fn, opts) => { + try { + return await fn({ attemptNumber: 1, retriesLeft: 0 }); + } catch (e) { + if (!(e instanceof pRetry.AbortError) && opts && opts.onFailedAttempt) { + opts.onFailedAttempt(Object.assign(e, { attemptNumber: 1, retriesLeft: 0 })); + } + throw e; + } + }; + pRetry.AbortError = class AbortError extends Error {}; + return pRetry; + } + return origLoad.call(this, req, parent, isMain); +}; +Object.defineProperty(process, "platform", { value: "darwin" }); +Object.defineProperty(process, "arch", { value: "x64" }); +const { startGateway } = require(${onboardPath}); +startGateway(null).catch(() => {}); +`; + fs.writeFileSync(scriptPath, script); + + const result = spawnSync(process.execPath, [scriptPath], { + cwd: repoRoot, + encoding: "utf-8", + env: { + ...process.env, + HOME: tmpDir, + PATH: `${fakeBin}:${process.env.PATH || ""}`, + NEMOCLAW_FAKE_OPENSHELL_TRACE: tracePath, + NEMOCLAW_HEALTH_POLL_COUNT: "5", + NEMOCLAW_NON_INTERACTIVE: "1", + }, + }); + + assert.equal(result.status, 1, `unexpected exit code; stderr:\n${result.stderr}`); + assert.ok( + result.stderr.includes("Docker daemon is not running"), + `expected Docker recovery guidance in stderr:\n${result.stderr}`, + ); + assert.ok( + result.stderr.includes("colima start"), + `expected macOS Docker start hint in stderr:\n${result.stderr}`, + ); + assert.ok( + !result.stdout.includes("Waiting for gateway health"), + `health polling should not start after Docker-unreachable output:\n${result.stdout}`, + ); + assert.ok( + !result.stdout.includes("HEALTH POLL REACHED"), + `gateway status/info probes should not run after Docker-unreachable output:\n${result.stdout}`, + ); + assert.ok( + !result.stderr.includes("Cleaning up failed gateway state"), + `Docker-unreachable failure should skip generic cleanup:\n${result.stderr}`, + ); + assert.ok( + !result.stderr.includes("openshell doctor logs"), + `Docker-unreachable failure should skip generic diagnostics:\n${result.stderr}`, + ); + + const trace = fs.readFileSync(tracePath, "utf8"); + assert.ok(trace.includes("__GATEWAY_START__"), `gateway start marker missing:\n${trace}`); + const postGatewayStartCommands = trace + .split("__GATEWAY_START__\n") + .slice(1) + .join("__GATEWAY_START__\n") + .split("\n") + .map((line) => line.trim()) + .filter(Boolean); + const forbiddenPostStartCommands = postGatewayStartCommands.filter( + (line) => + line === "status" || + /\bgateway\b.*\binfo\b/.test(line) || + /\bdoctor\b.*\blogs\b/.test(line) || + /\bgateway\b.*\b(?:destroy|remove)\b/.test(line), + ); + assert.deepEqual( + forbiddenPostStartCommands, + [], + `forbidden openshell commands ran after Docker-unreachable gateway start:\n${trace}`, + ); + }); +}); diff --git a/test/onboard.test.ts b/test/onboard.test.ts index 73f4e9ecfad..5b6426fb475 100644 --- a/test/onboard.test.ts +++ b/test/onboard.test.ts @@ -353,97 +353,6 @@ startGateway(null).catch(() => {}); }, ); - it( - "fast-fails gateway start before health polling when Docker is unreachable (#2347)", - testTimeoutOptions(20_000), - () => { - const repoRoot = path.join(import.meta.dirname, ".."); - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-gateway-docker-down-")); - const fakeBin = path.join(tmpDir, "bin"); - const scriptPath = path.join(tmpDir, "gateway-docker-down.cjs"); - const onboardPath = JSON.stringify(path.join(repoRoot, "dist", "lib", "onboard.js")); - - fs.mkdirSync(fakeBin, { recursive: true }); - fs.writeFileSync( - path.join(fakeBin, "openshell"), - `#!/usr/bin/env bash -if [[ "$*" == "gateway --help" ]]; then - printf "Commands: start destroy\\n" - exit 0 -fi -if [[ "$*" == *"gateway"*"start"* ]]; then - printf "Error: Failed to create Docker client.\\n" - printf "Socket not found: /var/run/docker.sock\\n" - exit 1 -fi -if [[ "$*" == *"status"* || "$*" == *"gateway"*"info"* ]]; then - printf "HEALTH POLL REACHED\\n" - exit 0 -fi -exit 1 -`, - { mode: 0o755 }, - ); - - const script = ` -const mod = require("module"); -const origLoad = mod._load; -mod._load = function(req, parent, isMain) { - if (req === "p-retry") { - const pRetry = async (fn, opts) => { - try { - return await fn({ attemptNumber: 1, retriesLeft: 0 }); - } catch (e) { - if (!(e instanceof pRetry.AbortError) && opts && opts.onFailedAttempt) { - opts.onFailedAttempt(Object.assign(e, { attemptNumber: 1, retriesLeft: 0 })); - } - throw e; - } - }; - pRetry.AbortError = class AbortError extends Error {}; - return pRetry; - } - return origLoad.call(this, req, parent, isMain); -}; -Object.defineProperty(process, "platform", { value: "darwin" }); -Object.defineProperty(process, "arch", { value: "x64" }); -const { startGateway } = require(${onboardPath}); -startGateway(null).catch(() => {}); -`; - fs.writeFileSync(scriptPath, script); - - const result = spawnSync(process.execPath, [scriptPath], { - cwd: repoRoot, - encoding: "utf-8", - env: { - ...process.env, - HOME: tmpDir, - PATH: `${fakeBin}:${process.env.PATH || ""}`, - NEMOCLAW_HEALTH_POLL_COUNT: "5", - NEMOCLAW_NON_INTERACTIVE: "1", - }, - }); - - assert.equal(result.status, 1, `unexpected exit code; stderr:\n${result.stderr}`); - assert.ok( - result.stderr.includes("Docker daemon is not running"), - `expected Docker recovery guidance in stderr:\n${result.stderr}`, - ); - assert.ok( - result.stderr.includes("colima start"), - `expected macOS Docker start hint in stderr:\n${result.stderr}`, - ); - assert.ok( - !result.stdout.includes("Waiting for gateway health"), - `health polling should not start after Docker-unreachable output:\n${result.stdout}`, - ); - assert.ok( - !result.stdout.includes("HEALTH POLL REACHED"), - `gateway status/info probes should not run after Docker-unreachable output:\n${result.stdout}`, - ); - }, - ); - it("normalizes sandbox name hints from the environment", () => { const previous = process.env.NEMOCLAW_SANDBOX_NAME; process.env.NEMOCLAW_SANDBOX_NAME = " My-Assistant "; diff --git a/test/regression-e2e-workflow.test.ts b/test/regression-e2e-workflow.test.ts new file mode 100644 index 00000000000..8a932973d2c --- /dev/null +++ b/test/regression-e2e-workflow.test.ts @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; + +import { readYaml, type WorkflowStep } from "./helpers/e2e-workflow-contract"; + +type RegressionWorkflow = { + on?: { + workflow_dispatch?: { + inputs?: { + jobs?: { + description?: string; + }; + }; + }; + }; + jobs?: Record< + string, + { + steps?: WorkflowStep[]; + } + >; +}; + +describe("Regression E2E workflow contract", () => { + const workflow = readYaml(".github/workflows/regression-e2e.yaml"); + + it("does not advertise or select the retired docker-unreachable gateway-start lane", () => { + const jobsDescription = workflow.on?.workflow_dispatch?.inputs?.jobs?.description ?? ""; + const selectorScript = + workflow.jobs?.select_regression_jobs?.steps?.find((step) => step.id === "select")?.run ?? ""; + + expect(jobsDescription).not.toContain("docker-unreachable-gateway-start-e2e"); + expect(Object.keys(workflow.jobs ?? {})).not.toContain("docker-unreachable-gateway-start-e2e"); + expect(selectorScript).not.toContain("docker-unreachable-gateway-start-e2e"); + expect(selectorScript).not.toContain("docker_unreachable_gateway_start"); + }); +});