From 970f403011856435c8a01691774c87d2fe2a3687 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Thu, 11 Jun 2026 09:49:08 -0400 Subject: [PATCH 1/3] test(e2e): add issue 4434 Vitest guard --- ...sue-4434-tui-unreachable-inference.test.ts | 315 ++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 test/e2e-scenario/live/issue-4434-tui-unreachable-inference.test.ts diff --git a/test/e2e-scenario/live/issue-4434-tui-unreachable-inference.test.ts b/test/e2e-scenario/live/issue-4434-tui-unreachable-inference.test.ts new file mode 100644 index 0000000000..9299ee7bcb --- /dev/null +++ b/test/e2e-scenario/live/issue-4434-tui-unreachable-inference.test.ts @@ -0,0 +1,315 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import fs from "node:fs"; +import path from "node:path"; + +import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts"; +import { trustedSandboxShellScript, validateSandboxName } from "../fixtures/clients/sandbox.ts"; +import { expect, test } from "../fixtures/e2e-test.ts"; +import { shouldRunLiveE2EScenarios } from "../fixtures/live-project-gate.ts"; +import { ubuntuRepoDocker } from "../scenarios/matrix.ts"; + +// Migrated from test/e2e/test-issue-4434-tui-unreachable-inference.sh. +// This remains a privileged opt-in live repro: it onboards a real cloud +// OpenClaw sandbox, installs temporary DOCKER-USER DROP rules for the NVIDIA +// endpoint IPs, drives `openclaw tui` through `openshell sandbox exec --tty`, +// and requires a visible inference error plus an error status instead of the +// broken spinner+connected signature from #4434. The legacy bash lane remains +// wired until Phase 11 shell retirement; this file adds the equivalent Vitest +// coverage without introducing shared framework or registry helpers. + +const REPO_ROOT = path.resolve(import.meta.dirname, "../../.."); +const DOCKERFILE_BASE = path.join(REPO_ROOT, "Dockerfile.base"); +const ENVIRONMENT = ubuntuRepoDocker("cloud-openclaw"); +const SANDBOX_NAME = process.env.NEMOCLAW_SANDBOX_NAME ?? "e2e-issue-4434-tui-unreachable"; +validateSandboxName(SANDBOX_NAME); + +const INTEGRATE_MODELS_URL = "https://integrate.api.nvidia.com/v1/models"; +const BLOCKED_IPS = ["75.2.113.119", "99.83.136.103"]; +const TUI_TIMEOUT_SEC = Number.parseInt( + process.env.NEMOCLAW_ISSUE_4434_TUI_TIMEOUT_SEC ?? "180", + 10, +); + +const VISIBLE_ERROR_RE = + /\b(error|failed|timeout|timed out|unavailable|fetch failed|ETIMEDOUT|ECONN|upstream|connection|refused|no route to host)\b/i; +const CONNECTED_SPINNER_RE = + /(?:flibbertigibbeting|thinking|waiting|processing).*?\|\s*connected|[0-9]+m\s+[0-9]+s\s*\|\s*connected/i; +const STATUS_LINE_RE = + /(connecting|gateway connected|connected|sending|running|flibbertigibbeting).*\|\s*(connected|error)/i; +const ERROR_STATUS_RE = /\|\s*error\b/i; + +const runIssue4434LiveTest = + shouldRunLiveE2EScenarios() && process.env.NEMOCLAW_ISSUE_4434_LIVE === "1" ? test : test.skip; + +type CommandResultText = { stdout: string; stderr: string }; + +function resultText(result: CommandResultText): string { + return [result.stdout, result.stderr].filter(Boolean).join("\n"); +} + +function shellSingleQuote(value: string): string { + return `'${value.replaceAll("'", "'\\''")}'`; +} + +function readBundledOpenClawVersion(): string { + const dockerfile = fs.readFileSync(DOCKERFILE_BASE, "utf8"); + const match = dockerfile.match(/^ARG OPENCLAW_VERSION=(\S+)\s*$/m); + if (!match?.[1]) { + throw new Error("could not parse OPENCLAW_VERSION from Dockerfile.base"); + } + return match[1]; +} + +function stripTerminalControl(value: string): string { + return value + .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "") + .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "") + .replace(/\r/g, "\n"); +} + +function analyzeIssue4434TuiCapture(capture: string) { + const plain = stripTerminalControl(capture); + const statusLines = plain + .split(/\n/) + .map((line) => line.trim()) + .filter((line) => STATUS_LINE_RE.test(line)); + const lastStatusLine = statusLines.at(-1) ?? ""; + return { + plain, + visibleError: VISIBLE_ERROR_RE.test(plain), + connectedSpinner: CONNECTED_SPINNER_RE.test(plain), + issue4434Signature: CONNECTED_SPINNER_RE.test(plain) && !VISIBLE_ERROR_RE.test(plain), + lastStatusLine, + finalStatusIsError: ERROR_STATUS_RE.test(lastStatusLine), + finalStatusIsConnectedSpinner: CONNECTED_SPINNER_RE.test(lastStatusLine), + }; +} + +function deleteFirewallRulesScript(ips: readonly string[]): string { + return [ + "set -euo pipefail", + ...ips.map( + (ip) => + `sudo iptables -D DOCKER-USER -d ${shellSingleQuote(ip)} -j DROP >/dev/null 2>&1 || true`, + ), + ].join("\n"); +} + +function buildExpectScript(): string { + return `set timeout $env(NEMOCLAW_ISSUE_4434_TUI_TIMEOUT) +set sandbox $env(NEMOCLAW_ISSUE_4434_SANDBOX) +set capture $env(NEMOCLAW_ISSUE_4434_CAPTURE) +log_file -a $capture +spawn openshell sandbox exec --name $sandbox --tty -- sh -lc {export TERM=xterm-256color; cd /sandbox; openclaw tui} +sleep 10 +send -- "hello\\r" +expect { + -nocase -re {(error|failed|timeout|timed out|unavailable|fetch failed|ETIMEDOUT|ECONN|upstream)} { + sleep 5 + send "\\003" + sleep 1 + send "\\003" + exit 0 + } + timeout { + send "\\003" + sleep 1 + send "\\003" + exit 20 + } + eof { exit 21 } +} +`; +} + +runIssue4434LiveTest( + "issue-4434: openclaw tui surfaces unreachable-inference errors and stops the connected spinner", + { timeout: 120 * 60_000 }, + async ({ artifacts, cleanup, environment, host, onboard, sandbox, secrets, skip }) => { + if (process.platform !== "linux") { + skip("Linux host required for DOCKER-USER iptables repro"); + } + + const apiKey = secrets.required("NVIDIA_API_KEY"); + expect(apiKey.startsWith("nvapi-"), "NVIDIA_API_KEY must start with nvapi-").toBe(true); + + await artifacts.writeJson("scenario.json", { + id: "issue-4434-tui-unreachable-inference", + runner: "vitest", + boundary: [ + "real cloud OpenClaw sandbox", + "host DOCKER-USER iptables DROP rules", + "openshell sandbox exec --tty", + "openclaw tui", + ], + migratedFrom: "test/e2e/test-issue-4434-tui-unreachable-inference.sh", + issue: "#4434", + }); + + const prereq = await host.command( + "bash", + [ + "-lc", + [ + "set -euo pipefail", + 'for command in docker sudo expect curl; do command -v "$command" >/dev/null; done', + "docker info >/dev/null", + "sudo -n true >/dev/null", + "sudo -n iptables --version >/dev/null", + ].join("\n"), + ], + { + artifactName: "issue4434-prerequisites", + env: buildAvailabilityProbeEnv(), + timeoutMs: 30_000, + }, + ); + expect(prereq.exitCode, resultText(prereq)).toBe(0); + + const ready = await environment.assertReady(ENVIRONMENT); + const instance = await onboard.from(ready, { + sandboxName: SANDBOX_NAME, + timeoutMs: 20 * 60_000, + }); + + const insertedIps: string[] = []; + cleanup.add("remove issue #4434 DOCKER-USER DROP rules", async () => { + if (insertedIps.length === 0) return; + const cleanupResult = await host.command( + "bash", + ["-lc", deleteFirewallRulesScript(insertedIps)], + { + artifactName: "cleanup-issue4434-firewall-rules", + env: buildAvailabilityProbeEnv(), + timeoutMs: 30_000, + }, + ); + if (cleanupResult.exitCode !== 0) { + throw new Error( + `failed to cleanup issue #4434 firewall rules\n${resultText(cleanupResult)}`, + ); + } + }); + + const expectedOpenClawVersion = readBundledOpenClawVersion(); + const version = await sandbox.exec(instance.sandboxName, ["openclaw", "--version"], { + artifactName: "issue4434-openclaw-version", + env: buildAvailabilityProbeEnv(), + timeoutMs: 30_000, + }); + expect(version.exitCode, resultText(version)).toBe(0); + expect( + version.stdout, + `expected sandbox OpenClaw ${expectedOpenClawVersion}; actual stdout: ${version.stdout}`, + ).toContain(expectedOpenClawVersion); + + const status = await host.nemoclaw([instance.sandboxName, "status"], { + artifactName: "issue4434-status-before-block", + env: buildAvailabilityProbeEnv(), + timeoutMs: 60_000, + }); + expect(status.exitCode, resultText(status)).toBe(0); + expect(resultText(status)).toMatch(/inference.*healthy|healthy.*inference/i); + + const connectProbe = await host.nemoclaw([instance.sandboxName, "connect", "--probe-only"], { + artifactName: "issue4434-connect-probe-before-block", + env: buildAvailabilityProbeEnv(), + timeoutMs: 60_000, + }); + expect(connectProbe.exitCode, resultText(connectProbe)).toBe(0); + + const preBlockEndpointProbe = await sandbox.execShell( + instance.sandboxName, + trustedSandboxShellScript( + `command -v curl >/dev/null && curl -sk --connect-timeout 5 --max-time 12 ${shellSingleQuote(INTEGRATE_MODELS_URL)} >/tmp/issue4434-models.before.out 2>&1`, + ), + { + artifactName: "issue4434-endpoint-probe-before-block", + env: buildAvailabilityProbeEnv(), + timeoutMs: 30_000, + }, + ); + expect(preBlockEndpointProbe.exitCode, resultText(preBlockEndpointProbe)).toBe(0); + + for (const ip of BLOCKED_IPS) { + const insert = await host.command( + "sudo", + ["iptables", "-I", "DOCKER-USER", "-d", ip, "-j", "DROP"], + { + artifactName: `issue4434-firewall-drop-${ip.replaceAll(".", "-")}`, + env: buildAvailabilityProbeEnv(), + timeoutMs: 30_000, + }, + ); + expect(insert.exitCode, resultText(insert)).toBe(0); + insertedIps.push(ip); + } + + const blockedEndpointProbe = await sandbox.execShell( + instance.sandboxName, + trustedSandboxShellScript( + `command -v curl >/dev/null && curl -sk --connect-timeout 5 --max-time 12 ${shellSingleQuote(INTEGRATE_MODELS_URL)} >/tmp/issue4434-models.blocked.out 2>&1`, + ), + { + artifactName: "issue4434-endpoint-probe-after-block", + env: buildAvailabilityProbeEnv(), + timeoutMs: 30_000, + }, + ); + expect( + blockedEndpointProbe.exitCode, + `integrate.api.nvidia.com remained reachable from inside the sandbox after firewall block\n${resultText(blockedEndpointProbe)}`, + ).not.toBe(0); + + const captureFile = artifacts.pathFor("openclaw-tui-capture.log"); + const expectLog = artifacts.pathFor("expect.log"); + const expectScript = artifacts.pathFor("issue4434-openclaw-tui.expect"); + fs.writeFileSync(expectScript, buildExpectScript(), { mode: 0o700 }); + + const tui = await host.command("expect", [expectScript], { + artifactName: "issue4434-openclaw-tui-expect", + env: { + ...buildAvailabilityProbeEnv(), + NEMOCLAW_ISSUE_4434_SANDBOX: instance.sandboxName, + NEMOCLAW_ISSUE_4434_CAPTURE: captureFile, + NEMOCLAW_ISSUE_4434_TUI_TIMEOUT: String(TUI_TIMEOUT_SEC), + }, + redactionValues: [apiKey], + timeoutMs: (TUI_TIMEOUT_SEC + 30) * 1000, + }); + fs.writeFileSync(expectLog, resultText(tui), "utf8"); + + const rawCapture = fs.existsSync(captureFile) ? fs.readFileSync(captureFile, "utf8") : ""; + const redactedRawCapture = secrets.redact(rawCapture, [apiKey]); + fs.writeFileSync(captureFile, redactedRawCapture, "utf8"); + const analysis = analyzeIssue4434TuiCapture(redactedRawCapture); + await artifacts.writeText("openclaw-tui-capture.plain.log", analysis.plain); + await artifacts.writeJson("scenario-result.json", { + id: "issue-4434-tui-unreachable-inference", + expectExitCode: tui.exitCode, + visibleError: analysis.visibleError, + connectedSpinner: analysis.connectedSpinner, + issue4434Signature: analysis.issue4434Signature, + lastStatusLine: analysis.lastStatusLine, + finalStatusIsError: analysis.finalStatusIsError, + finalStatusIsConnectedSpinner: analysis.finalStatusIsConnectedSpinner, + }); + + const failureContext = [ + `expect exit=${tui.exitCode}`, + `capture=${captureFile}`, + `lastStatusLine=${analysis.lastStatusLine}`, + "plain capture:", + analysis.plain, + ].join("\n"); + + expect(analysis.visibleError, failureContext).toBe(true); + expect(tui.exitCode, failureContext).toBe(0); + expect(analysis.issue4434Signature, failureContext).toBe(false); + expect(analysis.lastStatusLine, failureContext).not.toBe(""); + expect(analysis.finalStatusIsConnectedSpinner, failureContext).toBe(false); + expect(analysis.finalStatusIsError, failureContext).toBe(true); + }, +); From 21c5e2c1007a2299bf61c5305a9ffe95acfc154c Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Thu, 11 Jun 2026 11:24:31 -0400 Subject: [PATCH 2/3] ci(e2e): wire issue 4434 Vitest dispatch --- .github/workflows/e2e-vitest-scenarios.yaml | 130 +++++++++++++++++++- 1 file changed, 127 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e-vitest-scenarios.yaml b/.github/workflows/e2e-vitest-scenarios.yaml index 360cbb19fb..03aaf2fa0a 100644 --- a/.github/workflows/e2e-vitest-scenarios.yaml +++ b/.github/workflows/e2e-vitest-scenarios.yaml @@ -29,6 +29,7 @@ jobs: runs-on: ubuntu-latest outputs: matrix: ${{ steps.matrix.outputs.matrix }} + has_matrix: ${{ steps.matrix.outputs.has_matrix }} steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: @@ -55,10 +56,30 @@ jobs: echo "::error::Invalid scenario input: ${SCENARIOS}" >&2 exit 1 fi - args+=(--scenarios "${SCENARIOS}") + + registry_scenarios=() + IFS=',' read -ra requested_scenarios <<< "${SCENARIOS}" + for scenario in "${requested_scenarios[@]}"; do + case "${scenario}" in + issue-4434-tui-unreachable-inference) ;; + *) registry_scenarios+=("${scenario}") ;; + esac + done + + if [ "${#registry_scenarios[@]}" -gt 0 ]; then + registry_csv="$(IFS=,; echo "${registry_scenarios[*]}")" + args+=(--scenarios "${registry_csv}") + else + echo "matrix=[]" >> "$GITHUB_OUTPUT" + echo "has_matrix=false" >> "$GITHUB_OUTPUT" + matrix="[]" + fi + fi + if [ -z "${matrix:-}" ]; then + matrix="$(npx tsx test/e2e-scenario/scenarios/run.ts "${args[@]}")" + echo "matrix=${matrix}" >> "$GITHUB_OUTPUT" + echo "has_matrix=true" >> "$GITHUB_OUTPUT" fi - matrix="$(npx tsx test/e2e-scenario/scenarios/run.ts "${args[@]}")" - echo "matrix=${matrix}" >> "$GITHUB_OUTPUT" MATRIX_JSON="${matrix}" python - <<'PY' >> "$GITHUB_STEP_SUMMARY" import json import os @@ -74,6 +95,7 @@ jobs: live-scenarios: needs: generate-matrix + if: ${{ needs.generate-matrix.outputs.has_matrix == 'true' }} runs-on: ${{ matrix.runner }} timeout-minutes: 45 strategy: @@ -328,6 +350,107 @@ jobs: if-no-files-found: ignore retention-days: 14 + # Privileged live repro for #4434's unreachable NVIDIA endpoint TUI failure. + # Keeps the replacement Vitest coverage runnable before the retained legacy + # shell lane is retired in #5098 Phase 11. + issue-4434-tui-unreachable-inference-vitest: + if: >- + inputs.scenarios == '' || + contains(format(',{0},', inputs.scenarios), ',issue-4434-tui-unreachable-inference,') + runs-on: ubuntu-latest + timeout-minutes: 120 + env: + E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/vitest/issue-4434-tui-unreachable-inference + NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js + NEMOCLAW_RUN_E2E_SCENARIOS: "1" + NEMOCLAW_ISSUE_4434_LIVE: "1" + NEMOCLAW_NON_INTERACTIVE: "1" + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" + OPENSHELL_GATEWAY: "nemoclaw" + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Authenticate to Docker Hub + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + shell: bash + run: | + set -euo pipefail + if [[ -z "${DOCKERHUB_USERNAME}" || -z "${DOCKERHUB_TOKEN}" ]]; then + echo "::notice::Docker Hub credentials not configured; continuing with anonymous pulls." + exit 0 + fi + login_succeeded=0 + for attempt in 1 2 3; do + if echo "${DOCKERHUB_TOKEN}" | timeout 30s docker login docker.io --username "${DOCKERHUB_USERNAME}" --password-stdin; then + login_succeeded=1 + break + fi + if [[ "$attempt" -lt 3 ]]; then + echo "::warning::Docker Hub login attempt ${attempt} failed; retrying." + sleep 5 + fi + done + if [[ "$login_succeeded" -ne 1 ]]; then + echo "::warning::Docker Hub login failed after 3 attempts; continuing with anonymous pulls." + fi + + - name: Set up Node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.0.0 + with: + node-version: 22 + cache: npm + + - name: Install root dependencies + run: npm ci --ignore-scripts + + - name: Install issue #4434 host dependencies + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y expect iptables + + - name: Build CLI + run: npm run build:cli + + - name: Install OpenShell CLI + run: bash scripts/install-openshell.sh + + - name: Run issue #4434 TUI unreachable inference live test + env: + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} + run: | + set -euo pipefail + export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" + if command -v openshell >/dev/null 2>&1; then + OPENSHELL_BIN="$(command -v openshell)" + elif [ -x "$HOME/.local/bin/openshell" ]; then + OPENSHELL_BIN="$HOME/.local/bin/openshell" + else + echo "::error::OpenShell CLI not found after install" + ls -la /usr/local/bin/openshell "$HOME/.local/bin/openshell" 2>&1 || true + exit 1 + fi + export OPENSHELL_BIN + echo "Using OPENSHELL_BIN=$OPENSHELL_BIN" + "$OPENSHELL_BIN" --version + npx vitest run --project e2e-scenarios-live \ + test/e2e-scenario/live/issue-4434-tui-unreachable-inference.test.ts \ + --silent=false --reporter=default + + - name: Upload issue #4434 TUI unreachable inference artifacts + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: e2e-vitest-scenarios-issue-4434-tui-unreachable-inference + path: e2e-artifacts/vitest/issue-4434-tui-unreachable-inference/ + include-hidden-files: false + if-no-files-found: ignore + retention-days: 14 + # ── Free-standing recovery scenarios (#2701) ───────────────────────── # Recovery / disruption scenarios don't fit the steady-state expected-state # registry that drives `live-scenarios` above. They run as free-standing @@ -435,6 +558,7 @@ jobs: openshell-version-pin-vitest, onboard-negative-paths-vitest, openclaw-tui-chat-correlation-vitest, + issue-4434-tui-unreachable-inference-vitest, gateway-guard-recovery, ] if: ${{ always() && github.event_name == 'workflow_dispatch' }} From acd51cc20ced03c2fc0ec316b6986ddbdb367e55 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Thu, 11 Jun 2026 12:35:10 -0400 Subject: [PATCH 3/3] ci(e2e): align issue-4434-tui-unreachable-inference-vitest dispatch --- .github/workflows/e2e-vitest-scenarios.yaml | 110 +++++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e-vitest-scenarios.yaml b/.github/workflows/e2e-vitest-scenarios.yaml index 65e06c7eda..14abf26afb 100644 --- a/.github/workflows/e2e-vitest-scenarios.yaml +++ b/.github/workflows/e2e-vitest-scenarios.yaml @@ -40,7 +40,7 @@ jobs: SCENARIOS: ${{ inputs.scenarios }} run: | set -euo pipefail - allowed_jobs="openshell-version-pin-vitest,onboard-negative-paths-vitest,openclaw-tui-chat-correlation-vitest,gateway-guard-recovery" + allowed_jobs="openshell-version-pin-vitest,onboard-negative-paths-vitest,openclaw-tui-chat-correlation-vitest,gateway-guard-recovery,issue-4434-tui-unreachable-inference-vitest" if [ -n "${JOBS}" ] && [ -n "${SCENARIOS}" ]; then echo "::error::Use either scenarios or jobs, not both." >&2 exit 1 @@ -298,6 +298,113 @@ jobs: if-no-files-found: ignore retention-days: 14 + issue-4434-tui-unreachable-inference-vitest: + needs: validate-jobs + if: ${{ (inputs.jobs == '' && inputs.scenarios == '') || contains(format(',{0},', inputs.jobs), ',issue-4434-tui-unreachable-inference-vitest,') }} + runs-on: ubuntu-latest + timeout-minutes: 120 + env: + E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts/vitest/issue-4434-tui-unreachable-inference + NEMOCLAW_CLI_BIN: ${{ github.workspace }}/bin/nemoclaw.js + NEMOCLAW_RUN_E2E_SCENARIOS: "1" + NEMOCLAW_ISSUE_4434_LIVE: "1" + NEMOCLAW_NON_INTERACTIVE: "1" + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" + OPENSHELL_GATEWAY: "nemoclaw" + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Authenticate to Docker Hub + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + shell: bash + run: | + set -euo pipefail + if [[ -z "${DOCKERHUB_USERNAME}" || -z "${DOCKERHUB_TOKEN}" ]]; then + echo "::notice::Docker Hub credentials not configured; continuing with anonymous pulls." + exit 0 + fi + login_succeeded=0 + for attempt in 1 2 3; do + if echo "${DOCKERHUB_TOKEN}" | timeout 30s docker login docker.io --username "${DOCKERHUB_USERNAME}" --password-stdin; then + login_succeeded=1 + break + fi + if [[ "$attempt" -lt 3 ]]; then + echo "::warning::Docker Hub login attempt ${attempt} failed; retrying." + sleep 5 + fi + done + if [[ "$login_succeeded" -ne 1 ]]; then + echo "::warning::Docker Hub login failed after 3 attempts; continuing with anonymous pulls." + fi + + - name: Set up Node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.0.0 + with: + node-version: 22 + cache: npm + + - name: Install root dependencies + run: npm ci --ignore-scripts + + - name: Install issue #4434 host dependencies + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y expect iptables + + - name: Build CLI + run: npm run build:cli + + - name: Install OpenShell CLI + run: bash scripts/install-openshell.sh + + - name: Run issue #4434 TUI unreachable inference live test + env: + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} + run: | + set -euo pipefail + export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH" + if command -v openshell >/dev/null 2>&1; then + OPENSHELL_BIN="$(command -v openshell)" + elif [ -x "$HOME/.local/bin/openshell" ]; then + OPENSHELL_BIN="$HOME/.local/bin/openshell" + else + echo "::error::OpenShell CLI not found after install" + ls -la /usr/local/bin/openshell "$HOME/.local/bin/openshell" 2>&1 || true + exit 1 + fi + export OPENSHELL_BIN + echo "Using OPENSHELL_BIN=$OPENSHELL_BIN" + "$OPENSHELL_BIN" --version + npx vitest run --project e2e-scenarios-live \ + test/e2e-scenario/live/issue-4434-tui-unreachable-inference.test.ts \ + --silent=false --reporter=default + + - name: Upload issue #4434 TUI unreachable inference artifacts + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: e2e-vitest-scenarios-issue-4434-tui-unreachable-inference + path: e2e-artifacts/vitest/issue-4434-tui-unreachable-inference/ + include-hidden-files: false + if-no-files-found: ignore + retention-days: 14 + + # ── Free-standing recovery scenarios (#2701) ───────────────────────── + # Recovery / disruption scenarios don't fit the steady-state expected-state + # registry that drives `live-scenarios` above. They run as free-standing + # Vitest test files using the same `e2e-scenarios-live` project, framework + # fixtures, and live-project gate — just outside the matrix. + # + # First failing-test-first guard for #2701 (gateway recovery does not + # restore the /tmp guard chain after pod recreate). Will fail on `main` + # until the #2701 fix lands; flips green afterwards. + # Focused coverage slice for the #2603/#3145 OpenClaw websocket # protocol/history contract. The retained legacy bash lane remains the # source for full closeout until a later PR proves replacement and deletes it. @@ -481,6 +588,7 @@ jobs: live-scenarios, openshell-version-pin-vitest, onboard-negative-paths-vitest, + issue-4434-tui-unreachable-inference-vitest, openclaw-tui-chat-correlation-vitest, gateway-guard-recovery, ]