From a8b23d657a5ebe1fbd0608892fda8012a9eb8618 Mon Sep 17 00:00:00 2001 From: Chengjie Wang Date: Mon, 6 Jul 2026 13:48:15 +0800 Subject: [PATCH 01/37] test(openclaw): cover TUI idle timeout regression Signed-off-by: Chengjie Wang --- test/nemoclaw-start.test.ts | 8 ++++---- test/openclaw-tui-chat-correlation.test.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/nemoclaw-start.test.ts b/test/nemoclaw-start.test.ts index c95abc652e..0bcc6818e2 100644 --- a/test/nemoclaw-start.test.ts +++ b/test/nemoclaw-start.test.ts @@ -1629,7 +1629,7 @@ exit 2 }, 40_000); }); -describe("nemoclaw-start auto-pair slow-mode keepalive (#4263)", () => { +describe("nemoclaw-start auto-pair slow-mode keepalive (#4263, #6194)", () => { const src = fs.readFileSync(START_SCRIPT, "utf-8"); function buildAutoPairScript(): string { @@ -1639,7 +1639,7 @@ describe("nemoclaw-start auto-pair slow-mode keepalive (#4263)", () => { // Shared late-CLI poll timeline: // 1-2: first-time browser pairing request pending. // 3-6: browser paired, nothing pending (watcher converges to slow mode). - // 7-10: late CLI scope upgrade arrives. + // 7-10: late CLI/TUI scope upgrade arrives after the TUI has reached "connected idle". // 11+: cli paired alongside browser. function setupLateCliFixture(prefix: string): { tmpDir: string; @@ -1692,7 +1692,7 @@ exit 2 return { tmpDir, fakeOpenclaw, approveLog }; } - it("approves concurrent late CLI scope upgrades after browser pairing converges and drops back to fast cadence", () => { + it("approves concurrent late TUI CLI scope upgrades after browser pairing converges and drops back to fast cadence (#6194)", () => { const { tmpDir, fakeOpenclaw, approveLog } = setupLateCliFixture("nemoclaw-auto-pair-slow-"); try { const run = spawnSync("python3", ["-c", buildAutoPairScript()], { @@ -1716,7 +1716,7 @@ exit 2 expect(run.stdout).toContain( "[auto-pair] browser pairing converged; entering slow-mode approvals=1", ); - // Concurrent late wave — proxy for two sibling sandboxes' upgrades. + // Concurrent late wave — proxy for idle TUI interactions requesting CLI scopes. expect(run.stdout).toContain( "[auto-pair] approved request=late-cli client=openclaw-cli mode=cli", ); diff --git a/test/openclaw-tui-chat-correlation.test.ts b/test/openclaw-tui-chat-correlation.test.ts index e3e515eed0..fc2b957053 100644 --- a/test/openclaw-tui-chat-correlation.test.ts +++ b/test/openclaw-tui-chat-correlation.test.ts @@ -586,7 +586,7 @@ function runLiveIssue2603ReproWithEventCaptureRetry(sandboxName: string): LiveIs return { repro, attempts }; } -describe("OpenClaw TUI chat correlation regression (#2603)", () => { +describe("OpenClaw TUI chat correlation regression (#2603, #6194)", () => { it("classifies the observed gateway trace as broken (#2603)", () => { const analysis = analyzeIssue2603Trace(capturedIssue2603Trace); @@ -784,7 +784,7 @@ describe("OpenClaw TUI chat correlation regression (#2603)", () => { }); it.runIf(process.env[LIVE_REPRO_ENV] === "1")( - "keeps rapid live TUI/webchat sends correlated on a real OpenClaw sandbox", + "keeps rapid live TUI/webchat sends correlated after connected idle on a real OpenClaw sandbox (#6194)", () => { const sandboxName = process.env[LIVE_SANDBOX_ENV] || "hclaw"; const { repro, attempts } = runLiveIssue2603ReproWithEventCaptureRetry(sandboxName); From 9aec0ea7e7a917c77da6af87c6b4e28e7240c928 Mon Sep 17 00:00:00 2001 From: Chengjie Wang Date: Tue, 7 Jul 2026 00:57:29 +0800 Subject: [PATCH 02/37] test(openclaw): cover TUI post-idle interactions Signed-off-by: Chengjie Wang --- .../openclaw-tui-chat-correlation.test.ts | 133 ++++++++++++++++-- .../issue-6194-tui-post-idle-contract.test.ts | 24 ++++ test/nemoclaw-start.test.ts | 8 +- test/openclaw-tui-chat-correlation.test.ts | 4 +- .../cli/config-set-cli-dispatch.test.ts | 47 ++++++- 5 files changed, 201 insertions(+), 15 deletions(-) create mode 100644 test/e2e/support/issue-6194-tui-post-idle-contract.test.ts diff --git a/test/e2e/live/openclaw-tui-chat-correlation.test.ts b/test/e2e/live/openclaw-tui-chat-correlation.test.ts index 025cc35f0c..d4941e2c10 100644 --- a/test/e2e/live/openclaw-tui-chat-correlation.test.ts +++ b/test/e2e/live/openclaw-tui-chat-correlation.test.ts @@ -2,18 +2,18 @@ // SPDX-License-Identifier: Apache-2.0 /** - * Live E2E: OpenClaw TUI/chat correlation regression guard (#2603 + #3145). + * Live E2E: OpenClaw TUI/chat correlation regression guards (#2603 + #3145 + #6194). * * Focused coverage slice for the protocol/history assertions migrated from * entrypoint now hands off to this live target. * * Covered here: ordered, non-empty, correlated replies plus ordered, - * non-duplicated user turns against a real cloud OpenClaw sandbox. TUI - * rendering indicators and visible tool-call status stay out of scope. + * non-duplicated user turns against a real cloud OpenClaw sandbox, then + * terminal TUI input after the visible `connected idle` state. */ import { randomUUID } from "node:crypto"; -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -43,6 +43,10 @@ const EXPECTED_OPENCLAW_VERSION = const LIVE_SCRIPT_NAME = "openclaw-issue2603-chat-correlation.cjs"; const SANDBOX_GATEWAY_PORT = 18789; +const ISSUE6194_TUI_TIMEOUT_SEC = Number.parseInt( + process.env.NEMOCLAW_ISSUE_6194_TUI_TIMEOUT_SEC ?? "240", + 10, +); // ─── Trace analyzer types + helpers (mirrored from // test/openclaw-tui-chat-correlation.test.ts so the live test is @@ -95,6 +99,18 @@ type Issue2603Analysis = { }; type LiveIssue2603Trace = Issue2603Trace & { error?: string }; +type CommandResultText = { stdout: string; stderr: string }; + +function resultText(result: CommandResultText): string { + return [result.stdout, result.stderr].filter(Boolean).join("\n"); +} + +function stripTerminalControl(value: string): string { + return value + .replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)/gu, "") + .replace(/\u001b\[[0-?]*[ -/]*[@-~]/gu, ""); +} + function textFromContent(content: unknown): string { if (typeof content === "string") return content; if (!Array.isArray(content)) return ""; @@ -217,6 +233,68 @@ function analyzeIssue2603Trace({ }; } +function buildIssue6194TuiExpectScript(): string { + return `set timeout $env(NEMOCLAW_ISSUE_6194_TUI_TIMEOUT) +set sandbox $env(NEMOCLAW_ISSUE_6194_SANDBOX) +set capture $env(NEMOCLAW_ISSUE_6194_CAPTURE) +log_file -a $capture +spawn openshell sandbox exec --name $sandbox --tty -- sh -lc {export TERM=xterm-256color; cd /sandbox; openclaw tui} +expect { + -nocase -re {connected[^\\r\\n]*idle} { puts "ISSUE6194_MARK connected_idle_initial" } + timeout { + send "\\003" + exit 10 + } + eof { exit 11 } +} +send -- "Reply exactly NEMOCLAW6194_CHAT_OK and nothing else. Do not use tools.\\r" +expect { + -nocase -re {NEMOCLAW6194_CHAT_OK} { puts "ISSUE6194_MARK chat_reply" } + timeout { + send "\\003" + exit 20 + } + eof { exit 21 } +} +expect { + -nocase -re {connected[^\\r\\n]*idle} { puts "ISSUE6194_MARK connected_idle_after_chat" } + timeout { puts "ISSUE6194_MARK connected_idle_after_chat_missing" } + eof { exit 22 } +} +send -- "/nemoclaw status\\r" +expect { + -nocase -re {(nemoclaw|sandbox|docker|status|managed|openclaw)} { puts "ISSUE6194_MARK slash_status_output" } + timeout { + send "\\003" + exit 30 + } + eof { exit 31 } +} +expect { + -nocase -re {connected[^\\r\\n]*idle} { puts "ISSUE6194_MARK connected_idle_after_status" } + timeout { puts "ISSUE6194_MARK connected_idle_after_status_missing" } + eof { exit 32 } +} +send "\\003" +expect { + eof { + puts "ISSUE6194_MARK clean_exit" + exit 0 + } + timeout { + send "\\003" + expect { + eof { + puts "ISSUE6194_MARK clean_exit" + exit 0 + } + timeout { exit 40 } + } + } +} +`; +} + // The zero-chat-events failure is an observability race at the live // repro boundary: OpenClaw accepts the chat.send requests, but the // websocket client captures no chat stream events before assertions. @@ -484,15 +562,15 @@ async function runLiveIssue2603ReproWithEventCaptureRetry( // ─── The live regression guard ───────────────────────────────────── test( - "openclaw-tui-chat-correlation keeps rapid TUI and webchat sends correlated on a real OpenClaw sandbox (#2603, #3145)", - async ({ artifacts, environment, onboard, sandbox, secrets }) => { + "openclaw-tui-chat-correlation keeps rapid sends correlated and accepts terminal input after connected idle (#2603, #3145, #6194)", + async ({ artifacts, environment, host, onboard, sandbox, secrets }) => { secrets.required("NVIDIA_INFERENCE_API_KEY"); await artifacts.writeJson("target.json", { id: "openclaw-tui-chat-correlation", runner: "vitest", - boundary: "openclaw-gateway-websocket", - issues: ["#2603", "#3145"], + boundary: ["openclaw-gateway-websocket", "openclaw-tui-terminal-after-connected-idle"], + issues: ["#2603", "#3145", "#6194"], ownerIssue: "#4347", pinnedOpenClawVersion: EXPECTED_OPENCLAW_VERSION, }); @@ -574,6 +652,45 @@ test( ); expect(analysis.missingUserTurns, failureSummary).toEqual([]); expect(analysis.duplicateUserTurns, failureSummary).toEqual([]); + + const captureFile = artifacts.pathFor("issue6194-openclaw-tui-capture.log"); + const expectScript = artifacts.pathFor("issue6194-openclaw-tui.expect"); + writeFileSync(expectScript, buildIssue6194TuiExpectScript(), { mode: 0o700 }); + const tui = await host.command("expect", [expectScript], { + artifactName: "issue6194-openclaw-tui-post-idle", + env: { + ...buildAvailabilityProbeEnv(), + NEMOCLAW_ISSUE_6194_SANDBOX: instance.sandboxName, + NEMOCLAW_ISSUE_6194_CAPTURE: captureFile, + NEMOCLAW_ISSUE_6194_TUI_TIMEOUT: String(ISSUE6194_TUI_TIMEOUT_SEC), + }, + timeoutMs: (ISSUE6194_TUI_TIMEOUT_SEC + 30) * 1000, + }); + const plainCapture = stripTerminalControl(readFileSync(captureFile, "utf8")); + const combined = `${resultText(tui)}\n${plainCapture}`; + await artifacts.writeText("issue6194-openclaw-tui-capture.plain.log", plainCapture); + await artifacts.writeJson("issue6194-target-result.json", { + id: "issue-6194-tui-post-connected-idle", + expectExitCode: tui.exitCode, + connectedIdleInitial: combined.includes("ISSUE6194_MARK connected_idle_initial"), + chatReply: combined.includes("ISSUE6194_MARK chat_reply"), + slashStatusOutput: combined.includes("ISSUE6194_MARK slash_status_output"), + cleanExit: combined.includes("ISSUE6194_MARK clean_exit"), + }); + + expect(tui.exitCode, combined).toBe(0); + expect(combined, "TUI must reach connected idle before post-idle input").toContain( + "ISSUE6194_MARK connected_idle_initial", + ); + expect(combined, "post-idle chat must return a visible reply before timeout").toContain( + "ISSUE6194_MARK chat_reply", + ); + expect(combined, "post-idle slash command must render status output before timeout").toContain( + "ISSUE6194_MARK slash_status_output", + ); + expect(combined, "post-idle Ctrl+C must close the TUI session").toContain( + "ISSUE6194_MARK clean_exit", + ); }, // 75-minute budget covers cloud onboarding, sandbox provisioning, gateway // warmup, the 120-second wait-for-replies window, and retry. diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts new file mode 100644 index 0000000000..0ec6f4e125 --- /dev/null +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { readFileSync } from "node:fs"; + +import { describe, expect, it } from "vitest"; + +const LIVE_TUI_SOURCE = "test/e2e/live/openclaw-tui-chat-correlation.test.ts"; + +describe("live TUI post-idle coverage contract (#6194)", () => { + it("drives the real terminal TUI after connected idle", () => { + const source = readFileSync(LIVE_TUI_SOURCE, "utf8"); + + expect(source).toContain('"#6194"'); + expect(source).toContain("openclaw-tui-terminal-after-connected-idle"); + expect(source).toContain("spawn openshell sandbox exec --name $sandbox --tty"); + expect(source).toContain("openclaw tui"); + expect(source).toContain("connected[^\\\\r\\\\n]*idle"); + expect(source).toContain("NEMOCLAW6194_CHAT_OK"); + expect(source).toContain("/nemoclaw status"); + expect(source).toContain('send "\\\\003"'); + expect(source).toContain("ISSUE6194_MARK clean_exit"); + }); +}); diff --git a/test/nemoclaw-start.test.ts b/test/nemoclaw-start.test.ts index 0bcc6818e2..2dc0978f3b 100644 --- a/test/nemoclaw-start.test.ts +++ b/test/nemoclaw-start.test.ts @@ -1629,7 +1629,7 @@ exit 2 }, 40_000); }); -describe("nemoclaw-start auto-pair slow-mode keepalive (#4263, #6194)", () => { +describe("nemoclaw-start auto-pair slow-mode keepalive (#4263)", () => { const src = fs.readFileSync(START_SCRIPT, "utf-8"); function buildAutoPairScript(): string { @@ -1639,7 +1639,7 @@ describe("nemoclaw-start auto-pair slow-mode keepalive (#4263, #6194)", () => { // Shared late-CLI poll timeline: // 1-2: first-time browser pairing request pending. // 3-6: browser paired, nothing pending (watcher converges to slow mode). - // 7-10: late CLI/TUI scope upgrade arrives after the TUI has reached "connected idle". + // 7-10: late CLI scope upgrade arrives after browser pairing converges. // 11+: cli paired alongside browser. function setupLateCliFixture(prefix: string): { tmpDir: string; @@ -1692,7 +1692,7 @@ exit 2 return { tmpDir, fakeOpenclaw, approveLog }; } - it("approves concurrent late TUI CLI scope upgrades after browser pairing converges and drops back to fast cadence (#6194)", () => { + it("approves concurrent late CLI scope upgrades after browser pairing converges and drops back to fast cadence", () => { const { tmpDir, fakeOpenclaw, approveLog } = setupLateCliFixture("nemoclaw-auto-pair-slow-"); try { const run = spawnSync("python3", ["-c", buildAutoPairScript()], { @@ -1716,7 +1716,7 @@ exit 2 expect(run.stdout).toContain( "[auto-pair] browser pairing converged; entering slow-mode approvals=1", ); - // Concurrent late wave — proxy for idle TUI interactions requesting CLI scopes. + // Concurrent late wave after the watcher has already dropped to slow mode. expect(run.stdout).toContain( "[auto-pair] approved request=late-cli client=openclaw-cli mode=cli", ); diff --git a/test/openclaw-tui-chat-correlation.test.ts b/test/openclaw-tui-chat-correlation.test.ts index fc2b957053..e3e515eed0 100644 --- a/test/openclaw-tui-chat-correlation.test.ts +++ b/test/openclaw-tui-chat-correlation.test.ts @@ -586,7 +586,7 @@ function runLiveIssue2603ReproWithEventCaptureRetry(sandboxName: string): LiveIs return { repro, attempts }; } -describe("OpenClaw TUI chat correlation regression (#2603, #6194)", () => { +describe("OpenClaw TUI chat correlation regression (#2603)", () => { it("classifies the observed gateway trace as broken (#2603)", () => { const analysis = analyzeIssue2603Trace(capturedIssue2603Trace); @@ -784,7 +784,7 @@ describe("OpenClaw TUI chat correlation regression (#2603, #6194)", () => { }); it.runIf(process.env[LIVE_REPRO_ENV] === "1")( - "keeps rapid live TUI/webchat sends correlated after connected idle on a real OpenClaw sandbox (#6194)", + "keeps rapid live TUI/webchat sends correlated on a real OpenClaw sandbox", () => { const sandboxName = process.env[LIVE_SANDBOX_ENV] || "hclaw"; const { repro, attempts } = runLiveIssue2603ReproWithEventCaptureRetry(sandboxName); diff --git a/test/package-contract/cli/config-set-cli-dispatch.test.ts b/test/package-contract/cli/config-set-cli-dispatch.test.ts index 5ba2a454e1..a559e7d019 100644 --- a/test/package-contract/cli/config-set-cli-dispatch.test.ts +++ b/test/package-contract/cli/config-set-cli-dispatch.test.ts @@ -18,11 +18,15 @@ function deferred() { describe("config set CLI dispatch", () => { it("awaits configSet before completing the dispatcher", async () => { const cliPath = require.resolve("../../../dist/nemoclaw.js"); + const publicDispatchPath = require.resolve("../../../dist/lib/cli/public-dispatch.js"); + const oclifRunnerPath = require.resolve("../../../dist/lib/cli/oclif-runner.js"); const registryPath = require.resolve("../../../dist/lib/state/registry.js"); const sandboxConfigPath = require.resolve("../../../dist/lib/sandbox/config.js"); const runnerPath = require.resolve("../../../dist/lib/runner.js"); const priorCli = require.cache[cliPath]; + const priorPublicDispatch = require.cache[publicDispatchPath]; + const priorOclifRunner = require.cache[oclifRunnerPath]; const priorRegistry = require.cache[registryPath]; const priorSandboxConfig = require.cache[sandboxConfigPath]; const priorRunner = require.cache[runnerPath]; @@ -30,7 +34,29 @@ describe("config set CLI dispatch", () => { const configSetDeferred = deferred(); const validateName = vi.fn(); - const configSet = vi.fn(() => configSetDeferred.promise); + const configSet = vi.fn((_sandboxName: string, _opts: Record) => { + return configSetDeferred.promise; + }); + const runOclifArgv = vi.fn(async () => { + throw new Error("config set should dispatch by command id"); + }); + const runOclifCommandById = vi.fn(async (commandId: string, args: string[]) => { + expect(commandId).toBe("sandbox:config:set"); + expect(args).toEqual([ + "test-sandbox", + "--key", + "inference.endpoints", + "--value", + "HTTP://93.184.216.34/v1", + "--config-accept-new-path", + ]); + await configSet("test-sandbox", { + key: "inference.endpoints", + value: "HTTP://93.184.216.34/v1", + restart: false, + acceptNewPath: true, + }); + }); process.env.NEMOCLAW_DISABLE_AUTO_DISPATCH = "1"; @@ -52,6 +78,16 @@ describe("config set CLI dispatch", () => { ), } as any; + requireCache[oclifRunnerPath] = { + id: oclifRunnerPath, + filename: oclifRunnerPath, + loaded: true, + exports: { + runOclifArgv, + runOclifCommandById, + }, + } as any; + requireCache[registryPath] = { id: registryPath, filename: registryPath, @@ -75,6 +111,7 @@ describe("config set CLI dispatch", () => { try { delete require.cache[cliPath]; + delete require.cache[publicDispatchPath]; const { dispatchCli } = require(cliPath); const dispatchPromise = dispatchCli([ @@ -94,6 +131,8 @@ describe("config set CLI dispatch", () => { }); await vi.waitFor(() => expect(configSet).toHaveBeenCalledTimes(1)); + expect(runOclifArgv).not.toHaveBeenCalled(); + expect(runOclifCommandById).toHaveBeenCalledTimes(1); expect(configSet).toHaveBeenCalledTimes(1); expect(configSet).toHaveBeenCalledWith("test-sandbox", { key: "inference.endpoints", @@ -116,6 +155,12 @@ describe("config set CLI dispatch", () => { if (priorCli) requireCache[cliPath] = priorCli; else delete requireCache[cliPath]; + if (priorPublicDispatch) requireCache[publicDispatchPath] = priorPublicDispatch; + else delete requireCache[publicDispatchPath]; + + if (priorOclifRunner) requireCache[oclifRunnerPath] = priorOclifRunner; + else delete requireCache[oclifRunnerPath]; + if (priorRegistry) requireCache[registryPath] = priorRegistry; else delete requireCache[registryPath]; From 22a89ab91c0adb0235b6c1ebd3352d33e2e404bc Mon Sep 17 00:00:00 2001 From: Chengjie Wang Date: Tue, 7 Jul 2026 01:00:18 +0800 Subject: [PATCH 03/37] test(cli): avoid conditional cache cleanup Signed-off-by: Chengjie Wang --- .../cli/config-set-cli-dispatch.test.ts | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/test/package-contract/cli/config-set-cli-dispatch.test.ts b/test/package-contract/cli/config-set-cli-dispatch.test.ts index a559e7d019..ecfeb118d6 100644 --- a/test/package-contract/cli/config-set-cli-dispatch.test.ts +++ b/test/package-contract/cli/config-set-cli-dispatch.test.ts @@ -7,6 +7,10 @@ import { describe, expect, it, vi } from "vitest"; const require = createRequire(import.meta.url); const requireCache: Record = require.cache as any; +function restoreRequireCache(modulePath: string, prior: unknown): void { + prior === undefined ? delete requireCache[modulePath] : (requireCache[modulePath] = prior); +} + function deferred() { let resolve!: (value: T | PromiseLike) => void; const promise = new Promise((res) => { @@ -152,23 +156,12 @@ describe("config set CLI dispatch", () => { process.env.NEMOCLAW_DISABLE_AUTO_DISPATCH = priorDisableAutoDispatch; } - if (priorCli) requireCache[cliPath] = priorCli; - else delete requireCache[cliPath]; - - if (priorPublicDispatch) requireCache[publicDispatchPath] = priorPublicDispatch; - else delete requireCache[publicDispatchPath]; - - if (priorOclifRunner) requireCache[oclifRunnerPath] = priorOclifRunner; - else delete requireCache[oclifRunnerPath]; - - if (priorRegistry) requireCache[registryPath] = priorRegistry; - else delete requireCache[registryPath]; - - if (priorSandboxConfig) requireCache[sandboxConfigPath] = priorSandboxConfig; - else delete requireCache[sandboxConfigPath]; - - if (priorRunner) requireCache[runnerPath] = priorRunner; - else delete requireCache[runnerPath]; + restoreRequireCache(cliPath, priorCli); + restoreRequireCache(publicDispatchPath, priorPublicDispatch); + restoreRequireCache(oclifRunnerPath, priorOclifRunner); + restoreRequireCache(registryPath, priorRegistry); + restoreRequireCache(sandboxConfigPath, priorSandboxConfig); + restoreRequireCache(runnerPath, priorRunner); } }); }); From f1fd44de5292a69268a82d6910d62270d0081e24 Mon Sep 17 00:00:00 2001 From: Chengjie Wang Date: Tue, 7 Jul 2026 01:15:06 +0800 Subject: [PATCH 04/37] test(cli): align oclif runner mock shape --- .../cli/config-set-cli-dispatch.test.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/test/package-contract/cli/config-set-cli-dispatch.test.ts b/test/package-contract/cli/config-set-cli-dispatch.test.ts index 495fce165f..c8fd7ce62b 100644 --- a/test/package-contract/cli/config-set-cli-dispatch.test.ts +++ b/test/package-contract/cli/config-set-cli-dispatch.test.ts @@ -86,10 +86,17 @@ describe("config set CLI dispatch", () => { id: oclifRunnerPath, filename: oclifRunnerPath, loaded: true, - exports: { - runOclifArgv, - runOclifCommandById, - }, + exports: new Proxy( + { + runOclifArgv, + runOclifCommandById, + }, + { + get(target, prop) { + return prop in target ? target[prop as keyof typeof target] : vi.fn(); + }, + }, + ), } as any; requireCache[registryPath] = { From afdd0aed9c29ecfcbd2d36d8c5c61838e752d410 Mon Sep 17 00:00:00 2001 From: Chengjie Wang Date: Tue, 7 Jul 2026 01:36:12 +0800 Subject: [PATCH 05/37] test(rebuild): accept notice in recovery harness --- .../sandbox/rebuild-prepared-recovery.test.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/lib/actions/sandbox/rebuild-prepared-recovery.test.ts b/src/lib/actions/sandbox/rebuild-prepared-recovery.test.ts index ed97f54c0c..bb3be4f62f 100644 --- a/src/lib/actions/sandbox/rebuild-prepared-recovery.test.ts +++ b/src/lib/actions/sandbox/rebuild-prepared-recovery.test.ts @@ -1,24 +1,18 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { createRebuildFlowHarness, makePreparedRecoveryManifest, - snapshotEnv, + resetRebuildFlowTestEnvironment, + restoreRebuildFlowTestEnvironment, } from "../../../../test/helpers/rebuild-flow-harness"; -const restoreSandboxEnv = snapshotEnv(["NEMOCLAW_SANDBOX_NAME"]); - describe("prepared rebuild recovery", () => { - beforeEach(() => { - delete process.env.NEMOCLAW_SANDBOX_NAME; - }); + beforeEach(resetRebuildFlowTestEnvironment); - afterEach(() => { - vi.restoreAllMocks(); - restoreSandboxEnv(); - }); + afterEach(restoreRebuildFlowTestEnvironment); it("restores the validated pre-upgrade manifest without taking a second backup (#6114)", async () => { const harness = createRebuildFlowHarness({ From 646f1d0791797752b13b897f781d022589a07f98 Mon Sep 17 00:00:00 2001 From: Chengjie Wang Date: Tue, 7 Jul 2026 02:10:22 +0800 Subject: [PATCH 06/37] test(openclaw): cover post-idle network approval --- test/e2e/fixtures/clients/host.ts | 4 + test/e2e/live/issue-6194-tui-expect.ts | 100 +++++++++++++++++ .../openclaw-tui-chat-correlation.test.ts | 105 ++++++------------ .../issue-6194-tui-post-idle-contract.test.ts | 52 ++++++--- 4 files changed, 179 insertions(+), 82 deletions(-) create mode 100644 test/e2e/live/issue-6194-tui-expect.ts diff --git a/test/e2e/fixtures/clients/host.ts b/test/e2e/fixtures/clients/host.ts index 6f09e8e26c..692676db59 100644 --- a/test/e2e/fixtures/clients/host.ts +++ b/test/e2e/fixtures/clients/host.ts @@ -42,6 +42,10 @@ export class HostCliClient { args: string[] = [], options: ShellProbeRunOptions = {}, ): Promise { + // Trust boundary: this runs trusted host binaries with test-authored + // arguments on the CI host. Do not pass sandbox- or user-controlled + // script content here; sandbox interaction should stay mediated by + // OpenShell/ShellProbe redaction. const merged: ShellProbeRunOptions = { ...options }; if (this.cwd && !merged.cwd) { merged.cwd = this.cwd; diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts new file mode 100644 index 0000000000..1522ebe738 --- /dev/null +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +export const ISSUE6194_TUI_TIMEOUT_SEC = 240; + +export function buildIssue6194TuiExpectScript(): string { + return `set timeout $env(NEMOCLAW_ISSUE_6194_TUI_TIMEOUT) +set sandbox $env(NEMOCLAW_ISSUE_6194_SANDBOX) +set capture $env(NEMOCLAW_ISSUE_6194_CAPTURE) +log_file -a $capture +spawn openshell sandbox exec --name $sandbox --tty -- sh -lc {export TERM=xterm-256color; cd /sandbox; openclaw tui} +expect { + -nocase -re {connected[^\\r\\n]*idle} { puts "ISSUE6194_MARK connected_idle_initial" } + timeout { + send "\\003" + exit 10 + } + eof { exit 11 } +} +send -- "Reply with the three fragments joined by underscores: NEMOCLAW6194, CHAT, OK. Put only that joined token on its own line. Do not use tools.\\r" +expect { + -nocase -re {NEMOCLAW6194_CHAT_OK} { puts "ISSUE6194_MARK chat_reply" } + timeout { + send "\\003" + exit 20 + } + eof { exit 21 } +} +expect { + -nocase -re {connected[^\\r\\n]*idle} { puts "ISSUE6194_MARK connected_idle_after_chat" } + timeout { + send "\\003" + exit 22 + } + eof { exit 23 } +} +send -- "/nemoclaw status\\r" +expect { + -nocase -re "Sandbox:[^\\r\\n]*$sandbox" { puts "ISSUE6194_MARK slash_status_output" } + timeout { + send "\\003" + exit 30 + } + eof { exit 31 } +} +expect { + -nocase -re {connected[^\\r\\n]*idle} { puts "ISSUE6194_MARK connected_idle_after_status" } + timeout { + send "\\003" + exit 32 + } + eof { exit 33 } +} +send -- "Use an available tool to call https://api.atlassian.com/oauth/token/accessible-resources now. Do not describe it.\\r" +expect { + -nocase -re {(Network Rules|pending|approve|blocked)} { puts "ISSUE6194_MARK network_approval_text" } + timeout { + send "\\003" + exit 50 + } + eof { exit 51 } +} +expect { + -nocase -re "Sandbox:[^\\r\\n]*$sandbox" { puts "ISSUE6194_MARK network_approval_prompt" } + timeout { + send "\\003" + exit 52 + } + eof { exit 53 } +} +send -- "a" +after 500 +send -- "y\\r" +expect { + -nocase -re {connected[^\\r\\n]*idle} { puts "ISSUE6194_MARK connected_idle_after_network_approval" } + timeout { + send "\\003" + exit 54 + } + eof { exit 55 } +} +send "\\003" +expect { + eof { + puts "ISSUE6194_MARK clean_exit" + exit 0 + } + timeout { + send "\\003" + expect { + eof { + puts "ISSUE6194_MARK clean_exit" + exit 0 + } + timeout { exit 40 } + } + } +} +`; +} diff --git a/test/e2e/live/openclaw-tui-chat-correlation.test.ts b/test/e2e/live/openclaw-tui-chat-correlation.test.ts index d4941e2c10..cd4ba8c912 100644 --- a/test/e2e/live/openclaw-tui-chat-correlation.test.ts +++ b/test/e2e/live/openclaw-tui-chat-correlation.test.ts @@ -23,6 +23,10 @@ import { type SandboxClient, trustedSandboxShellScript } from "../fixtures/clien import { expect, test } from "../fixtures/e2e-test.ts"; import type { NemoClawInstance } from "../fixtures/phases/onboarding.ts"; import { ubuntuRepoDocker } from "../registry/matrix.ts"; +import { + buildIssue6194TuiExpectScript, + ISSUE6194_TUI_TIMEOUT_SEC, +} from "./issue-6194-tui-expect.ts"; // Reuses the standard ubuntu-repo-docker environment with the // `cloud-openclaw` onboarding profile (already in @@ -43,10 +47,6 @@ const EXPECTED_OPENCLAW_VERSION = const LIVE_SCRIPT_NAME = "openclaw-issue2603-chat-correlation.cjs"; const SANDBOX_GATEWAY_PORT = 18789; -const ISSUE6194_TUI_TIMEOUT_SEC = Number.parseInt( - process.env.NEMOCLAW_ISSUE_6194_TUI_TIMEOUT_SEC ?? "240", - 10, -); // ─── Trace analyzer types + helpers (mirrored from // test/openclaw-tui-chat-correlation.test.ts so the live test is @@ -233,68 +233,6 @@ function analyzeIssue2603Trace({ }; } -function buildIssue6194TuiExpectScript(): string { - return `set timeout $env(NEMOCLAW_ISSUE_6194_TUI_TIMEOUT) -set sandbox $env(NEMOCLAW_ISSUE_6194_SANDBOX) -set capture $env(NEMOCLAW_ISSUE_6194_CAPTURE) -log_file -a $capture -spawn openshell sandbox exec --name $sandbox --tty -- sh -lc {export TERM=xterm-256color; cd /sandbox; openclaw tui} -expect { - -nocase -re {connected[^\\r\\n]*idle} { puts "ISSUE6194_MARK connected_idle_initial" } - timeout { - send "\\003" - exit 10 - } - eof { exit 11 } -} -send -- "Reply exactly NEMOCLAW6194_CHAT_OK and nothing else. Do not use tools.\\r" -expect { - -nocase -re {NEMOCLAW6194_CHAT_OK} { puts "ISSUE6194_MARK chat_reply" } - timeout { - send "\\003" - exit 20 - } - eof { exit 21 } -} -expect { - -nocase -re {connected[^\\r\\n]*idle} { puts "ISSUE6194_MARK connected_idle_after_chat" } - timeout { puts "ISSUE6194_MARK connected_idle_after_chat_missing" } - eof { exit 22 } -} -send -- "/nemoclaw status\\r" -expect { - -nocase -re {(nemoclaw|sandbox|docker|status|managed|openclaw)} { puts "ISSUE6194_MARK slash_status_output" } - timeout { - send "\\003" - exit 30 - } - eof { exit 31 } -} -expect { - -nocase -re {connected[^\\r\\n]*idle} { puts "ISSUE6194_MARK connected_idle_after_status" } - timeout { puts "ISSUE6194_MARK connected_idle_after_status_missing" } - eof { exit 32 } -} -send "\\003" -expect { - eof { - puts "ISSUE6194_MARK clean_exit" - exit 0 - } - timeout { - send "\\003" - expect { - eof { - puts "ISSUE6194_MARK clean_exit" - exit 0 - } - timeout { exit 40 } - } - } -} -`; -} - // The zero-chat-events failure is an observability race at the live // repro boundary: OpenClaw accepts the chat.send requests, but the // websocket client captures no chat stream events before assertions. @@ -564,7 +502,7 @@ async function runLiveIssue2603ReproWithEventCaptureRetry( test( "openclaw-tui-chat-correlation keeps rapid sends correlated and accepts terminal input after connected idle (#2603, #3145, #6194)", async ({ artifacts, environment, host, onboard, sandbox, secrets }) => { - secrets.required("NVIDIA_INFERENCE_API_KEY"); + const apiKey = secrets.required("NVIDIA_INFERENCE_API_KEY"); await artifacts.writeJson("target.json", { id: "openclaw-tui-chat-correlation", @@ -664,9 +602,21 @@ test( NEMOCLAW_ISSUE_6194_CAPTURE: captureFile, NEMOCLAW_ISSUE_6194_TUI_TIMEOUT: String(ISSUE6194_TUI_TIMEOUT_SEC), }, + redactionValues: [apiKey], timeoutMs: (ISSUE6194_TUI_TIMEOUT_SEC + 30) * 1000, }); - const plainCapture = stripTerminalControl(readFileSync(captureFile, "utf8")); + let rawCapture = ""; + try { + rawCapture = readFileSync(captureFile, "utf8"); + } catch (error) { + const fileError = error as NodeJS.ErrnoException; + if (fileError.code !== "ENOENT") { + throw error; + } + } + const redactedCapture = secrets.redact(rawCapture, [apiKey]); + writeFileSync(captureFile, redactedCapture, "utf8"); + const plainCapture = stripTerminalControl(redactedCapture); const combined = `${resultText(tui)}\n${plainCapture}`; await artifacts.writeText("issue6194-openclaw-tui-capture.plain.log", plainCapture); await artifacts.writeJson("issue6194-target-result.json", { @@ -674,7 +624,13 @@ test( expectExitCode: tui.exitCode, connectedIdleInitial: combined.includes("ISSUE6194_MARK connected_idle_initial"), chatReply: combined.includes("ISSUE6194_MARK chat_reply"), + connectedIdleAfterChat: combined.includes("ISSUE6194_MARK connected_idle_after_chat"), slashStatusOutput: combined.includes("ISSUE6194_MARK slash_status_output"), + connectedIdleAfterStatus: combined.includes("ISSUE6194_MARK connected_idle_after_status"), + networkApprovalPrompt: combined.includes("ISSUE6194_MARK network_approval_prompt"), + connectedIdleAfterNetworkApproval: combined.includes( + "ISSUE6194_MARK connected_idle_after_network_approval", + ), cleanExit: combined.includes("ISSUE6194_MARK clean_exit"), }); @@ -685,9 +641,22 @@ test( expect(combined, "post-idle chat must return a visible reply before timeout").toContain( "ISSUE6194_MARK chat_reply", ); + expect(combined, "TUI must return to connected idle after the post-idle chat reply").toContain( + "ISSUE6194_MARK connected_idle_after_chat", + ); expect(combined, "post-idle slash command must render status output before timeout").toContain( "ISSUE6194_MARK slash_status_output", ); + expect(combined, "TUI must return to connected idle after /nemoclaw status").toContain( + "ISSUE6194_MARK connected_idle_after_status", + ); + expect( + combined, + "post-idle network request must present the sandbox approval prompt", + ).toContain("ISSUE6194_MARK network_approval_prompt"); + expect(combined, "TUI must return to connected idle after network approval input").toContain( + "ISSUE6194_MARK connected_idle_after_network_approval", + ); expect(combined, "post-idle Ctrl+C must close the TUI session").toContain( "ISSUE6194_MARK clean_exit", ); diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index 0ec6f4e125..4bf5acc2d4 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -1,24 +1,48 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { readFileSync } from "node:fs"; - import { describe, expect, it } from "vitest"; -const LIVE_TUI_SOURCE = "test/e2e/live/openclaw-tui-chat-correlation.test.ts"; +import { + buildIssue6194TuiExpectScript, + ISSUE6194_TUI_TIMEOUT_SEC, +} from "../live/issue-6194-tui-expect.ts"; describe("live TUI post-idle coverage contract (#6194)", () => { - it("drives the real terminal TUI after connected idle", () => { - const source = readFileSync(LIVE_TUI_SOURCE, "utf8"); + it("builds an expect flow for chat, slash status, network approval, and clean exit", () => { + const script = buildIssue6194TuiExpectScript(); + + expect(ISSUE6194_TUI_TIMEOUT_SEC).toBe(240); + expect(script).toContain("spawn openshell sandbox exec --name $sandbox --tty"); + expect(script).toContain("openclaw tui"); + expect(script).toContain("ISSUE6194_MARK connected_idle_initial"); + expect(script).toContain("ISSUE6194_MARK chat_reply"); + expect(script).toContain("ISSUE6194_MARK connected_idle_after_chat"); + expect(script).toContain("/nemoclaw status"); + expect(script).toContain("Sandbox:[^\\r\\n]*$sandbox"); + expect(script).toContain("ISSUE6194_MARK slash_status_output"); + expect(script).toContain("ISSUE6194_MARK connected_idle_after_status"); + expect(script).toContain("https://api.atlassian.com/oauth/token/accessible-resources"); + expect(script).toContain("ISSUE6194_MARK network_approval_text"); + expect(script).toContain("ISSUE6194_MARK network_approval_prompt"); + expect(script).toContain("ISSUE6194_MARK connected_idle_after_network_approval"); + expect(script).toContain("ISSUE6194_MARK clean_exit"); + expect(script).not.toContain("NEMOCLAW_ISSUE_6194_TUI_TIMEOUT_SEC"); + expect(script).not.toContain("(nemoclaw|sandbox|docker|status|managed|openclaw)"); + expect(script).not.toContain("approval-flow probe"); - expect(source).toContain('"#6194"'); - expect(source).toContain("openclaw-tui-terminal-after-connected-idle"); - expect(source).toContain("spawn openshell sandbox exec --name $sandbox --tty"); - expect(source).toContain("openclaw tui"); - expect(source).toContain("connected[^\\\\r\\\\n]*idle"); - expect(source).toContain("NEMOCLAW6194_CHAT_OK"); - expect(source).toContain("/nemoclaw status"); - expect(source).toContain('send "\\\\003"'); - expect(source).toContain("ISSUE6194_MARK clean_exit"); + const order = [ + "connected_idle_initial", + "chat_reply", + "connected_idle_after_chat", + "slash_status_output", + "connected_idle_after_status", + "network_approval_text", + "network_approval_prompt", + "connected_idle_after_network_approval", + "clean_exit", + ].map((marker) => script.indexOf(marker)); + expect(order.every((index) => index >= 0)).toBe(true); + expect([...order].sort((a, b) => a - b)).toEqual(order); }); }); From e887c23d1caf97cd4e1847a431c1e80016920156 Mon Sep 17 00:00:00 2001 From: Chengjie Wang Date: Tue, 7 Jul 2026 02:13:47 +0800 Subject: [PATCH 07/37] test(openclaw): address post-idle review follow-up --- test/e2e/live/issue-6194-tui-expect.ts | 14 ++++++++++++++ .../e2e/live/openclaw-tui-chat-correlation.test.ts | 11 ++--------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index 1522ebe738..7267f65179 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -1,8 +1,22 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { readFileSync } from "node:fs"; + export const ISSUE6194_TUI_TIMEOUT_SEC = 240; +export function readOptionalIssue6194Capture(path: string): string { + try { + return readFileSync(path, "utf8"); + } catch (error) { + const fileError = error as NodeJS.ErrnoException; + if (fileError.code !== "ENOENT") { + throw error; + } + return ""; + } +} + export function buildIssue6194TuiExpectScript(): string { return `set timeout $env(NEMOCLAW_ISSUE_6194_TUI_TIMEOUT) set sandbox $env(NEMOCLAW_ISSUE_6194_SANDBOX) diff --git a/test/e2e/live/openclaw-tui-chat-correlation.test.ts b/test/e2e/live/openclaw-tui-chat-correlation.test.ts index cd4ba8c912..3c7def064b 100644 --- a/test/e2e/live/openclaw-tui-chat-correlation.test.ts +++ b/test/e2e/live/openclaw-tui-chat-correlation.test.ts @@ -26,6 +26,7 @@ import { ubuntuRepoDocker } from "../registry/matrix.ts"; import { buildIssue6194TuiExpectScript, ISSUE6194_TUI_TIMEOUT_SEC, + readOptionalIssue6194Capture, } from "./issue-6194-tui-expect.ts"; // Reuses the standard ubuntu-repo-docker environment with the @@ -605,15 +606,7 @@ test( redactionValues: [apiKey], timeoutMs: (ISSUE6194_TUI_TIMEOUT_SEC + 30) * 1000, }); - let rawCapture = ""; - try { - rawCapture = readFileSync(captureFile, "utf8"); - } catch (error) { - const fileError = error as NodeJS.ErrnoException; - if (fileError.code !== "ENOENT") { - throw error; - } - } + const rawCapture = readOptionalIssue6194Capture(captureFile); const redactedCapture = secrets.redact(rawCapture, [apiKey]); writeFileSync(captureFile, redactedCapture, "utf8"); const plainCapture = stripTerminalControl(redactedCapture); From c467c94f5a19609a2fbd9280162c649dd609c5db Mon Sep 17 00:00:00 2001 From: Chengjie Wang Date: Tue, 7 Jul 2026 02:26:19 +0800 Subject: [PATCH 08/37] test(openclaw): tighten network approval markers --- test/e2e/live/issue-6194-tui-expect.ts | 32 ++++++++++++++----- .../openclaw-tui-chat-correlation.test.ts | 4 +++ .../issue-6194-tui-post-idle-contract.test.ts | 2 ++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index 7267f65179..1cdc17d8f2 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -67,31 +67,47 @@ expect { } send -- "Use an available tool to call https://api.atlassian.com/oauth/token/accessible-resources now. Do not describe it.\\r" expect { - -nocase -re {(Network Rules|pending|approve|blocked)} { puts "ISSUE6194_MARK network_approval_text" } - timeout { + -nocase -re {(blocked|denied|rejected)} { send "\\003" exit 50 } - eof { exit 51 } + -nocase -re {(Network Rules|pending|approve)} { puts "ISSUE6194_MARK network_approval_text" } + timeout { + send "\\003" + exit 51 + } + eof { exit 52 } } expect { - -nocase -re "Sandbox:[^\\r\\n]*$sandbox" { puts "ISSUE6194_MARK network_approval_prompt" } + -nocase -re "(Sandbox:[^\\r\\n]*$sandbox[^\\r\\n]*(pending|approve)|(pending|approve)[^\\r\\n]*Sandbox:[^\\r\\n]*$sandbox)" { puts "ISSUE6194_MARK network_approval_prompt" } timeout { send "\\003" - exit 52 + exit 53 } - eof { exit 53 } + eof { exit 54 } } send -- "a" after 500 send -- "y\\r" +expect { + -nocase -re {(approved|allowed|accepted|approval[^\\r\\n]*(processed|granted)|request[^\\r\\n]*(approved|allowed))} { puts "ISSUE6194_MARK network_approval_processed" } + -nocase -re {(blocked|denied|rejected)} { + send "\\003" + exit 55 + } + timeout { + send "\\003" + exit 56 + } + eof { exit 57 } +} expect { -nocase -re {connected[^\\r\\n]*idle} { puts "ISSUE6194_MARK connected_idle_after_network_approval" } timeout { send "\\003" - exit 54 + exit 58 } - eof { exit 55 } + eof { exit 59 } } send "\\003" expect { diff --git a/test/e2e/live/openclaw-tui-chat-correlation.test.ts b/test/e2e/live/openclaw-tui-chat-correlation.test.ts index 3c7def064b..ac6a63316f 100644 --- a/test/e2e/live/openclaw-tui-chat-correlation.test.ts +++ b/test/e2e/live/openclaw-tui-chat-correlation.test.ts @@ -621,6 +621,7 @@ test( slashStatusOutput: combined.includes("ISSUE6194_MARK slash_status_output"), connectedIdleAfterStatus: combined.includes("ISSUE6194_MARK connected_idle_after_status"), networkApprovalPrompt: combined.includes("ISSUE6194_MARK network_approval_prompt"), + networkApprovalProcessed: combined.includes("ISSUE6194_MARK network_approval_processed"), connectedIdleAfterNetworkApproval: combined.includes( "ISSUE6194_MARK connected_idle_after_network_approval", ), @@ -647,6 +648,9 @@ test( combined, "post-idle network request must present the sandbox approval prompt", ).toContain("ISSUE6194_MARK network_approval_prompt"); + expect(combined, "post-idle network approval input must be processed").toContain( + "ISSUE6194_MARK network_approval_processed", + ); expect(combined, "TUI must return to connected idle after network approval input").toContain( "ISSUE6194_MARK connected_idle_after_network_approval", ); diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index 4bf5acc2d4..8b943d80c1 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -25,6 +25,7 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect(script).toContain("https://api.atlassian.com/oauth/token/accessible-resources"); expect(script).toContain("ISSUE6194_MARK network_approval_text"); expect(script).toContain("ISSUE6194_MARK network_approval_prompt"); + expect(script).toContain("ISSUE6194_MARK network_approval_processed"); expect(script).toContain("ISSUE6194_MARK connected_idle_after_network_approval"); expect(script).toContain("ISSUE6194_MARK clean_exit"); expect(script).not.toContain("NEMOCLAW_ISSUE_6194_TUI_TIMEOUT_SEC"); @@ -39,6 +40,7 @@ describe("live TUI post-idle coverage contract (#6194)", () => { "connected_idle_after_status", "network_approval_text", "network_approval_prompt", + "network_approval_processed", "connected_idle_after_network_approval", "clean_exit", ].map((marker) => script.indexOf(marker)); From 933640c8fede1e4c6778540d63ff4f3d49956117 Mon Sep 17 00:00:00 2001 From: Chengjie Wang Date: Tue, 7 Jul 2026 02:39:19 +0800 Subject: [PATCH 09/37] test(openclaw): match issue 6194 session path --- test/e2e/live/issue-6194-tui-expect.ts | 17 ++--------- .../openclaw-tui-chat-correlation.test.ts | 3 +- .../issue-6194-tui-post-idle-contract.test.ts | 4 ++- .../cli/config-set-cli-dispatch.test.ts | 29 ++++++------------- 4 files changed, 15 insertions(+), 38 deletions(-) diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index 1cdc17d8f2..4bcd1d0e2f 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -1,28 +1,15 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { readFileSync } from "node:fs"; - export const ISSUE6194_TUI_TIMEOUT_SEC = 240; - -export function readOptionalIssue6194Capture(path: string): string { - try { - return readFileSync(path, "utf8"); - } catch (error) { - const fileError = error as NodeJS.ErrnoException; - if (fileError.code !== "ENOENT") { - throw error; - } - return ""; - } -} +export const ISSUE6194_TUI_SESSION = "test-session"; export function buildIssue6194TuiExpectScript(): string { return `set timeout $env(NEMOCLAW_ISSUE_6194_TUI_TIMEOUT) set sandbox $env(NEMOCLAW_ISSUE_6194_SANDBOX) set capture $env(NEMOCLAW_ISSUE_6194_CAPTURE) log_file -a $capture -spawn openshell sandbox exec --name $sandbox --tty -- sh -lc {export TERM=xterm-256color; cd /sandbox; openclaw tui} + spawn openshell sandbox exec --name $sandbox --tty -- sh -lc {export TERM=xterm-256color; cd /sandbox; openclaw tui --session ${ISSUE6194_TUI_SESSION}} expect { -nocase -re {connected[^\\r\\n]*idle} { puts "ISSUE6194_MARK connected_idle_initial" } timeout { diff --git a/test/e2e/live/openclaw-tui-chat-correlation.test.ts b/test/e2e/live/openclaw-tui-chat-correlation.test.ts index ac6a63316f..872b211c69 100644 --- a/test/e2e/live/openclaw-tui-chat-correlation.test.ts +++ b/test/e2e/live/openclaw-tui-chat-correlation.test.ts @@ -26,7 +26,6 @@ import { ubuntuRepoDocker } from "../registry/matrix.ts"; import { buildIssue6194TuiExpectScript, ISSUE6194_TUI_TIMEOUT_SEC, - readOptionalIssue6194Capture, } from "./issue-6194-tui-expect.ts"; // Reuses the standard ubuntu-repo-docker environment with the @@ -606,7 +605,7 @@ test( redactionValues: [apiKey], timeoutMs: (ISSUE6194_TUI_TIMEOUT_SEC + 30) * 1000, }); - const rawCapture = readOptionalIssue6194Capture(captureFile); + const rawCapture = readFileSync(captureFile, "utf8"); const redactedCapture = secrets.redact(rawCapture, [apiKey]); writeFileSync(captureFile, redactedCapture, "utf8"); const plainCapture = stripTerminalControl(redactedCapture); diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index 8b943d80c1..d6e5bfc986 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest"; import { buildIssue6194TuiExpectScript, + ISSUE6194_TUI_SESSION, ISSUE6194_TUI_TIMEOUT_SEC, } from "../live/issue-6194-tui-expect.ts"; @@ -13,8 +14,9 @@ describe("live TUI post-idle coverage contract (#6194)", () => { const script = buildIssue6194TuiExpectScript(); expect(ISSUE6194_TUI_TIMEOUT_SEC).toBe(240); + expect(ISSUE6194_TUI_SESSION).toBe("test-session"); expect(script).toContain("spawn openshell sandbox exec --name $sandbox --tty"); - expect(script).toContain("openclaw tui"); + expect(script).toContain("openclaw tui --session test-session"); expect(script).toContain("ISSUE6194_MARK connected_idle_initial"); expect(script).toContain("ISSUE6194_MARK chat_reply"); expect(script).toContain("ISSUE6194_MARK connected_idle_after_chat"); diff --git a/test/package-contract/cli/config-set-cli-dispatch.test.ts b/test/package-contract/cli/config-set-cli-dispatch.test.ts index c8fd7ce62b..19793f4133 100644 --- a/test/package-contract/cli/config-set-cli-dispatch.test.ts +++ b/test/package-contract/cli/config-set-cli-dispatch.test.ts @@ -20,7 +20,7 @@ function deferred() { } describe("config set CLI dispatch", () => { - it("awaits configSet before completing the dispatcher", async () => { + it("awaits config set command dispatch before completing the dispatcher", async () => { const cliPath = require.resolve("../../../dist/nemoclaw.js"); const publicDispatchPath = require.resolve("../../../dist/lib/cli/public-dispatch.js"); const oclifRunnerPath = require.resolve("../../../dist/lib/cli/oclif-runner.js"); @@ -36,11 +36,9 @@ describe("config set CLI dispatch", () => { const priorRunner = require.cache[runnerPath]; const priorDisableAutoDispatch = process.env.NEMOCLAW_DISABLE_AUTO_DISPATCH; - const configSetDeferred = deferred(); + const commandDispatchDeferred = deferred(); const validateName = vi.fn(); - const configSet = vi.fn((_sandboxName: string, _opts: Record) => { - return configSetDeferred.promise; - }); + const configSet = vi.fn(); const runOclifArgv = vi.fn(async () => { throw new Error("config set should dispatch by command id"); }); @@ -54,12 +52,7 @@ describe("config set CLI dispatch", () => { "HTTP://93.184.216.34/v1", "--config-accept-new-path", ]); - await configSet("test-sandbox", { - key: "inference.endpoints", - value: "HTTP://93.184.216.34/v1", - restart: false, - acceptNewPath: true, - }); + return commandDispatchDeferred.promise; }); process.env.NEMOCLAW_DISABLE_AUTO_DISPATCH = "1"; @@ -141,19 +134,15 @@ describe("config set CLI dispatch", () => { settled = true; }); - await vi.waitFor(() => expect(configSet).toHaveBeenCalledTimes(1), { timeout: 4_000 }); + await vi.waitFor(() => expect(runOclifCommandById).toHaveBeenCalledTimes(1), { + timeout: 4_000, + }); expect(runOclifArgv).not.toHaveBeenCalled(); expect(runOclifCommandById).toHaveBeenCalledTimes(1); - expect(configSet).toHaveBeenCalledTimes(1); - expect(configSet).toHaveBeenCalledWith("test-sandbox", { - key: "inference.endpoints", - value: "HTTP://93.184.216.34/v1", - restart: false, - acceptNewPath: true, - }); + expect(configSet).not.toHaveBeenCalled(); expect(settled).toBe(false); - configSetDeferred.resolve(); + commandDispatchDeferred.resolve(); await expect(dispatchPromise).resolves.toBeUndefined(); expect(settled).toBe(true); } finally { From ab81f28c49b1b6aabf6e537f1b4b25d6333ff437 Mon Sep 17 00:00:00 2001 From: Chengjie Wang Date: Tue, 7 Jul 2026 02:52:43 +0800 Subject: [PATCH 10/37] test(openclaw): harden issue 6194 follow-up coverage --- test/e2e/live/issue-6194-tui-expect.ts | 44 +++--- .../openclaw-tui-chat-correlation.test.ts | 136 ++++++++++-------- .../issue-6194-tui-post-idle-contract.test.ts | 22 +-- .../cli/config-set-cli-dispatch.test.ts | 30 ++-- 4 files changed, 123 insertions(+), 109 deletions(-) diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index 4bcd1d0e2f..4463fb7574 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -9,9 +9,13 @@ export function buildIssue6194TuiExpectScript(): string { set sandbox $env(NEMOCLAW_ISSUE_6194_SANDBOX) set capture $env(NEMOCLAW_ISSUE_6194_CAPTURE) log_file -a $capture - spawn openshell sandbox exec --name $sandbox --tty -- sh -lc {export TERM=xterm-256color; cd /sandbox; openclaw tui --session ${ISSUE6194_TUI_SESSION}} +proc mark {name} { + puts "ISSUE6194_MARK $name" + send_log "ISSUE6194_MARK $name\\n" +} +spawn openshell sandbox exec --name $sandbox --tty -- sh -lc {export TERM=xterm-256color; cd /sandbox; openclaw tui --session ${ISSUE6194_TUI_SESSION}} expect { - -nocase -re {connected[^\\r\\n]*idle} { puts "ISSUE6194_MARK connected_idle_initial" } + -nocase -re {connected[^\\r\\n]*idle} { mark connected_idle_initial } timeout { send "\\003" exit 10 @@ -20,7 +24,7 @@ expect { } send -- "Reply with the three fragments joined by underscores: NEMOCLAW6194, CHAT, OK. Put only that joined token on its own line. Do not use tools.\\r" expect { - -nocase -re {NEMOCLAW6194_CHAT_OK} { puts "ISSUE6194_MARK chat_reply" } + -nocase -re {NEMOCLAW6194_CHAT_OK} { mark chat_reply } timeout { send "\\003" exit 20 @@ -28,7 +32,7 @@ expect { eof { exit 21 } } expect { - -nocase -re {connected[^\\r\\n]*idle} { puts "ISSUE6194_MARK connected_idle_after_chat" } + -nocase -re {connected[^\\r\\n]*idle} { mark connected_idle_after_chat } timeout { send "\\003" exit 22 @@ -37,7 +41,7 @@ expect { } send -- "/nemoclaw status\\r" expect { - -nocase -re "Sandbox:[^\\r\\n]*$sandbox" { puts "ISSUE6194_MARK slash_status_output" } + -nocase -re "Sandbox:[^\\r\\n]*$sandbox" { mark slash_status_output } timeout { send "\\003" exit 30 @@ -45,7 +49,7 @@ expect { eof { exit 31 } } expect { - -nocase -re {connected[^\\r\\n]*idle} { puts "ISSUE6194_MARK connected_idle_after_status" } + -nocase -re {connected[^\\r\\n]*idle} { mark connected_idle_after_status } timeout { send "\\003" exit 32 @@ -58,55 +62,47 @@ expect { send "\\003" exit 50 } - -nocase -re {(Network Rules|pending|approve)} { puts "ISSUE6194_MARK network_approval_text" } + -nocase -re "(Sandbox:[^\\r\\n]*$sandbox[^\\r\\n]*(Network Rules|pending|approve)|(Network Rules|pending|approve)[^\\r\\n]*Sandbox:[^\\r\\n]*$sandbox)" { mark network_approval_prompt } timeout { send "\\003" exit 51 } eof { exit 52 } } -expect { - -nocase -re "(Sandbox:[^\\r\\n]*$sandbox[^\\r\\n]*(pending|approve)|(pending|approve)[^\\r\\n]*Sandbox:[^\\r\\n]*$sandbox)" { puts "ISSUE6194_MARK network_approval_prompt" } - timeout { - send "\\003" - exit 53 - } - eof { exit 54 } -} send -- "a" after 500 send -- "y\\r" expect { - -nocase -re {(approved|allowed|accepted|approval[^\\r\\n]*(processed|granted)|request[^\\r\\n]*(approved|allowed))} { puts "ISSUE6194_MARK network_approval_processed" } + -nocase -re {(approved|allowed|accepted|approval[^\\r\\n]*(processed|granted)|request[^\\r\\n]*(approved|allowed))} { mark network_approval_processed } -nocase -re {(blocked|denied|rejected)} { send "\\003" - exit 55 + exit 53 } timeout { send "\\003" - exit 56 + exit 54 } - eof { exit 57 } + eof { exit 55 } } expect { - -nocase -re {connected[^\\r\\n]*idle} { puts "ISSUE6194_MARK connected_idle_after_network_approval" } + -nocase -re {connected[^\\r\\n]*idle} { mark connected_idle_after_network_approval } timeout { send "\\003" - exit 58 + exit 56 } - eof { exit 59 } + eof { exit 57 } } send "\\003" expect { eof { - puts "ISSUE6194_MARK clean_exit" + mark clean_exit exit 0 } timeout { send "\\003" expect { eof { - puts "ISSUE6194_MARK clean_exit" + mark clean_exit exit 0 } timeout { exit 40 } diff --git a/test/e2e/live/openclaw-tui-chat-correlation.test.ts b/test/e2e/live/openclaw-tui-chat-correlation.test.ts index 872b211c69..bf93ac67e9 100644 --- a/test/e2e/live/openclaw-tui-chat-correlation.test.ts +++ b/test/e2e/live/openclaw-tui-chat-correlation.test.ts @@ -591,71 +591,83 @@ test( expect(analysis.missingUserTurns, failureSummary).toEqual([]); expect(analysis.duplicateUserTurns, failureSummary).toEqual([]); - const captureFile = artifacts.pathFor("issue6194-openclaw-tui-capture.log"); + const captureDir = mkdtempSync(join(tmpdir(), "nemoclaw-issue6194-tui-")); + const captureFile = join(captureDir, "openclaw-tui-capture.log"); const expectScript = artifacts.pathFor("issue6194-openclaw-tui.expect"); writeFileSync(expectScript, buildIssue6194TuiExpectScript(), { mode: 0o700 }); - const tui = await host.command("expect", [expectScript], { - artifactName: "issue6194-openclaw-tui-post-idle", - env: { - ...buildAvailabilityProbeEnv(), - NEMOCLAW_ISSUE_6194_SANDBOX: instance.sandboxName, - NEMOCLAW_ISSUE_6194_CAPTURE: captureFile, - NEMOCLAW_ISSUE_6194_TUI_TIMEOUT: String(ISSUE6194_TUI_TIMEOUT_SEC), - }, - redactionValues: [apiKey], - timeoutMs: (ISSUE6194_TUI_TIMEOUT_SEC + 30) * 1000, - }); - const rawCapture = readFileSync(captureFile, "utf8"); - const redactedCapture = secrets.redact(rawCapture, [apiKey]); - writeFileSync(captureFile, redactedCapture, "utf8"); - const plainCapture = stripTerminalControl(redactedCapture); - const combined = `${resultText(tui)}\n${plainCapture}`; - await artifacts.writeText("issue6194-openclaw-tui-capture.plain.log", plainCapture); - await artifacts.writeJson("issue6194-target-result.json", { - id: "issue-6194-tui-post-connected-idle", - expectExitCode: tui.exitCode, - connectedIdleInitial: combined.includes("ISSUE6194_MARK connected_idle_initial"), - chatReply: combined.includes("ISSUE6194_MARK chat_reply"), - connectedIdleAfterChat: combined.includes("ISSUE6194_MARK connected_idle_after_chat"), - slashStatusOutput: combined.includes("ISSUE6194_MARK slash_status_output"), - connectedIdleAfterStatus: combined.includes("ISSUE6194_MARK connected_idle_after_status"), - networkApprovalPrompt: combined.includes("ISSUE6194_MARK network_approval_prompt"), - networkApprovalProcessed: combined.includes("ISSUE6194_MARK network_approval_processed"), - connectedIdleAfterNetworkApproval: combined.includes( - "ISSUE6194_MARK connected_idle_after_network_approval", - ), - cleanExit: combined.includes("ISSUE6194_MARK clean_exit"), - }); + try { + const tui = await host.command("expect", [expectScript], { + artifactName: "issue6194-openclaw-tui-post-idle", + env: { + ...buildAvailabilityProbeEnv(), + NEMOCLAW_ISSUE_6194_SANDBOX: instance.sandboxName, + NEMOCLAW_ISSUE_6194_CAPTURE: captureFile, + NEMOCLAW_ISSUE_6194_TUI_TIMEOUT: String(ISSUE6194_TUI_TIMEOUT_SEC), + }, + redactionValues: [apiKey], + timeoutMs: (ISSUE6194_TUI_TIMEOUT_SEC + 30) * 1000, + }); + const rawCapture = readFileSync(captureFile, "utf8"); + const redactedCapture = secrets.redact(rawCapture, [apiKey]); + const plainCapture = stripTerminalControl(redactedCapture); + expect(plainCapture.length, "TUI expect capture must not be empty").toBeGreaterThan(0); + expect(plainCapture, "TUI expect capture must include expect-script markers").toContain( + "ISSUE6194_MARK", + ); - expect(tui.exitCode, combined).toBe(0); - expect(combined, "TUI must reach connected idle before post-idle input").toContain( - "ISSUE6194_MARK connected_idle_initial", - ); - expect(combined, "post-idle chat must return a visible reply before timeout").toContain( - "ISSUE6194_MARK chat_reply", - ); - expect(combined, "TUI must return to connected idle after the post-idle chat reply").toContain( - "ISSUE6194_MARK connected_idle_after_chat", - ); - expect(combined, "post-idle slash command must render status output before timeout").toContain( - "ISSUE6194_MARK slash_status_output", - ); - expect(combined, "TUI must return to connected idle after /nemoclaw status").toContain( - "ISSUE6194_MARK connected_idle_after_status", - ); - expect( - combined, - "post-idle network request must present the sandbox approval prompt", - ).toContain("ISSUE6194_MARK network_approval_prompt"); - expect(combined, "post-idle network approval input must be processed").toContain( - "ISSUE6194_MARK network_approval_processed", - ); - expect(combined, "TUI must return to connected idle after network approval input").toContain( - "ISSUE6194_MARK connected_idle_after_network_approval", - ); - expect(combined, "post-idle Ctrl+C must close the TUI session").toContain( - "ISSUE6194_MARK clean_exit", - ); + const combined = `${resultText(tui)}\n${plainCapture}`; + await artifacts.writeText("issue6194-openclaw-tui-capture.log", redactedCapture); + await artifacts.writeText("issue6194-openclaw-tui-capture.plain.log", plainCapture); + await artifacts.writeJson("issue6194-target-result.json", { + id: "issue-6194-tui-post-connected-idle", + expectExitCode: tui.exitCode, + connectedIdleInitial: combined.includes("ISSUE6194_MARK connected_idle_initial"), + chatReply: combined.includes("ISSUE6194_MARK chat_reply"), + connectedIdleAfterChat: combined.includes("ISSUE6194_MARK connected_idle_after_chat"), + slashStatusOutput: combined.includes("ISSUE6194_MARK slash_status_output"), + connectedIdleAfterStatus: combined.includes("ISSUE6194_MARK connected_idle_after_status"), + networkApprovalPrompt: combined.includes("ISSUE6194_MARK network_approval_prompt"), + networkApprovalProcessed: combined.includes("ISSUE6194_MARK network_approval_processed"), + connectedIdleAfterNetworkApproval: combined.includes( + "ISSUE6194_MARK connected_idle_after_network_approval", + ), + cleanExit: combined.includes("ISSUE6194_MARK clean_exit"), + }); + + expect(tui.exitCode, combined).toBe(0); + expect(combined, "TUI must reach connected idle before post-idle input").toContain( + "ISSUE6194_MARK connected_idle_initial", + ); + expect(combined, "post-idle chat must return a visible reply before timeout").toContain( + "ISSUE6194_MARK chat_reply", + ); + expect( + combined, + "TUI must return to connected idle after the post-idle chat reply", + ).toContain("ISSUE6194_MARK connected_idle_after_chat"); + expect( + combined, + "post-idle slash command must render status output before timeout", + ).toContain("ISSUE6194_MARK slash_status_output"); + expect(combined, "TUI must return to connected idle after /nemoclaw status").toContain( + "ISSUE6194_MARK connected_idle_after_status", + ); + expect( + combined, + "post-idle network request must present the sandbox approval prompt", + ).toContain("ISSUE6194_MARK network_approval_prompt"); + expect(combined, "post-idle network approval input must be processed").toContain( + "ISSUE6194_MARK network_approval_processed", + ); + expect(combined, "TUI must return to connected idle after network approval input").toContain( + "ISSUE6194_MARK connected_idle_after_network_approval", + ); + expect(combined, "post-idle Ctrl+C must close the TUI session").toContain( + "ISSUE6194_MARK clean_exit", + ); + } finally { + rmSync(captureDir, { recursive: true, force: true }); + } }, // 75-minute budget covers cloud onboarding, sandbox provisioning, gateway // warmup, the 120-second wait-for-replies window, and retry. diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index d6e5bfc986..0b2072bbbe 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -17,19 +17,20 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect(ISSUE6194_TUI_SESSION).toBe("test-session"); expect(script).toContain("spawn openshell sandbox exec --name $sandbox --tty"); expect(script).toContain("openclaw tui --session test-session"); - expect(script).toContain("ISSUE6194_MARK connected_idle_initial"); - expect(script).toContain("ISSUE6194_MARK chat_reply"); - expect(script).toContain("ISSUE6194_MARK connected_idle_after_chat"); + expect(script).toContain('puts "ISSUE6194_MARK $name"'); + expect(script).toContain('send_log "ISSUE6194_MARK $name\\n"'); + expect(script).toContain("mark connected_idle_initial"); + expect(script).toContain("mark chat_reply"); + expect(script).toContain("mark connected_idle_after_chat"); expect(script).toContain("/nemoclaw status"); expect(script).toContain("Sandbox:[^\\r\\n]*$sandbox"); - expect(script).toContain("ISSUE6194_MARK slash_status_output"); - expect(script).toContain("ISSUE6194_MARK connected_idle_after_status"); + expect(script).toContain("mark slash_status_output"); + expect(script).toContain("mark connected_idle_after_status"); expect(script).toContain("https://api.atlassian.com/oauth/token/accessible-resources"); - expect(script).toContain("ISSUE6194_MARK network_approval_text"); - expect(script).toContain("ISSUE6194_MARK network_approval_prompt"); - expect(script).toContain("ISSUE6194_MARK network_approval_processed"); - expect(script).toContain("ISSUE6194_MARK connected_idle_after_network_approval"); - expect(script).toContain("ISSUE6194_MARK clean_exit"); + expect(script).toContain("mark network_approval_prompt"); + expect(script).toContain("mark network_approval_processed"); + expect(script).toContain("mark connected_idle_after_network_approval"); + expect(script).toContain("mark clean_exit"); expect(script).not.toContain("NEMOCLAW_ISSUE_6194_TUI_TIMEOUT_SEC"); expect(script).not.toContain("(nemoclaw|sandbox|docker|status|managed|openclaw)"); expect(script).not.toContain("approval-flow probe"); @@ -40,7 +41,6 @@ describe("live TUI post-idle coverage contract (#6194)", () => { "connected_idle_after_chat", "slash_status_output", "connected_idle_after_status", - "network_approval_text", "network_approval_prompt", "network_approval_processed", "connected_idle_after_network_approval", diff --git a/test/package-contract/cli/config-set-cli-dispatch.test.ts b/test/package-contract/cli/config-set-cli-dispatch.test.ts index 19793f4133..a41f8c6e4b 100644 --- a/test/package-contract/cli/config-set-cli-dispatch.test.ts +++ b/test/package-contract/cli/config-set-cli-dispatch.test.ts @@ -36,24 +36,21 @@ describe("config set CLI dispatch", () => { const priorRunner = require.cache[runnerPath]; const priorDisableAutoDispatch = process.env.NEMOCLAW_DISABLE_AUTO_DISPATCH; + const expectedConfigSetDispatchArgs = [ + "test-sandbox", + "--key", + "inference.endpoints", + "--value", + "HTTP://93.184.216.34/v1", + "--config-accept-new-path", + ]; const commandDispatchDeferred = deferred(); const validateName = vi.fn(); const configSet = vi.fn(); const runOclifArgv = vi.fn(async () => { throw new Error("config set should dispatch by command id"); }); - const runOclifCommandById = vi.fn(async (commandId: string, args: string[]) => { - expect(commandId).toBe("sandbox:config:set"); - expect(args).toEqual([ - "test-sandbox", - "--key", - "inference.endpoints", - "--value", - "HTTP://93.184.216.34/v1", - "--config-accept-new-path", - ]); - return commandDispatchDeferred.promise; - }); + const runOclifCommandById = vi.fn(async () => commandDispatchDeferred.promise); process.env.NEMOCLAW_DISABLE_AUTO_DISPATCH = "1"; @@ -139,6 +136,15 @@ describe("config set CLI dispatch", () => { }); expect(runOclifArgv).not.toHaveBeenCalled(); expect(runOclifCommandById).toHaveBeenCalledTimes(1); + expect(runOclifCommandById).toHaveBeenCalledWith( + "sandbox:config:set", + expectedConfigSetDispatchArgs, + expect.objectContaining({ + error: expect.any(Function), + exit: expect.any(Function), + rootDir: process.cwd(), + }), + ); expect(configSet).not.toHaveBeenCalled(); expect(settled).toBe(false); From 8c3b7734840c6cad98ae4f286ee873001400f8df Mon Sep 17 00:00:00 2001 From: Chengjie Wang Date: Tue, 7 Jul 2026 03:03:52 +0800 Subject: [PATCH 11/37] test(openclaw): avoid appended tui capture --- test/e2e/live/issue-6194-tui-expect.ts | 2 +- test/e2e/support/issue-6194-tui-post-idle-contract.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index 4463fb7574..43af91d8bd 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -8,7 +8,7 @@ export function buildIssue6194TuiExpectScript(): string { return `set timeout $env(NEMOCLAW_ISSUE_6194_TUI_TIMEOUT) set sandbox $env(NEMOCLAW_ISSUE_6194_SANDBOX) set capture $env(NEMOCLAW_ISSUE_6194_CAPTURE) -log_file -a $capture +log_file -noappend $capture proc mark {name} { puts "ISSUE6194_MARK $name" send_log "ISSUE6194_MARK $name\\n" diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index 0b2072bbbe..dd3f1001b2 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -15,6 +15,7 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect(ISSUE6194_TUI_TIMEOUT_SEC).toBe(240); expect(ISSUE6194_TUI_SESSION).toBe("test-session"); + expect(script).toContain("log_file -noappend $capture"); expect(script).toContain("spawn openshell sandbox exec --name $sandbox --tty"); expect(script).toContain("openclaw tui --session test-session"); expect(script).toContain('puts "ISSUE6194_MARK $name"'); From 02f22f40d42af0501d1065278577772cfb60c80b Mon Sep 17 00:00:00 2001 From: Chengjie Wang Date: Tue, 7 Jul 2026 03:15:09 +0800 Subject: [PATCH 12/37] test(openclaw): use sandbox access env for tui expect --- test/e2e/live/openclaw-tui-chat-correlation.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/e2e/live/openclaw-tui-chat-correlation.test.ts b/test/e2e/live/openclaw-tui-chat-correlation.test.ts index bf93ac67e9..6a4cc703d1 100644 --- a/test/e2e/live/openclaw-tui-chat-correlation.test.ts +++ b/test/e2e/live/openclaw-tui-chat-correlation.test.ts @@ -19,7 +19,11 @@ import { join } from "node:path"; import { containsReplyTokenAllowingWhitespace } from "../../helpers/e2e-answer-assertions.ts"; import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts"; -import { type SandboxClient, trustedSandboxShellScript } from "../fixtures/clients/sandbox.ts"; +import { + sandboxAccessEnv, + type SandboxClient, + trustedSandboxShellScript, +} from "../fixtures/clients/sandbox.ts"; import { expect, test } from "../fixtures/e2e-test.ts"; import type { NemoClawInstance } from "../fixtures/phases/onboarding.ts"; import { ubuntuRepoDocker } from "../registry/matrix.ts"; @@ -599,7 +603,7 @@ test( const tui = await host.command("expect", [expectScript], { artifactName: "issue6194-openclaw-tui-post-idle", env: { - ...buildAvailabilityProbeEnv(), + ...sandboxAccessEnv(), NEMOCLAW_ISSUE_6194_SANDBOX: instance.sandboxName, NEMOCLAW_ISSUE_6194_CAPTURE: captureFile, NEMOCLAW_ISSUE_6194_TUI_TIMEOUT: String(ISSUE6194_TUI_TIMEOUT_SEC), From 8bf393a144c472424ae0cb3ba9e8f6ef33abbff4 Mon Sep 17 00:00:00 2001 From: Chengjie Wang Date: Tue, 7 Jul 2026 03:32:14 +0800 Subject: [PATCH 13/37] test(openclaw): factor issue 6194 expect helper --- test/e2e/live/issue-6194-tui-expect.ts | 62 +++++-------------- .../issue-6194-tui-post-idle-contract.test.ts | 23 +++++-- 2 files changed, 32 insertions(+), 53 deletions(-) diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index 43af91d8bd..35975906d9 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -13,49 +13,24 @@ proc mark {name} { puts "ISSUE6194_MARK $name" send_log "ISSUE6194_MARK $name\\n" } -spawn openshell sandbox exec --name $sandbox --tty -- sh -lc {export TERM=xterm-256color; cd /sandbox; openclaw tui --session ${ISSUE6194_TUI_SESSION}} -expect { - -nocase -re {connected[^\\r\\n]*idle} { mark connected_idle_initial } - timeout { - send "\\003" - exit 10 +proc expect_or_exit {pattern markName timeoutExit eofExit} { + expect { + -nocase -re $pattern { mark $markName } + timeout { + send "\\003" + exit $timeoutExit + } + eof { exit $eofExit } } - eof { exit 11 } } +spawn openshell sandbox exec --name $sandbox --tty -- sh -lc {export TERM=xterm-256color; cd /sandbox; openclaw tui --session ${ISSUE6194_TUI_SESSION}} +expect_or_exit {connected[^\\r\\n]*idle} connected_idle_initial 10 11 send -- "Reply with the three fragments joined by underscores: NEMOCLAW6194, CHAT, OK. Put only that joined token on its own line. Do not use tools.\\r" -expect { - -nocase -re {NEMOCLAW6194_CHAT_OK} { mark chat_reply } - timeout { - send "\\003" - exit 20 - } - eof { exit 21 } -} -expect { - -nocase -re {connected[^\\r\\n]*idle} { mark connected_idle_after_chat } - timeout { - send "\\003" - exit 22 - } - eof { exit 23 } -} +expect_or_exit {NEMOCLAW6194_CHAT_OK} chat_reply 20 21 +expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_chat 22 23 send -- "/nemoclaw status\\r" -expect { - -nocase -re "Sandbox:[^\\r\\n]*$sandbox" { mark slash_status_output } - timeout { - send "\\003" - exit 30 - } - eof { exit 31 } -} -expect { - -nocase -re {connected[^\\r\\n]*idle} { mark connected_idle_after_status } - timeout { - send "\\003" - exit 32 - } - eof { exit 33 } -} +expect_or_exit "Sandbox:[^\\r\\n]*$sandbox" slash_status_output 30 31 +expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_status 32 33 send -- "Use an available tool to call https://api.atlassian.com/oauth/token/accessible-resources now. Do not describe it.\\r" expect { -nocase -re {(blocked|denied|rejected)} { @@ -84,14 +59,7 @@ expect { } eof { exit 55 } } -expect { - -nocase -re {connected[^\\r\\n]*idle} { mark connected_idle_after_network_approval } - timeout { - send "\\003" - exit 56 - } - eof { exit 57 } -} +expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_network_approval 56 57 send "\\003" expect { eof { diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index dd3f1001b2..6280791de3 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -20,17 +20,28 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect(script).toContain("openclaw tui --session test-session"); expect(script).toContain('puts "ISSUE6194_MARK $name"'); expect(script).toContain('send_log "ISSUE6194_MARK $name\\n"'); - expect(script).toContain("mark connected_idle_initial"); - expect(script).toContain("mark chat_reply"); - expect(script).toContain("mark connected_idle_after_chat"); + expect(script).toContain("proc expect_or_exit"); + expect(script).toContain( + "expect_or_exit {connected[^\\r\\n]*idle} connected_idle_initial 10 11", + ); + expect(script).toContain("expect_or_exit {NEMOCLAW6194_CHAT_OK} chat_reply 20 21"); + expect(script).toContain( + "expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_chat 22 23", + ); expect(script).toContain("/nemoclaw status"); expect(script).toContain("Sandbox:[^\\r\\n]*$sandbox"); - expect(script).toContain("mark slash_status_output"); - expect(script).toContain("mark connected_idle_after_status"); + expect(script).toContain( + 'expect_or_exit "Sandbox:[^\\r\\n]*$sandbox" slash_status_output 30 31', + ); + expect(script).toContain( + "expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_status 32 33", + ); expect(script).toContain("https://api.atlassian.com/oauth/token/accessible-resources"); expect(script).toContain("mark network_approval_prompt"); expect(script).toContain("mark network_approval_processed"); - expect(script).toContain("mark connected_idle_after_network_approval"); + expect(script).toContain( + "expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_network_approval 56 57", + ); expect(script).toContain("mark clean_exit"); expect(script).not.toContain("NEMOCLAW_ISSUE_6194_TUI_TIMEOUT_SEC"); expect(script).not.toContain("(nemoclaw|sandbox|docker|status|managed|openclaw)"); From ed931570f886dae1904789a04fee9be918584bc5 Mon Sep 17 00:00:00 2001 From: Chengjie Wang Date: Tue, 7 Jul 2026 03:42:41 +0800 Subject: [PATCH 14/37] test(openclaw): document issue 6194 repro scope --- test/e2e/live/openclaw-tui-chat-correlation.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/e2e/live/openclaw-tui-chat-correlation.test.ts b/test/e2e/live/openclaw-tui-chat-correlation.test.ts index 6a4cc703d1..18564f7788 100644 --- a/test/e2e/live/openclaw-tui-chat-correlation.test.ts +++ b/test/e2e/live/openclaw-tui-chat-correlation.test.ts @@ -515,6 +515,8 @@ test( issues: ["#2603", "#3145", "#6194"], ownerIssue: "#4347", pinnedOpenClawVersion: EXPECTED_OPENCLAW_VERSION, + historicalReproScope: + "#6194 reported NemoClaw v0.0.72 as the known-bad release; this live target guards the current branch against the same post-idle TUI regression instead of reinstalling the old bad version.", }); // Setup ──────────────────────────────────────────────────────── @@ -523,9 +525,12 @@ test( sandboxName: SANDBOX_NAME, }); - // Assertion: openclaw-version-pinned. The regression target only - // reproduces against the bundled OpenClaw build; if the sandbox installed - // a different version, the rest of the test is meaningless. + // Assertion: openclaw-version-pinned. The issue reporter used NemoClaw + // v0.0.72 to demonstrate the historical failure. Reinstalling that known + // bad release in PR CI would prove the old bug, not the proposed guard. + // This target provisions the current branch and validates the bundled + // OpenClaw build before exercising the same post-connected-idle terminal + // paths so future changes cannot reintroduce #6194. // // Every sandbox.* call must pass `env: buildAvailabilityProbeEnv()`: // ShellProbe.run spawns with an empty env when none is provided, From cbc0dcf38f6c648bf7335698ed262e6d408ca480 Mon Sep 17 00:00:00 2001 From: Chengjie Wang Date: Tue, 7 Jul 2026 04:00:04 +0800 Subject: [PATCH 15/37] test(openclaw): isolate issue 6194 tui flow --- test/e2e/live/issue-6194-tui-expect.ts | 3 + .../openclaw-tui-chat-correlation.test.ts | 105 +++++++++--------- .../issue-6194-tui-post-idle-contract.test.ts | 3 - 3 files changed, 58 insertions(+), 53 deletions(-) diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index 35975906d9..54330fe70d 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -31,6 +31,9 @@ expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_chat 22 23 send -- "/nemoclaw status\\r" expect_or_exit "Sandbox:[^\\r\\n]*$sandbox" slash_status_output 30 31 expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_status 32 33 +# Use a stable, policy-relevant HTTPS endpoint to trigger the real OpenClaw +# network approval UI. The test only needs the approval prompt; it does not +# depend on a successful third-party response body. send -- "Use an available tool to call https://api.atlassian.com/oauth/token/accessible-resources now. Do not describe it.\\r" expect { -nocase -re {(blocked|denied|rejected)} { diff --git a/test/e2e/live/openclaw-tui-chat-correlation.test.ts b/test/e2e/live/openclaw-tui-chat-correlation.test.ts index 18564f7788..18671595ae 100644 --- a/test/e2e/live/openclaw-tui-chat-correlation.test.ts +++ b/test/e2e/live/openclaw-tui-chat-correlation.test.ts @@ -550,56 +550,11 @@ test( `actual: ${versionResult.stdout}`, ).toContain(EXPECTED_OPENCLAW_VERSION); - // Drive the websocket repro and capture the trace ────────────── - const { repro, attempts } = await runLiveIssue2603ReproWithEventCaptureRetry( - sandbox, - instance.sandboxName, - ); - - await artifacts.writeJson("issue2603-trace.json", { - sentRuns: repro.sentRuns, - eventCount: repro.events?.length ?? 0, - attempts: attempts.length, - error: repro.error, - }); - - if (repro.error) { - throw new Error(`live repro failed before assertions: ${repro.error}`); - } - - const analysis = analyzeIssue2603Trace(repro); - const failureSummary = JSON.stringify( - { - sentRuns: repro.sentRuns, - eventCount: repro.events.length, - analysis, - }, - null, - 2, - ); - - // #2603 protocol/history subset — every submitted run produces a - // non-empty final, every reply correlates to the run that accepted - // the prompt, and observed user turns remain in submitted A/B/C order. - // TUI rendering indicators and visible tool-call status are covered - // outside this websocket-level guard. - expect(analysis.emptyFinalsForSubmittedRuns, failureSummary).toEqual([]); - expect(analysis.uncorrelatedReplies, failureSummary).toEqual([]); - expect(analysis.userTurnOrder, failureSummary).toEqual( - repro.sentRuns.map((entry) => entry.promptToken), - ); - - // #3145 contract — no missing replies, no duplicate replies, no - // out-of-order final replies, and no history corruption (missing or - // duplicated user turns). - expect(analysis.missingReplies, failureSummary).toEqual([]); - expect(analysis.duplicateReplies, failureSummary).toEqual([]); - expect(analysis.finalReplyOrder, failureSummary).toEqual( - repro.sentRuns.map((entry) => entry.replyToken), - ); - expect(analysis.missingUserTurns, failureSummary).toEqual([]); - expect(analysis.duplicateUserTurns, failureSummary).toEqual([]); - + // Drive the #6194 terminal flow before websocket correlation so the + // post-idle TUI regression guard is independent of any gateway/session + // state created by the #2603/#3145 websocket replay below. Keeping both + // flows in this target reuses the same provisioned sandbox and avoids a + // second long cloud setup for a tests-only PR. const captureDir = mkdtempSync(join(tmpdir(), "nemoclaw-issue6194-tui-")); const captureFile = join(captureDir, "openclaw-tui-capture.log"); const expectScript = artifacts.pathFor("issue6194-openclaw-tui.expect"); @@ -677,6 +632,56 @@ test( } finally { rmSync(captureDir, { recursive: true, force: true }); } + + // Drive the websocket repro and capture the trace ────────────── + const { repro, attempts } = await runLiveIssue2603ReproWithEventCaptureRetry( + sandbox, + instance.sandboxName, + ); + + await artifacts.writeJson("issue2603-trace.json", { + sentRuns: repro.sentRuns, + eventCount: repro.events?.length ?? 0, + attempts: attempts.length, + error: repro.error, + }); + + if (repro.error) { + throw new Error(`live repro failed before assertions: ${repro.error}`); + } + + const analysis = analyzeIssue2603Trace(repro); + const failureSummary = JSON.stringify( + { + sentRuns: repro.sentRuns, + eventCount: repro.events.length, + analysis, + }, + null, + 2, + ); + + // #2603 protocol/history subset — every submitted run produces a + // non-empty final, every reply correlates to the run that accepted + // the prompt, and observed user turns remain in submitted A/B/C order. + // TUI rendering indicators and visible tool-call status are covered + // outside this websocket-level guard. + expect(analysis.emptyFinalsForSubmittedRuns, failureSummary).toEqual([]); + expect(analysis.uncorrelatedReplies, failureSummary).toEqual([]); + expect(analysis.userTurnOrder, failureSummary).toEqual( + repro.sentRuns.map((entry) => entry.promptToken), + ); + + // #3145 contract — no missing replies, no duplicate replies, no + // out-of-order final replies, and no history corruption (missing or + // duplicated user turns). + expect(analysis.missingReplies, failureSummary).toEqual([]); + expect(analysis.duplicateReplies, failureSummary).toEqual([]); + expect(analysis.finalReplyOrder, failureSummary).toEqual( + repro.sentRuns.map((entry) => entry.replyToken), + ); + expect(analysis.missingUserTurns, failureSummary).toEqual([]); + expect(analysis.duplicateUserTurns, failureSummary).toEqual([]); }, // 75-minute budget covers cloud onboarding, sandbox provisioning, gateway // warmup, the 120-second wait-for-replies window, and retry. diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index 6280791de3..892fc3d8bd 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -43,9 +43,6 @@ describe("live TUI post-idle coverage contract (#6194)", () => { "expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_network_approval 56 57", ); expect(script).toContain("mark clean_exit"); - expect(script).not.toContain("NEMOCLAW_ISSUE_6194_TUI_TIMEOUT_SEC"); - expect(script).not.toContain("(nemoclaw|sandbox|docker|status|managed|openclaw)"); - expect(script).not.toContain("approval-flow probe"); const order = [ "connected_idle_initial", From 477e9f167f1851624ee48c4bef2c8163cf314f88 Mon Sep 17 00:00:00 2001 From: Chengjie Wang Date: Tue, 7 Jul 2026 04:30:47 +0800 Subject: [PATCH 16/37] test(openclaw): tighten issue 6194 pr feedback --- test/e2e/fixtures/clients/host.ts | 14 +++++++++++++- test/e2e/live/issue-6194-tui-expect.ts | 12 +++++++----- .../live/openclaw-tui-chat-correlation.test.ts | 3 +++ test/e2e/support/e2e-clients.test.ts | 10 ++++++++++ .../issue-6194-tui-post-idle-contract.test.ts | 7 ++++--- test/helpers/require-cache.ts | 12 ++++++++++++ .../cli/config-set-cli-dispatch.test.ts | 18 ++++++++---------- 7 files changed, 57 insertions(+), 19 deletions(-) create mode 100644 test/helpers/require-cache.ts diff --git a/test/e2e/fixtures/clients/host.ts b/test/e2e/fixtures/clients/host.ts index 692676db59..1cc833c055 100644 --- a/test/e2e/fixtures/clients/host.ts +++ b/test/e2e/fixtures/clients/host.ts @@ -17,11 +17,22 @@ export interface HostClientOptions { cwd?: string; } +export type HostCommandRunOptions = ShellProbeRunOptions; + const GATEWAY_ALREADY_ABSENT = /gateway[^\n]*(?:does not exist|not found)|No (?:active )?gateway|No gateway metadata found/i; const GATEWAY_REMOVE_UNSUPPORTED = /unrecognized subcommand ['"]remove['"]|unknown command ['"]remove['"]/i; +function assertTrustedHostCommandOptions(options: HostCommandRunOptions): void { + // `/sandbox` is the remote OpenShell working directory, not a trusted host cwd. + // Sandbox-derived values can still be valid data, such as a sandbox name passed + // to an OpenShell host binary, so callers own that source review at construction. + if (options.cwd === "/sandbox" || options.cwd?.startsWith("/sandbox/")) { + throw new Error("HostCliClient.command cannot run with a remote sandbox cwd"); + } +} + export class HostCliClient { private readonly runner: CommandRunner; private readonly cliPath: string; @@ -40,7 +51,7 @@ export class HostCliClient { command( command: string, args: string[] = [], - options: ShellProbeRunOptions = {}, + options: HostCommandRunOptions = {}, ): Promise { // Trust boundary: this runs trusted host binaries with test-authored // arguments on the CI host. Do not pass sandbox- or user-controlled @@ -50,6 +61,7 @@ export class HostCliClient { if (this.cwd && !merged.cwd) { merged.cwd = this.cwd; } + assertTrustedHostCommandOptions(merged); return this.runner.run( trustedShellCommand({ command, diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index 54330fe70d..9f63069767 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -2,12 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 export const ISSUE6194_TUI_TIMEOUT_SEC = 240; -export const ISSUE6194_TUI_SESSION = "test-session"; +export const ISSUE6194_TUI_SESSION_PREFIX = "issue-6194-tui"; export function buildIssue6194TuiExpectScript(): string { return `set timeout $env(NEMOCLAW_ISSUE_6194_TUI_TIMEOUT) set sandbox $env(NEMOCLAW_ISSUE_6194_SANDBOX) set capture $env(NEMOCLAW_ISSUE_6194_CAPTURE) +set session $env(NEMOCLAW_ISSUE_6194_SESSION) log_file -noappend $capture proc mark {name} { puts "ISSUE6194_MARK $name" @@ -23,7 +24,7 @@ proc expect_or_exit {pattern markName timeoutExit eofExit} { eof { exit $eofExit } } } -spawn openshell sandbox exec --name $sandbox --tty -- sh -lc {export TERM=xterm-256color; cd /sandbox; openclaw tui --session ${ISSUE6194_TUI_SESSION}} +spawn openshell sandbox exec --name $sandbox --tty -- sh -lc "export TERM=xterm-256color; cd /sandbox; openclaw tui --session $session" expect_or_exit {connected[^\\r\\n]*idle} connected_idle_initial 10 11 send -- "Reply with the three fragments joined by underscores: NEMOCLAW6194, CHAT, OK. Put only that joined token on its own line. Do not use tools.\\r" expect_or_exit {NEMOCLAW6194_CHAT_OK} chat_reply 20 21 @@ -31,9 +32,10 @@ expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_chat 22 23 send -- "/nemoclaw status\\r" expect_or_exit "Sandbox:[^\\r\\n]*$sandbox" slash_status_output 30 31 expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_status 32 33 -# Use a stable, policy-relevant HTTPS endpoint to trigger the real OpenClaw -# network approval UI. The test only needs the approval prompt; it does not -# depend on a successful third-party response body. +# Use the same policy-relevant external HTTPS origin from the historical issue +# repro to trigger the real OpenClaw network approval UI. A local endpoint can +# bypass that egress approval boundary; this test only needs the prompt and +# approval handling, not a successful third-party response body. send -- "Use an available tool to call https://api.atlassian.com/oauth/token/accessible-resources now. Do not describe it.\\r" expect { -nocase -re {(blocked|denied|rejected)} { diff --git a/test/e2e/live/openclaw-tui-chat-correlation.test.ts b/test/e2e/live/openclaw-tui-chat-correlation.test.ts index 18671595ae..14d81b9a43 100644 --- a/test/e2e/live/openclaw-tui-chat-correlation.test.ts +++ b/test/e2e/live/openclaw-tui-chat-correlation.test.ts @@ -29,6 +29,7 @@ import type { NemoClawInstance } from "../fixtures/phases/onboarding.ts"; import { ubuntuRepoDocker } from "../registry/matrix.ts"; import { buildIssue6194TuiExpectScript, + ISSUE6194_TUI_SESSION_PREFIX, ISSUE6194_TUI_TIMEOUT_SEC, } from "./issue-6194-tui-expect.ts"; @@ -558,6 +559,7 @@ test( const captureDir = mkdtempSync(join(tmpdir(), "nemoclaw-issue6194-tui-")); const captureFile = join(captureDir, "openclaw-tui-capture.log"); const expectScript = artifacts.pathFor("issue6194-openclaw-tui.expect"); + const tuiSession = `${ISSUE6194_TUI_SESSION_PREFIX}-${instance.sandboxName}-${Date.now()}-${randomUUID()}`; writeFileSync(expectScript, buildIssue6194TuiExpectScript(), { mode: 0o700 }); try { const tui = await host.command("expect", [expectScript], { @@ -566,6 +568,7 @@ test( ...sandboxAccessEnv(), NEMOCLAW_ISSUE_6194_SANDBOX: instance.sandboxName, NEMOCLAW_ISSUE_6194_CAPTURE: captureFile, + NEMOCLAW_ISSUE_6194_SESSION: tuiSession, NEMOCLAW_ISSUE_6194_TUI_TIMEOUT: String(ISSUE6194_TUI_TIMEOUT_SEC), }, redactionValues: [apiKey], diff --git a/test/e2e/support/e2e-clients.test.ts b/test/e2e/support/e2e-clients.test.ts index ce9ba6b40c..1c903410bf 100644 --- a/test/e2e/support/e2e-clients.test.ts +++ b/test/e2e/support/e2e-clients.test.ts @@ -229,6 +229,16 @@ describe("E2E fixture clients", () => { }); }); + it("host client rejects remote sandbox cwd for host commands", () => { + const runner = new FakeRunner(); + const host = new HostCliClient(runner, { cliPath: "nemoclaw" }); + + expect(() => host.command("node", ["--version"], { cwd: "/sandbox/project" })).toThrow( + "HostCliClient.command cannot run with a remote sandbox cwd", + ); + expect(runner.calls).toEqual([]); + }); + it("gateway client delegates through NemoClaw gateway status", async () => { const runner = new FakeRunner(); const host = new HostCliClient(runner, { cliPath: "nemoclaw" }); diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index 892fc3d8bd..f590297a04 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -5,7 +5,7 @@ import { describe, expect, it } from "vitest"; import { buildIssue6194TuiExpectScript, - ISSUE6194_TUI_SESSION, + ISSUE6194_TUI_SESSION_PREFIX, ISSUE6194_TUI_TIMEOUT_SEC, } from "../live/issue-6194-tui-expect.ts"; @@ -14,10 +14,11 @@ describe("live TUI post-idle coverage contract (#6194)", () => { const script = buildIssue6194TuiExpectScript(); expect(ISSUE6194_TUI_TIMEOUT_SEC).toBe(240); - expect(ISSUE6194_TUI_SESSION).toBe("test-session"); + expect(ISSUE6194_TUI_SESSION_PREFIX).toBe("issue-6194-tui"); expect(script).toContain("log_file -noappend $capture"); + expect(script).toContain("set session $env(NEMOCLAW_ISSUE_6194_SESSION)"); expect(script).toContain("spawn openshell sandbox exec --name $sandbox --tty"); - expect(script).toContain("openclaw tui --session test-session"); + expect(script).toContain("openclaw tui --session $session"); expect(script).toContain('puts "ISSUE6194_MARK $name"'); expect(script).toContain('send_log "ISSUE6194_MARK $name\\n"'); expect(script).toContain("proc expect_or_exit"); diff --git a/test/helpers/require-cache.ts b/test/helpers/require-cache.ts new file mode 100644 index 0000000000..06a1b5e2e6 --- /dev/null +++ b/test/helpers/require-cache.ts @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +export type RequireCache = Record; + +export function restoreRequireCache( + requireCache: RequireCache, + modulePath: string, + prior: unknown, +): void { + prior === undefined ? delete requireCache[modulePath] : (requireCache[modulePath] = prior); +} diff --git a/test/package-contract/cli/config-set-cli-dispatch.test.ts b/test/package-contract/cli/config-set-cli-dispatch.test.ts index a41f8c6e4b..f52cc05087 100644 --- a/test/package-contract/cli/config-set-cli-dispatch.test.ts +++ b/test/package-contract/cli/config-set-cli-dispatch.test.ts @@ -4,13 +4,11 @@ import { createRequire } from "node:module"; import { describe, expect, it, vi } from "vitest"; +import { restoreRequireCache } from "../../helpers/require-cache.ts"; + const require = createRequire(import.meta.url); const requireCache: Record = require.cache as any; -function restoreRequireCache(modulePath: string, prior: unknown): void { - prior === undefined ? delete requireCache[modulePath] : (requireCache[modulePath] = prior); -} - function deferred() { let resolve!: (value: T | PromiseLike) => void; const promise = new Promise((res) => { @@ -158,12 +156,12 @@ describe("config set CLI dispatch", () => { process.env.NEMOCLAW_DISABLE_AUTO_DISPATCH = priorDisableAutoDispatch; } - restoreRequireCache(cliPath, priorCli); - restoreRequireCache(publicDispatchPath, priorPublicDispatch); - restoreRequireCache(oclifRunnerPath, priorOclifRunner); - restoreRequireCache(registryPath, priorRegistry); - restoreRequireCache(sandboxConfigPath, priorSandboxConfig); - restoreRequireCache(runnerPath, priorRunner); + restoreRequireCache(requireCache, cliPath, priorCli); + restoreRequireCache(requireCache, publicDispatchPath, priorPublicDispatch); + restoreRequireCache(requireCache, oclifRunnerPath, priorOclifRunner); + restoreRequireCache(requireCache, registryPath, priorRegistry); + restoreRequireCache(requireCache, sandboxConfigPath, priorSandboxConfig); + restoreRequireCache(requireCache, runnerPath, priorRunner); } }); }); From ba4a7992cc7ecc7027b955f54713a5d5beb9f1c4 Mon Sep 17 00:00:00 2001 From: Chengjie Wang Date: Tue, 7 Jul 2026 04:42:38 +0800 Subject: [PATCH 17/37] test(openclaw): cover issue 6194 artifact redaction --- test/e2e/live/issue-6194-tui-expect.ts | 5 +++- .../openclaw-tui-chat-correlation.test.ts | 9 +++---- .../issue-6194-tui-post-idle-contract.test.ts | 24 ++++++++++++++++++- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index 9f63069767..1aba7951d4 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -3,12 +3,15 @@ export const ISSUE6194_TUI_TIMEOUT_SEC = 240; export const ISSUE6194_TUI_SESSION_PREFIX = "issue-6194-tui"; +export const ISSUE6194_NETWORK_APPROVAL_ENDPOINT = + "https://api.atlassian.com/oauth/token/accessible-resources"; export function buildIssue6194TuiExpectScript(): string { return `set timeout $env(NEMOCLAW_ISSUE_6194_TUI_TIMEOUT) set sandbox $env(NEMOCLAW_ISSUE_6194_SANDBOX) set capture $env(NEMOCLAW_ISSUE_6194_CAPTURE) set session $env(NEMOCLAW_ISSUE_6194_SESSION) +set networkEndpoint $env(NEMOCLAW_ISSUE_6194_NETWORK_ENDPOINT) log_file -noappend $capture proc mark {name} { puts "ISSUE6194_MARK $name" @@ -36,7 +39,7 @@ expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_status 32 33 # repro to trigger the real OpenClaw network approval UI. A local endpoint can # bypass that egress approval boundary; this test only needs the prompt and # approval handling, not a successful third-party response body. -send -- "Use an available tool to call https://api.atlassian.com/oauth/token/accessible-resources now. Do not describe it.\\r" +send -- "Use an available tool to call $networkEndpoint now. Do not describe it.\\r" expect { -nocase -re {(blocked|denied|rejected)} { send "\\003" diff --git a/test/e2e/live/openclaw-tui-chat-correlation.test.ts b/test/e2e/live/openclaw-tui-chat-correlation.test.ts index 14d81b9a43..3f693aa343 100644 --- a/test/e2e/live/openclaw-tui-chat-correlation.test.ts +++ b/test/e2e/live/openclaw-tui-chat-correlation.test.ts @@ -27,8 +27,10 @@ import { import { expect, test } from "../fixtures/e2e-test.ts"; import type { NemoClawInstance } from "../fixtures/phases/onboarding.ts"; import { ubuntuRepoDocker } from "../registry/matrix.ts"; +import { stripTerminalControl } from "../support/issue-4434-tui-capture.ts"; import { buildIssue6194TuiExpectScript, + ISSUE6194_NETWORK_APPROVAL_ENDPOINT, ISSUE6194_TUI_SESSION_PREFIX, ISSUE6194_TUI_TIMEOUT_SEC, } from "./issue-6194-tui-expect.ts"; @@ -110,12 +112,6 @@ function resultText(result: CommandResultText): string { return [result.stdout, result.stderr].filter(Boolean).join("\n"); } -function stripTerminalControl(value: string): string { - return value - .replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)/gu, "") - .replace(/\u001b\[[0-?]*[ -/]*[@-~]/gu, ""); -} - function textFromContent(content: unknown): string { if (typeof content === "string") return content; if (!Array.isArray(content)) return ""; @@ -568,6 +564,7 @@ test( ...sandboxAccessEnv(), NEMOCLAW_ISSUE_6194_SANDBOX: instance.sandboxName, NEMOCLAW_ISSUE_6194_CAPTURE: captureFile, + NEMOCLAW_ISSUE_6194_NETWORK_ENDPOINT: ISSUE6194_NETWORK_APPROVAL_ENDPOINT, NEMOCLAW_ISSUE_6194_SESSION: tuiSession, NEMOCLAW_ISSUE_6194_TUI_TIMEOUT: String(ISSUE6194_TUI_TIMEOUT_SEC), }, diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index f590297a04..3a787f7c9e 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -5,9 +5,12 @@ import { describe, expect, it } from "vitest"; import { buildIssue6194TuiExpectScript, + ISSUE6194_NETWORK_APPROVAL_ENDPOINT, ISSUE6194_TUI_SESSION_PREFIX, ISSUE6194_TUI_TIMEOUT_SEC, } from "../live/issue-6194-tui-expect.ts"; +import { SecretStore } from "../fixtures/secrets.ts"; +import { stripTerminalControl } from "./issue-4434-tui-capture.ts"; describe("live TUI post-idle coverage contract (#6194)", () => { it("builds an expect flow for chat, slash status, network approval, and clean exit", () => { @@ -15,8 +18,12 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect(ISSUE6194_TUI_TIMEOUT_SEC).toBe(240); expect(ISSUE6194_TUI_SESSION_PREFIX).toBe("issue-6194-tui"); + expect(ISSUE6194_NETWORK_APPROVAL_ENDPOINT).toBe( + "https://api.atlassian.com/oauth/token/accessible-resources", + ); expect(script).toContain("log_file -noappend $capture"); expect(script).toContain("set session $env(NEMOCLAW_ISSUE_6194_SESSION)"); + expect(script).toContain("set networkEndpoint $env(NEMOCLAW_ISSUE_6194_NETWORK_ENDPOINT)"); expect(script).toContain("spawn openshell sandbox exec --name $sandbox --tty"); expect(script).toContain("openclaw tui --session $session"); expect(script).toContain('puts "ISSUE6194_MARK $name"'); @@ -37,7 +44,7 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect(script).toContain( "expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_status 32 33", ); - expect(script).toContain("https://api.atlassian.com/oauth/token/accessible-resources"); + expect(script).toContain("call $networkEndpoint now"); expect(script).toContain("mark network_approval_prompt"); expect(script).toContain("mark network_approval_processed"); expect(script).toContain( @@ -59,4 +66,19 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect(order.every((index) => index >= 0)).toBe(true); expect([...order].sort((a, b) => a - b)).toEqual(order); }); + + it("redacts secrets from ANSI terminal captures before artifact publication", () => { + const secret = "nvapi-secret-issue-6194"; + const secrets = new SecretStore({ NVIDIA_INFERENCE_API_KEY: secret }, (note?: string) => { + throw new Error(note ?? "unexpected skip"); + }); + const capture = `before \u001b[32m${secret}\u001b[0m after`; + + const redactedCapture = secrets.redact(capture, [secret]); + const plainCapture = stripTerminalControl(redactedCapture); + + expect(redactedCapture).not.toContain(secret); + expect(plainCapture).not.toContain(secret); + expect(plainCapture).toContain("[REDACTED]"); + }); }); From 2fa6a09e03a6234770f781eaf0662cd5e14a95fd Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Wed, 8 Jul 2026 15:31:00 -0700 Subject: [PATCH 18/37] test(openclaw): keep TUI idle coverage scoped Remove unrelated config dispatch and auto-pair commentary changes. Keep the branch focused on issue #6194 live coverage. Co-authored-by: Chengjie Wang Signed-off-by: Apurv Kumaria --- test/helpers/require-cache.ts | 12 --- test/nemoclaw-start.test.ts | 2 +- .../cli/config-set-cli-dispatch.test.ts | 82 +++++-------------- 3 files changed, 23 insertions(+), 73 deletions(-) delete mode 100644 test/helpers/require-cache.ts diff --git a/test/helpers/require-cache.ts b/test/helpers/require-cache.ts deleted file mode 100644 index 06a1b5e2e6..0000000000 --- a/test/helpers/require-cache.ts +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -export type RequireCache = Record; - -export function restoreRequireCache( - requireCache: RequireCache, - modulePath: string, - prior: unknown, -): void { - prior === undefined ? delete requireCache[modulePath] : (requireCache[modulePath] = prior); -} diff --git a/test/nemoclaw-start.test.ts b/test/nemoclaw-start.test.ts index 0ce6c0645c..5efee03d85 100644 --- a/test/nemoclaw-start.test.ts +++ b/test/nemoclaw-start.test.ts @@ -1642,7 +1642,7 @@ describe("nemoclaw-start auto-pair slow-mode keepalive (#4263)", () => { // Shared late-CLI poll timeline: // 1-2: first-time browser pairing request pending. // 3-6: browser paired, nothing pending (watcher converges to slow mode). - // 7-10: late CLI scope upgrade arrives after browser pairing converges. + // 7-10: late CLI scope upgrade arrives. // 11+: cli paired alongside browser. function setupLateCliFixture(prefix: string): { tmpDir: string; diff --git a/test/package-contract/cli/config-set-cli-dispatch.test.ts b/test/package-contract/cli/config-set-cli-dispatch.test.ts index f52cc05087..9a79e6772a 100644 --- a/test/package-contract/cli/config-set-cli-dispatch.test.ts +++ b/test/package-contract/cli/config-set-cli-dispatch.test.ts @@ -4,8 +4,6 @@ import { createRequire } from "node:module"; import { describe, expect, it, vi } from "vitest"; -import { restoreRequireCache } from "../../helpers/require-cache.ts"; - const require = createRequire(import.meta.url); const requireCache: Record = require.cache as any; @@ -18,37 +16,21 @@ function deferred() { } describe("config set CLI dispatch", () => { - it("awaits config set command dispatch before completing the dispatcher", async () => { + it("awaits configSet before completing the dispatcher", async () => { const cliPath = require.resolve("../../../dist/nemoclaw.js"); - const publicDispatchPath = require.resolve("../../../dist/lib/cli/public-dispatch.js"); - const oclifRunnerPath = require.resolve("../../../dist/lib/cli/oclif-runner.js"); const registryPath = require.resolve("../../../dist/lib/state/registry.js"); const sandboxConfigPath = require.resolve("../../../dist/lib/sandbox/config.js"); const runnerPath = require.resolve("../../../dist/lib/runner.js"); const priorCli = require.cache[cliPath]; - const priorPublicDispatch = require.cache[publicDispatchPath]; - const priorOclifRunner = require.cache[oclifRunnerPath]; const priorRegistry = require.cache[registryPath]; const priorSandboxConfig = require.cache[sandboxConfigPath]; const priorRunner = require.cache[runnerPath]; const priorDisableAutoDispatch = process.env.NEMOCLAW_DISABLE_AUTO_DISPATCH; - const expectedConfigSetDispatchArgs = [ - "test-sandbox", - "--key", - "inference.endpoints", - "--value", - "HTTP://93.184.216.34/v1", - "--config-accept-new-path", - ]; - const commandDispatchDeferred = deferred(); + const configSetDeferred = deferred(); const validateName = vi.fn(); - const configSet = vi.fn(); - const runOclifArgv = vi.fn(async () => { - throw new Error("config set should dispatch by command id"); - }); - const runOclifCommandById = vi.fn(async () => commandDispatchDeferred.promise); + const configSet = vi.fn(() => configSetDeferred.promise); process.env.NEMOCLAW_DISABLE_AUTO_DISPATCH = "1"; @@ -70,23 +52,6 @@ describe("config set CLI dispatch", () => { ), } as any; - requireCache[oclifRunnerPath] = { - id: oclifRunnerPath, - filename: oclifRunnerPath, - loaded: true, - exports: new Proxy( - { - runOclifArgv, - runOclifCommandById, - }, - { - get(target, prop) { - return prop in target ? target[prop as keyof typeof target] : vi.fn(); - }, - }, - ), - } as any; - requireCache[registryPath] = { id: registryPath, filename: registryPath, @@ -110,7 +75,6 @@ describe("config set CLI dispatch", () => { try { delete require.cache[cliPath]; - delete require.cache[publicDispatchPath]; const { dispatchCli } = require(cliPath); const dispatchPromise = dispatchCli([ @@ -129,24 +93,17 @@ describe("config set CLI dispatch", () => { settled = true; }); - await vi.waitFor(() => expect(runOclifCommandById).toHaveBeenCalledTimes(1), { - timeout: 4_000, + await vi.waitFor(() => expect(configSet).toHaveBeenCalledTimes(1), { timeout: 4_000 }); + expect(configSet).toHaveBeenCalledTimes(1); + expect(configSet).toHaveBeenCalledWith("test-sandbox", { + key: "inference.endpoints", + value: "HTTP://93.184.216.34/v1", + restart: false, + acceptNewPath: true, }); - expect(runOclifArgv).not.toHaveBeenCalled(); - expect(runOclifCommandById).toHaveBeenCalledTimes(1); - expect(runOclifCommandById).toHaveBeenCalledWith( - "sandbox:config:set", - expectedConfigSetDispatchArgs, - expect.objectContaining({ - error: expect.any(Function), - exit: expect.any(Function), - rootDir: process.cwd(), - }), - ); - expect(configSet).not.toHaveBeenCalled(); expect(settled).toBe(false); - commandDispatchDeferred.resolve(); + configSetDeferred.resolve(); await expect(dispatchPromise).resolves.toBeUndefined(); expect(settled).toBe(true); } finally { @@ -156,12 +113,17 @@ describe("config set CLI dispatch", () => { process.env.NEMOCLAW_DISABLE_AUTO_DISPATCH = priorDisableAutoDispatch; } - restoreRequireCache(requireCache, cliPath, priorCli); - restoreRequireCache(requireCache, publicDispatchPath, priorPublicDispatch); - restoreRequireCache(requireCache, oclifRunnerPath, priorOclifRunner); - restoreRequireCache(requireCache, registryPath, priorRegistry); - restoreRequireCache(requireCache, sandboxConfigPath, priorSandboxConfig); - restoreRequireCache(requireCache, runnerPath, priorRunner); + if (priorCli) requireCache[cliPath] = priorCli; + else delete requireCache[cliPath]; + + if (priorRegistry) requireCache[registryPath] = priorRegistry; + else delete requireCache[registryPath]; + + if (priorSandboxConfig) requireCache[sandboxConfigPath] = priorSandboxConfig; + else delete requireCache[sandboxConfigPath]; + + if (priorRunner) requireCache[runnerPath] = priorRunner; + else delete requireCache[runnerPath]; } }); }); From eceddf6d9bce431ef615a9014a1e4b115d5f9d8f Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Wed, 8 Jul 2026 15:42:39 -0700 Subject: [PATCH 19/37] ci(e2e): install expect for TUI live target Pin the host dependency allowlist and retry contract. This lets the #6194 PTY flow run on GitHub-hosted workers. Co-authored-by: Chengjie Wang Signed-off-by: Apurv Kumaria --- .github/workflows/e2e.yaml | 20 ++++++++++++ ...-host-dependency-workflow-boundary.test.ts | 6 ++++ tools/e2e/workflow-boundary.mts | 31 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index c1761a91a3..d810d10fc3 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -3848,6 +3848,26 @@ jobs: - *dockerhub-auth + # The #6194 terminal regression uses Expect as its PTY driver. Keep this + # privileged host setup inline in the reviewed workflow: the selected + # target ref may consume expect but cannot expand the package allowlist. + - name: Install OpenClaw TUI host dependencies + shell: bash + run: | + set -euo pipefail + for attempt in 1 2 3; do + if sudo apt-get update; then + break + fi + if [ "$attempt" -eq 3 ]; then + echo "::error::apt-get update failed after 3 attempts." >&2 + exit 1 + fi + echo "::warning::apt-get update attempt ${attempt} failed; retrying." >&2 + sleep $((attempt * 5)) + done + sudo apt-get install -y --no-install-recommends expect + - name: Prepare E2E workspace uses: NVIDIA/NemoClaw/.github/actions/prepare-e2e@50281ee84c4a6fc759da95ea28fc0b7d9c378a28 diff --git a/test/e2e/support/e2e-host-dependency-workflow-boundary.test.ts b/test/e2e/support/e2e-host-dependency-workflow-boundary.test.ts index e9cb8d5789..960378141c 100644 --- a/test/e2e/support/e2e-host-dependency-workflow-boundary.test.ts +++ b/test/e2e/support/e2e-host-dependency-workflow-boundary.test.ts @@ -45,6 +45,12 @@ describe("inline E2E host dependency boundary", () => { expected: "issue-4434-tui-unreachable-inference host dependency install must be exactly 'sudo apt-get install -y --no-install-recommends expect iptables'", }, + { + jobName: "openclaw-tui-chat-correlation", + stepName: "Install OpenClaw TUI host dependencies", + expected: + "openclaw-tui-chat-correlation host dependency install must be exactly 'sudo apt-get install -y --no-install-recommends expect'", + }, ])("rejects package allowlist drift in $jobName", ({ jobName, stepName, expected }) => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "e2e-workflow-apt-allowlist-")); const workflowPath = path.join(tmp, "workflow.yaml"); diff --git a/tools/e2e/workflow-boundary.mts b/tools/e2e/workflow-boundary.mts index 909c27938d..5b9d50116d 100644 --- a/tools/e2e/workflow-boundary.mts +++ b/tools/e2e/workflow-boundary.mts @@ -885,6 +885,36 @@ function validateIssue4434HostDependencies(errors: string[], jobs: WorkflowRecor ); } +function validateOpenclawTuiChatCorrelationHostDependencies( + errors: string[], + jobs: WorkflowRecord, +): void { + const jobName = "openclaw-tui-chat-correlation"; + const job = asRecord(jobs[jobName]); + if (Object.keys(job).length === 0) { + errors.push(`workflow missing ${jobName} job`); + return; + } + const steps = asSteps(job.steps); + validateInlineHostDependencyInstall( + errors, + jobName, + steps, + "Install OpenClaw TUI host dependencies", + ["expect"], + ); + const install = requireJobStep( + errors, + jobName, + steps, + "Install OpenClaw TUI host dependencies", + ); + const prepare = requireJobStep(errors, jobName, steps, "Prepare E2E workspace"); + if (install && prepare && steps.indexOf(install) >= steps.indexOf(prepare)) { + errors.push(`${jobName} host dependencies must be installed before workspace prep`); + } +} + function validateCommonEgressAgentJob(errors: string[], jobs: WorkflowRecord): void { const jobName = "common-egress-agent"; const job = asRecord(jobs[jobName]); @@ -3821,6 +3851,7 @@ export function validateE2eWorkflowBoundary(workflowPath = DEFAULT_E2E_WORKFLOW_ "issue-4434-tui-unreachable-inference", ); validateIssue4434HostDependencies(errors, jobs); + validateOpenclawTuiChatCorrelationHostDependencies(errors, jobs); validateDiagnosticsJob(errors, jobs); validateModelRouterProviderRoutedInferenceJob(errors, jobs); validateSnapshotCommandsJob(errors, jobs); From bedfc31e8c6623eee90e9a0a415cab024a98d805 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Wed, 8 Jul 2026 15:59:13 -0700 Subject: [PATCH 20/37] test(openclaw): harden TUI idle live proof Co-authored-by: Chengjie Wang Signed-off-by: Apurv Kumaria --- .github/workflows/e2e.yaml | 7 +++++ test/e2e/fixtures/clients/host.ts | 18 +------------ test/e2e/live/issue-6194-tui-expect.ts | 26 +++++++++---------- .../openclaw-tui-chat-correlation.test.ts | 20 +++++++++++--- test/e2e/support/e2e-clients.test.ts | 10 ------- ...-host-dependency-workflow-boundary.test.ts | 24 +++++++++++++++++ .../issue-6194-tui-post-idle-contract.test.ts | 20 ++++++++------ 7 files changed, 73 insertions(+), 52 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index d810d10fc3..4022a0bebf 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -3851,6 +3851,13 @@ jobs: # The #6194 terminal regression uses Expect as its PTY driver. Keep this # privileged host setup inline in the reviewed workflow: the selected # target ref may consume expect but cannot expand the package allowlist. + # invalidState: hosted-runner Ubuntu mirrors can fail transiently during update. + # sourceBoundary: only this trusted workflow chooses the exact root-installed package. + # whyNotSourceFix: GitHub's Ubuntu image and configured repository move together, so a + # fixed package version would make the target brittle across routine runner refreshes. + # The runner's configured Ubuntu repository is therefore an accepted trust source. + # regressionTest: e2e-host-dependency-workflow-boundary rejects package or order drift. + # removalCondition: remove this step when the hosted image provides Expect itself. - name: Install OpenClaw TUI host dependencies shell: bash run: | diff --git a/test/e2e/fixtures/clients/host.ts b/test/e2e/fixtures/clients/host.ts index fa887d2869..5ab206fcfb 100644 --- a/test/e2e/fixtures/clients/host.ts +++ b/test/e2e/fixtures/clients/host.ts @@ -17,8 +17,6 @@ export interface HostClientOptions { cwd?: string; } -export type HostCommandRunOptions = ShellProbeRunOptions; - const GATEWAY_ALREADY_ABSENT = /gateway[^\n]*(?:does not exist|not found)|No (?:active )?gateway|No gateway metadata found/i; const GATEWAY_REMOVE_UNSUPPORTED = @@ -26,15 +24,6 @@ const GATEWAY_REMOVE_UNSUPPORTED = const FORWARD_ALREADY_ABSENT = /no (?:active )?forward|forward[^\n]*(?:not found|not running)|forward stop[^\n]*not running/i; -function assertTrustedHostCommandOptions(options: HostCommandRunOptions): void { - // `/sandbox` is the remote OpenShell working directory, not a trusted host cwd. - // Sandbox-derived values can still be valid data, such as a sandbox name passed - // to an OpenShell host binary, so callers own that source review at construction. - if (options.cwd === "/sandbox" || options.cwd?.startsWith("/sandbox/")) { - throw new Error("HostCliClient.command cannot run with a remote sandbox cwd"); - } -} - export class HostCliClient { private readonly runner: CommandRunner; private readonly cliPath: string; @@ -53,17 +42,12 @@ export class HostCliClient { command( command: string, args: string[] = [], - options: HostCommandRunOptions = {}, + options: ShellProbeRunOptions = {}, ): Promise { - // Trust boundary: this runs trusted host binaries with test-authored - // arguments on the CI host. Do not pass sandbox- or user-controlled - // script content here; sandbox interaction should stay mediated by - // OpenShell/ShellProbe redaction. const merged: ShellProbeRunOptions = { ...options }; if (this.cwd && !merged.cwd) { merged.cwd = this.cwd; } - assertTrustedHostCommandOptions(merged); return this.runner.run( trustedShellCommand({ command, diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index 1aba7951d4..5896f4c090 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -33,19 +33,21 @@ send -- "Reply with the three fragments joined by underscores: NEMOCLAW6194, CHA expect_or_exit {NEMOCLAW6194_CHAT_OK} chat_reply 20 21 expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_chat 22 23 send -- "/nemoclaw status\\r" -expect_or_exit "Sandbox:[^\\r\\n]*$sandbox" slash_status_output 30 31 +set slashStatusPattern [format {Sandbox:[^\\r\\n]*%s} $sandbox] +expect_or_exit $slashStatusPattern slash_status_output 30 31 expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_status 32 33 -# Use the same policy-relevant external HTTPS origin from the historical issue -# repro to trigger the real OpenClaw network approval UI. A local endpoint can -# bypass that egress approval boundary; this test only needs the prompt and -# approval handling, not a successful third-party response body. +# Use a public HTTPS origin outside the target's baseline policy to trigger the +# real OpenClaw network approval UI. A local endpoint can bypass that boundary. +# The assertion consumes only the local prompt and approval state, never the +# remote status or body, so endpoint availability is not a test oracle. send -- "Use an available tool to call $networkEndpoint now. Do not describe it.\\r" +set networkApprovalPattern [format {(Sandbox:[^\\r\\n]*%s[^\\r\\n]*Network Rules[^\\r\\n]*(approve|allow)|Network Rules[^\\r\\n]*(approve|allow)[^\\r\\n]*Sandbox:[^\\r\\n]*%s)} $sandbox $sandbox] expect { -nocase -re {(blocked|denied|rejected)} { send "\\003" exit 50 } - -nocase -re "(Sandbox:[^\\r\\n]*$sandbox[^\\r\\n]*(Network Rules|pending|approve)|(Network Rules|pending|approve)[^\\r\\n]*Sandbox:[^\\r\\n]*$sandbox)" { mark network_approval_prompt } + -nocase -re $networkApprovalPattern { mark network_approval_prompt } timeout { send "\\003" exit 51 @@ -70,20 +72,16 @@ expect { expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_network_approval 56 57 send "\\003" expect { - eof { - mark clean_exit - exit 0 - } + eof {} timeout { send "\\003" expect { - eof { - mark clean_exit - exit 0 - } + eof {} timeout { exit 40 } } } } +mark clean_exit +exit 0 `; } diff --git a/test/e2e/live/openclaw-tui-chat-correlation.test.ts b/test/e2e/live/openclaw-tui-chat-correlation.test.ts index ccd5f10c84..31bebae645 100644 --- a/test/e2e/live/openclaw-tui-chat-correlation.test.ts +++ b/test/e2e/live/openclaw-tui-chat-correlation.test.ts @@ -20,8 +20,8 @@ import { join } from "node:path"; import { containsReplyTokenAllowingWhitespace } from "../../helpers/e2e-answer-assertions.ts"; import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts"; import { - sandboxAccessEnv, type SandboxClient, + sandboxAccessEnv, trustedSandboxShellScript, } from "../fixtures/clients/sandbox.ts"; import { expect, test } from "../fixtures/e2e-test.ts"; @@ -581,8 +581,22 @@ test( ); const combined = `${resultText(tui)}\n${plainCapture}`; - await artifacts.writeText("issue6194-openclaw-tui-capture.log", redactedCapture); - await artifacts.writeText("issue6194-openclaw-tui-capture.plain.log", plainCapture); + const redactedArtifact = await artifacts.writeText( + "issue6194-openclaw-tui-capture.log", + redactedCapture, + ); + const plainArtifact = await artifacts.writeText( + "issue6194-openclaw-tui-capture.plain.log", + plainCapture, + ); + expect( + readFileSync(redactedArtifact, "utf8"), + "published ANSI capture must redact API key", + ).not.toContain(apiKey); + expect( + readFileSync(plainArtifact, "utf8"), + "published plain capture must redact API key", + ).not.toContain(apiKey); await artifacts.writeJson("issue6194-target-result.json", { id: "issue-6194-tui-post-connected-idle", expectExitCode: tui.exitCode, diff --git a/test/e2e/support/e2e-clients.test.ts b/test/e2e/support/e2e-clients.test.ts index b3164a9ad6..44d7d7dd41 100644 --- a/test/e2e/support/e2e-clients.test.ts +++ b/test/e2e/support/e2e-clients.test.ts @@ -257,16 +257,6 @@ describe("E2E fixture clients", () => { }); }); - it("host client rejects remote sandbox cwd for host commands", () => { - const runner = new FakeRunner(); - const host = new HostCliClient(runner, { cliPath: "nemoclaw" }); - - expect(() => host.command("node", ["--version"], { cwd: "/sandbox/project" })).toThrow( - "HostCliClient.command cannot run with a remote sandbox cwd", - ); - expect(runner.calls).toEqual([]); - }); - it("gateway client delegates through NemoClaw gateway status", async () => { const runner = new FakeRunner(); const host = new HostCliClient(runner, { cliPath: "nemoclaw" }); diff --git a/test/e2e/support/e2e-host-dependency-workflow-boundary.test.ts b/test/e2e/support/e2e-host-dependency-workflow-boundary.test.ts index 960378141c..a4c378e051 100644 --- a/test/e2e/support/e2e-host-dependency-workflow-boundary.test.ts +++ b/test/e2e/support/e2e-host-dependency-workflow-boundary.test.ts @@ -65,4 +65,28 @@ describe("inline E2E host dependency boundary", () => { fs.rmSync(tmp, { recursive: true, force: true }); } }); + + it("rejects installing the OpenClaw TUI host dependency after workspace preparation", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "e2e-workflow-host-dependency-order-")); + const workflowPath = path.join(tmp, "workflow.yaml"); + const workflow = readWorkflow(); + const steps = workflow.jobs["openclaw-tui-chat-correlation"]?.steps ?? []; + const installIndex = steps.findIndex( + (step) => step.name === "Install OpenClaw TUI host dependencies", + ); + const prepareIndex = steps.findIndex((step) => step.name === "Prepare E2E workspace"); + if (installIndex < 0 || prepareIndex < 0) { + throw new Error("fixture must contain OpenClaw TUI install and workspace preparation steps"); + } + [steps[installIndex], steps[prepareIndex]] = [steps[prepareIndex]!, steps[installIndex]!]; + fs.writeFileSync(workflowPath, YAML.stringify(workflow)); + + try { + expect(validateE2eWorkflowBoundary(workflowPath)).toContain( + "openclaw-tui-chat-correlation host dependencies must be installed before workspace prep", + ); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } + }); }); diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index 3a787f7c9e..daead62617 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -2,14 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 import { describe, expect, it } from "vitest"; - +import { SecretStore } from "../fixtures/secrets.ts"; import { buildIssue6194TuiExpectScript, ISSUE6194_NETWORK_APPROVAL_ENDPOINT, ISSUE6194_TUI_SESSION_PREFIX, ISSUE6194_TUI_TIMEOUT_SEC, } from "../live/issue-6194-tui-expect.ts"; -import { SecretStore } from "../fixtures/secrets.ts"; import { stripTerminalControl } from "./issue-4434-tui-capture.ts"; describe("live TUI post-idle coverage contract (#6194)", () => { @@ -37,14 +36,15 @@ describe("live TUI post-idle coverage contract (#6194)", () => { "expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_chat 22 23", ); expect(script).toContain("/nemoclaw status"); - expect(script).toContain("Sandbox:[^\\r\\n]*$sandbox"); - expect(script).toContain( - 'expect_or_exit "Sandbox:[^\\r\\n]*$sandbox" slash_status_output 30 31', - ); + expect(script).toContain("set slashStatusPattern [format {Sandbox:[^\\r\\n]*%s} $sandbox]"); + expect(script).toContain("expect_or_exit $slashStatusPattern slash_status_output 30 31"); expect(script).toContain( "expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_status 32 33", ); expect(script).toContain("call $networkEndpoint now"); + expect(script).toContain("set networkApprovalPattern [format"); + expect(script).toContain("Network Rules[^\\r\\n]*(approve|allow)"); + expect(script).toContain("-nocase -re $networkApprovalPattern"); expect(script).toContain("mark network_approval_prompt"); expect(script).toContain("mark network_approval_processed"); expect(script).toContain( @@ -52,7 +52,7 @@ describe("live TUI post-idle coverage contract (#6194)", () => { ); expect(script).toContain("mark clean_exit"); - const order = [ + const markers = [ "connected_idle_initial", "chat_reply", "connected_idle_after_chat", @@ -62,7 +62,11 @@ describe("live TUI post-idle coverage contract (#6194)", () => { "network_approval_processed", "connected_idle_after_network_approval", "clean_exit", - ].map((marker) => script.indexOf(marker)); + ]; + for (const marker of markers) { + expect(script.match(new RegExp(`\\b${marker}\\b`, "gu")) ?? []).toHaveLength(1); + } + const order = markers.map((marker) => script.indexOf(marker)); expect(order.every((index) => index >= 0)).toBe(true); expect([...order].sort((a, b) => a - b)).toEqual(order); }); From 2344f7d46096f47d083ba9cc2d33ce1cd60e8d6e Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Wed, 8 Jul 2026 16:04:07 -0700 Subject: [PATCH 21/37] test(e2e): keep workflow boundary linear Co-authored-by: Chengjie Wang Signed-off-by: Apurv Kumaria --- test/e2e/support/e2e-host-dependency-workflow-boundary.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/e2e/support/e2e-host-dependency-workflow-boundary.test.ts b/test/e2e/support/e2e-host-dependency-workflow-boundary.test.ts index a4c378e051..7246f9e71d 100644 --- a/test/e2e/support/e2e-host-dependency-workflow-boundary.test.ts +++ b/test/e2e/support/e2e-host-dependency-workflow-boundary.test.ts @@ -75,9 +75,6 @@ describe("inline E2E host dependency boundary", () => { (step) => step.name === "Install OpenClaw TUI host dependencies", ); const prepareIndex = steps.findIndex((step) => step.name === "Prepare E2E workspace"); - if (installIndex < 0 || prepareIndex < 0) { - throw new Error("fixture must contain OpenClaw TUI install and workspace preparation steps"); - } [steps[installIndex], steps[prepareIndex]] = [steps[prepareIndex]!, steps[installIndex]!]; fs.writeFileSync(workflowPath, YAML.stringify(workflow)); From 456b612931339c5fda15e709f2a502c6d54eab06 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Wed, 8 Jul 2026 16:11:36 -0700 Subject: [PATCH 22/37] test(openclaw): match rendered slash status Co-authored-by: Chengjie Wang Signed-off-by: Apurv Kumaria --- test/e2e/live/issue-6194-tui-expect.ts | 3 +-- test/e2e/live/openclaw-tui-chat-correlation.test.ts | 3 +++ test/e2e/support/issue-6194-tui-post-idle-contract.test.ts | 3 +-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index 5896f4c090..849d845b82 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -33,8 +33,7 @@ send -- "Reply with the three fragments joined by underscores: NEMOCLAW6194, CHA expect_or_exit {NEMOCLAW6194_CHAT_OK} chat_reply 20 21 expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_chat 22 23 send -- "/nemoclaw status\\r" -set slashStatusPattern [format {Sandbox:[^\\r\\n]*%s} $sandbox] -expect_or_exit $slashStatusPattern slash_status_output 30 31 +expect_or_exit {NemoClaw Status} slash_status_output 30 31 expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_status 32 33 # Use a public HTTPS origin outside the target's baseline policy to trigger the # real OpenClaw network approval UI. A local endpoint can bypass that boundary. diff --git a/test/e2e/live/openclaw-tui-chat-correlation.test.ts b/test/e2e/live/openclaw-tui-chat-correlation.test.ts index 31bebae645..13f4703981 100644 --- a/test/e2e/live/openclaw-tui-chat-correlation.test.ts +++ b/test/e2e/live/openclaw-tui-chat-correlation.test.ts @@ -628,6 +628,9 @@ test( combined, "post-idle slash command must render status output before timeout", ).toContain("ISSUE6194_MARK slash_status_output"); + expect(combined, "rendered status output must include its sandbox field").toMatch( + /NemoClaw Status[\s\S]*Sandbox:/u, + ); expect(combined, "TUI must return to connected idle after /nemoclaw status").toContain( "ISSUE6194_MARK connected_idle_after_status", ); diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index daead62617..bfa2115820 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -36,8 +36,7 @@ describe("live TUI post-idle coverage contract (#6194)", () => { "expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_chat 22 23", ); expect(script).toContain("/nemoclaw status"); - expect(script).toContain("set slashStatusPattern [format {Sandbox:[^\\r\\n]*%s} $sandbox]"); - expect(script).toContain("expect_or_exit $slashStatusPattern slash_status_output 30 31"); + expect(script).toContain("expect_or_exit {NemoClaw Status} slash_status_output 30 31"); expect(script).toContain( "expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_status 32 33", ); From d56841c7ee57c7dbc25ea0534a25e5010e42588d Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Wed, 8 Jul 2026 22:57:23 -0700 Subject: [PATCH 23/37] test(e2e): reuse shared command result helper Signed-off-by: Carlos Villela --- test/e2e/live/openclaw-tui-chat-correlation.test.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/test/e2e/live/openclaw-tui-chat-correlation.test.ts b/test/e2e/live/openclaw-tui-chat-correlation.test.ts index 13f4703981..2218ad3004 100644 --- a/test/e2e/live/openclaw-tui-chat-correlation.test.ts +++ b/test/e2e/live/openclaw-tui-chat-correlation.test.ts @@ -19,6 +19,7 @@ import { join } from "node:path"; import { containsReplyTokenAllowingWhitespace } from "../../helpers/e2e-answer-assertions.ts"; import { buildAvailabilityProbeEnv } from "../fixtures/availability-env.ts"; +import { resultText } from "../fixtures/clients/command.ts"; import { type SandboxClient, sandboxAccessEnv, @@ -106,12 +107,6 @@ type Issue2603Analysis = { }; type LiveIssue2603Trace = Issue2603Trace & { error?: string }; -type CommandResultText = { stdout: string; stderr: string }; - -function resultText(result: CommandResultText): string { - return [result.stdout, result.stderr].filter(Boolean).join("\n"); -} - function textFromContent(content: unknown): string { if (typeof content === "string") return content; if (!Array.isArray(content)) return ""; From 2ca76423784eff8de437c1207c55f30f41d88c59 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 9 Jul 2026 01:08:44 -0700 Subject: [PATCH 24/37] test(openclaw): make network approval proof deterministic Co-authored-by: Chengjie Wang Signed-off-by: Apurv Kumaria --- test/e2e/live/issue-6194-tui-expect.ts | 197 +++++++++++++++--- .../openclaw-tui-chat-correlation.test.ts | 98 +++++++-- .../issue-6194-tui-post-idle-contract.test.ts | 110 ++++++++-- 3 files changed, 340 insertions(+), 65 deletions(-) diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index 849d845b82..a40cb6399f 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -5,13 +5,13 @@ export const ISSUE6194_TUI_TIMEOUT_SEC = 240; export const ISSUE6194_TUI_SESSION_PREFIX = "issue-6194-tui"; export const ISSUE6194_NETWORK_APPROVAL_ENDPOINT = "https://api.atlassian.com/oauth/token/accessible-resources"; +export const ISSUE6194_NETWORK_APPROVAL_HOST = "api.atlassian.com"; export function buildIssue6194TuiExpectScript(): string { return `set timeout $env(NEMOCLAW_ISSUE_6194_TUI_TIMEOUT) set sandbox $env(NEMOCLAW_ISSUE_6194_SANDBOX) set capture $env(NEMOCLAW_ISSUE_6194_CAPTURE) set session $env(NEMOCLAW_ISSUE_6194_SESSION) -set networkEndpoint $env(NEMOCLAW_ISSUE_6194_NETWORK_ENDPOINT) log_file -noappend $capture proc mark {name} { puts "ISSUE6194_MARK $name" @@ -35,52 +35,185 @@ expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_chat 22 23 send -- "/nemoclaw status\\r" expect_or_exit {NemoClaw Status} slash_status_output 30 31 expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_status 32 33 -# Use a public HTTPS origin outside the target's baseline policy to trigger the -# real OpenClaw network approval UI. A local endpoint can bypass that boundary. -# The assertion consumes only the local prompt and approval state, never the -# remote status or body, so endpoint availability is not a test oracle. -send -- "Use an available tool to call $networkEndpoint now. Do not describe it.\\r" -set networkApprovalPattern [format {(Sandbox:[^\\r\\n]*%s[^\\r\\n]*Network Rules[^\\r\\n]*(approve|allow)|Network Rules[^\\r\\n]*(approve|allow)[^\\r\\n]*Sandbox:[^\\r\\n]*%s)} $sandbox $sandbox] +# Network-rule approvals belong to the separate OpenShell terminal UI. Keep +# this OpenClaw TUI regression scoped to inputs it can perform directly so a +# tool-less hosted model cannot turn assistant prose into a test oracle. +send "\\003" expect { - -nocase -re {(blocked|denied|rejected)} { - send "\\003" - exit 50 - } - -nocase -re $networkApprovalPattern { mark network_approval_prompt } + eof {} timeout { send "\\003" - exit 51 + expect { + eof {} + timeout { exit 40 } + } } - eof { exit 52 } } -send -- "a" -after 500 -send -- "y\\r" -expect { - -nocase -re {(approved|allowed|accepted|approval[^\\r\\n]*(processed|granted)|request[^\\r\\n]*(approved|allowed))} { mark network_approval_processed } - -nocase -re {(blocked|denied|rejected)} { - send "\\003" - exit 53 +mark clean_exit +exit 0 +`; +} + +export function buildIssue6194OpenShellApprovalExpectScript(): string { + return `set timeout $env(NEMOCLAW_ISSUE_6194_TUI_TIMEOUT) +set sandbox $env(NEMOCLAW_ISSUE_6194_SANDBOX) +set capture $env(NEMOCLAW_ISSUE_6194_CAPTURE) +set triggerCapture $env(NEMOCLAW_ISSUE_6194_TRIGGER_CAPTURE) +set ruleCapture $env(NEMOCLAW_ISSUE_6194_RULE_CAPTURE) +set networkEndpoint $env(NEMOCLAW_ISSUE_6194_NETWORK_ENDPOINT) +set networkHost $env(NEMOCLAW_ISSUE_6194_NETWORK_HOST) +log_file -noappend $capture +proc mark {name} { + puts "ISSUE6194_MARK $name" + send_log "ISSUE6194_MARK $name\\n" +} +proc stop_spawn {target} { + catch {send -i $target "\\003"} + catch {close -i $target} + catch {wait -i $target} +} +proc write_capture {path value} { + set handle [open $path w] + puts -nonewline $handle $value + close $handle +} +proc expect_or_exit {target pattern markName timeoutExit eofExit} { + expect -i $target { + -nocase -re $pattern { mark $markName } + timeout { + stop_spawn $target + exit $timeoutExit + } + eof { + catch {wait -i $target} + exit $eofExit + } + } +} +proc expect_exact_or_exit {target value markName timeoutExit eofExit} { + expect -i $target { + -nocase -exact $value { mark $markName } + timeout { + stop_spawn $target + exit $timeoutExit + } + eof { + catch {wait -i $target} + exit $eofExit + } } +} +# The OpenShell TUI starts on Gateways. Refuse to navigate by position unless +# this ephemeral target owns exactly one sandbox. +if {[catch {exec openshell sandbox list --names} sandboxNames]} { + puts "ISSUE6194_DIAGNOSTIC sandbox listing failed: $sandboxNames" + exit 60 +} +if {[string trim $sandboxNames] ne $sandbox} { + puts "ISSUE6194_DIAGNOSTIC expected sole sandbox '$sandbox', got: $sandboxNames" + exit 61 +} +mark sole_sandbox_verified +# Do not clear unexpected rules: a pre-existing or concurrent pending request +# must fail this target instead of being hidden or accidentally approved. +if {[catch {exec openshell rule get $sandbox --status pending} pendingBefore]} { + puts "ISSUE6194_DIAGNOSTIC pending-rule preflight failed: $pendingBefore" + exit 62 +} +set expectedEmpty "No network rules for sandbox '$sandbox'" +if {[string trim $pendingBefore] ne $expectedEmpty} { + puts "ISSUE6194_DIAGNOSTIC pending-rule queue was not empty: $pendingBefore" + exit 63 +} +mark pending_queue_empty +spawn openshell term +set termSpawn $spawn_id +expect_exact_or_exit $termSpawn {Sandboxes} openshell_dashboard 64 65 +# Gateways -> Providers -> Sandboxes. +send -i $termSpawn -- "\\t" +after 200 +send -i $termSpawn -- "\\t" +after 200 +expect_exact_or_exit $termSpawn $sandbox openshell_sandbox_listed 66 67 +send -i $termSpawn -- "\\r" +expect_or_exit $termSpawn {(Dashboard-)?Sandbox:} openshell_sandbox_detail 68 69 +expect_exact_or_exit $termSpawn $sandbox openshell_sandbox_detail_name 70 71 +# OpenShell documents 'r' as the Network Rules focus key in sandbox detail. +send -i $termSpawn -- "r" +expect_exact_or_exit $termSpawn {Network Rules} network_rules_focused 72 73 +# The request uses argv boundaries and hard time limits. Its output belongs to +# a separate spawn, so it cannot satisfy any openshell term UI assertion. +spawn -noecho openshell sandbox exec --name $sandbox --no-tty --timeout 40 -- /usr/bin/curl -sS --connect-timeout 5 --max-time 30 -o /dev/null $networkEndpoint +set curlSpawn $spawn_id +mark network_request_triggered +set savedTimeout $timeout +set timeout 45 +expect -i $curlSpawn { + eof { set triggerOutput $expect_out(buffer) } timeout { - send "\\003" - exit 54 + stop_spawn $curlSpawn + stop_spawn $termSpawn + exit 74 } - eof { exit 55 } } -expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_network_approval 56 57 -send "\\003" -expect { +set timeout $savedTimeout +catch {wait -i $curlSpawn} curlWait +write_capture $triggerCapture $triggerOutput +mark network_request_completed +# Poll the supported rule CLI until it proves there is exactly one pending +# chunk and that it belongs to this exact curl/endpoint pair. +set pendingOutput "" +set pendingReady 0 +for {set attempt 0} {$attempt < 20} {incr attempt} { + if {[catch {exec openshell rule get $sandbox --status pending} candidate]} { + set pendingOutput $candidate + } else { + set pendingOutput $candidate + set chunkCount [regexp -all -line {^[[:space:]]*Chunk:} $pendingOutput] + set oneChunk [regexp -nocase {Network Rules:[^\\r\\n]*1 chunk} $pendingOutput] + set pendingStatus [regexp -nocase {Status:[[:space:]]*pending} $pendingOutput] + set curlBinary [regexp {Binary:[[:space:]]*/usr/bin/curl} $pendingOutput] + set expectedEndpoint [expr {[string first $networkHost $pendingOutput] >= 0}] + if {$chunkCount == 1 && $oneChunk && $pendingStatus && $curlBinary && $expectedEndpoint} { + set pendingReady 1 + break + } + } + after 500 +} +write_capture $ruleCapture $pendingOutput +if {!$pendingReady} { + puts "ISSUE6194_DIAGNOSTIC expected one curl pending rule, got: $pendingOutput" + stop_spawn $termSpawn + exit 75 +} +mark network_rule_singleton +# Only the OpenShell term spawn can satisfy these patterns. The trigger output +# and assistant transcript are isolated from this buffer. +expect_exact_or_exit $termSpawn $networkHost network_rule_endpoint 76 77 +send -i $termSpawn -- "\\r" +expect_or_exit $termSpawn {Status:[^\\r\\n]*pending} network_rule_detail 78 79 +expect_or_exit $termSpawn {Binary:[^\\r\\n]*/usr/bin/curl} network_rule_detail_binary 80 81 +expect_exact_or_exit $termSpawn $networkHost network_rule_detail_endpoint 82 83 +expect_or_exit $termSpawn {\\[a\\][^\\r\\n]*Approve} network_rule_approve_action 84 85 +send -i $termSpawn -- "a" +expect_or_exit $termSpawn {Approved '[^']+'[^\\r\\n]*policy v[0-9]+} network_approval_processed 86 87 +send -i $termSpawn -- "q" +expect -i $termSpawn { eof {} timeout { - send "\\003" - expect { + send -i $termSpawn "\\003" + expect -i $termSpawn { eof {} - timeout { exit 40 } + timeout { + stop_spawn $termSpawn + exit 89 + } } } } -mark clean_exit +catch {wait -i $termSpawn} termWait +mark openshell_clean_exit exit 0 `; } diff --git a/test/e2e/live/openclaw-tui-chat-correlation.test.ts b/test/e2e/live/openclaw-tui-chat-correlation.test.ts index 2218ad3004..85a21ed862 100644 --- a/test/e2e/live/openclaw-tui-chat-correlation.test.ts +++ b/test/e2e/live/openclaw-tui-chat-correlation.test.ts @@ -30,8 +30,10 @@ import type { NemoClawInstance } from "../fixtures/phases/onboarding.ts"; import { ubuntuRepoDocker } from "../registry/matrix.ts"; import { stripTerminalControl } from "../support/issue-4434-tui-capture.ts"; import { + buildIssue6194OpenShellApprovalExpectScript, buildIssue6194TuiExpectScript, ISSUE6194_NETWORK_APPROVAL_ENDPOINT, + ISSUE6194_NETWORK_APPROVAL_HOST, ISSUE6194_TUI_SESSION_PREFIX, ISSUE6194_TUI_TIMEOUT_SEC, } from "./issue-6194-tui-expect.ts"; @@ -523,7 +525,9 @@ test( // bad release in PR CI would prove the old bug, not the proposed guard. // This target provisions the current branch and validates the bundled // OpenClaw build before exercising the same post-connected-idle terminal - // paths so future changes cannot reintroduce #6194. + // paths so future changes cannot reintroduce #6194. OpenShell's separate + // terminal UI owns network-rule approvals; this OpenClaw TUI flow must not + // rely on a hosted model choosing a network tool that may not exist. // // Every sandbox.* call must pass `env: buildAvailabilityProbeEnv()`: // ShellProbe.run spawns with an empty env when none is provided, @@ -560,7 +564,6 @@ test( ...sandboxAccessEnv(), NEMOCLAW_ISSUE_6194_SANDBOX: instance.sandboxName, NEMOCLAW_ISSUE_6194_CAPTURE: captureFile, - NEMOCLAW_ISSUE_6194_NETWORK_ENDPOINT: ISSUE6194_NETWORK_APPROVAL_ENDPOINT, NEMOCLAW_ISSUE_6194_SESSION: tuiSession, NEMOCLAW_ISSUE_6194_TUI_TIMEOUT: String(ISSUE6194_TUI_TIMEOUT_SEC), }, @@ -600,11 +603,6 @@ test( connectedIdleAfterChat: combined.includes("ISSUE6194_MARK connected_idle_after_chat"), slashStatusOutput: combined.includes("ISSUE6194_MARK slash_status_output"), connectedIdleAfterStatus: combined.includes("ISSUE6194_MARK connected_idle_after_status"), - networkApprovalPrompt: combined.includes("ISSUE6194_MARK network_approval_prompt"), - networkApprovalProcessed: combined.includes("ISSUE6194_MARK network_approval_processed"), - connectedIdleAfterNetworkApproval: combined.includes( - "ISSUE6194_MARK connected_idle_after_network_approval", - ), cleanExit: combined.includes("ISSUE6194_MARK clean_exit"), }); @@ -629,18 +627,86 @@ test( expect(combined, "TUI must return to connected idle after /nemoclaw status").toContain( "ISSUE6194_MARK connected_idle_after_status", ); + expect(combined, "post-idle Ctrl+C must close the TUI session").toContain( + "ISSUE6194_MARK clean_exit", + ); + + // OpenShell's terminal UI owns network-rule approval. Exercise that + // boundary separately with a direct sandbox curl so hosted models that + // expose no network tools cannot make this assertion nondeterministic. + const approvalCaptureFile = join(captureDir, "openshell-approval-capture.log"); + const triggerCaptureFile = join(captureDir, "openshell-network-trigger.log"); + const ruleCaptureFile = join(captureDir, "openshell-pending-rule.log"); + const approvalExpectScript = artifacts.pathFor("issue6194-openshell-approval.expect"); + writeFileSync(triggerCaptureFile, "", { mode: 0o600 }); + writeFileSync(ruleCaptureFile, "", { mode: 0o600 }); + writeFileSync(approvalExpectScript, buildIssue6194OpenShellApprovalExpectScript(), { + mode: 0o700, + }); + const approval = await host.command("expect", [approvalExpectScript], { + artifactName: "issue6194-openshell-network-approval", + env: { + ...sandboxAccessEnv(), + NEMOCLAW_ISSUE_6194_SANDBOX: instance.sandboxName, + NEMOCLAW_ISSUE_6194_CAPTURE: approvalCaptureFile, + NEMOCLAW_ISSUE_6194_TRIGGER_CAPTURE: triggerCaptureFile, + NEMOCLAW_ISSUE_6194_RULE_CAPTURE: ruleCaptureFile, + NEMOCLAW_ISSUE_6194_NETWORK_ENDPOINT: ISSUE6194_NETWORK_APPROVAL_ENDPOINT, + NEMOCLAW_ISSUE_6194_NETWORK_HOST: ISSUE6194_NETWORK_APPROVAL_HOST, + NEMOCLAW_ISSUE_6194_TUI_TIMEOUT: String(ISSUE6194_TUI_TIMEOUT_SEC), + }, + redactionValues: [apiKey], + timeoutMs: (ISSUE6194_TUI_TIMEOUT_SEC + 30) * 1000, + }); + const rawApprovalCapture = readFileSync(approvalCaptureFile, "utf8"); + const rawTriggerCapture = readFileSync(triggerCaptureFile, "utf8"); + const rawRuleCapture = readFileSync(ruleCaptureFile, "utf8"); + const redactedApprovalCapture = secrets.redact(rawApprovalCapture, [apiKey]); + const redactedTriggerCapture = secrets.redact(rawTriggerCapture, [apiKey]); + const redactedRuleCapture = secrets.redact(rawRuleCapture, [apiKey]); + const plainApprovalCapture = stripTerminalControl(redactedApprovalCapture); + const approvalCombined = `${resultText(approval)}\n${plainApprovalCapture}\n${redactedTriggerCapture}\n${redactedRuleCapture}`; + await artifacts.writeText( + "issue6194-openshell-approval-capture.log", + redactedApprovalCapture, + ); + await artifacts.writeText( + "issue6194-openshell-approval-capture.plain.log", + plainApprovalCapture, + ); + await artifacts.writeText("issue6194-openshell-network-trigger.log", redactedTriggerCapture); + await artifacts.writeText("issue6194-openshell-pending-rule.log", redactedRuleCapture); + await artifacts.writeJson("issue6194-approval-result.json", { + id: "issue-6194-openshell-network-approval", + expectExitCode: approval.exitCode, + pendingQueueEmpty: approvalCombined.includes("ISSUE6194_MARK pending_queue_empty"), + requestTriggered: approvalCombined.includes("ISSUE6194_MARK network_request_triggered"), + requestCompleted: approvalCombined.includes("ISSUE6194_MARK network_request_completed"), + singletonRule: approvalCombined.includes("ISSUE6194_MARK network_rule_singleton"), + rulesFocused: approvalCombined.includes("ISSUE6194_MARK network_rules_focused"), + endpointRendered: approvalCombined.includes("ISSUE6194_MARK network_rule_endpoint"), + detailBinary: approvalCombined.includes("ISSUE6194_MARK network_rule_detail_binary"), + approvalProcessed: approvalCombined.includes("ISSUE6194_MARK network_approval_processed"), + cleanExit: approvalCombined.includes("ISSUE6194_MARK openshell_clean_exit"), + }); + + expect(approval.exitCode, approvalCombined).toBe(0); expect( - combined, - "post-idle network request must present the sandbox approval prompt", - ).toContain("ISSUE6194_MARK network_approval_prompt"); - expect(combined, "post-idle network approval input must be processed").toContain( - "ISSUE6194_MARK network_approval_processed", + approvalCombined, + "direct curl must create exactly one matching pending rule", + ).toContain("ISSUE6194_MARK network_rule_singleton"); + expect(approvalCombined, "OpenShell must render the exact blocked endpoint").toContain( + "ISSUE6194_MARK network_rule_detail_endpoint", ); - expect(combined, "TUI must return to connected idle after network approval input").toContain( - "ISSUE6194_MARK connected_idle_after_network_approval", + expect( + approvalCombined, + "OpenShell must attribute the rule to the direct curl binary", + ).toContain("ISSUE6194_MARK network_rule_detail_binary"); + expect(approvalCombined, "OpenShell approval input must be processed").toContain( + "ISSUE6194_MARK network_approval_processed", ); - expect(combined, "post-idle Ctrl+C must close the TUI session").toContain( - "ISSUE6194_MARK clean_exit", + expect(approvalCombined, "OpenShell terminal must exit cleanly after approval").toContain( + "ISSUE6194_MARK openshell_clean_exit", ); } finally { rmSync(captureDir, { recursive: true, force: true }); diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index bfa2115820..3f0f1a1af5 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -4,25 +4,23 @@ import { describe, expect, it } from "vitest"; import { SecretStore } from "../fixtures/secrets.ts"; import { + buildIssue6194OpenShellApprovalExpectScript, buildIssue6194TuiExpectScript, ISSUE6194_NETWORK_APPROVAL_ENDPOINT, + ISSUE6194_NETWORK_APPROVAL_HOST, ISSUE6194_TUI_SESSION_PREFIX, ISSUE6194_TUI_TIMEOUT_SEC, } from "../live/issue-6194-tui-expect.ts"; import { stripTerminalControl } from "./issue-4434-tui-capture.ts"; describe("live TUI post-idle coverage contract (#6194)", () => { - it("builds an expect flow for chat, slash status, network approval, and clean exit", () => { + it("builds an expect flow for chat, slash status, return to idle, and clean exit", () => { const script = buildIssue6194TuiExpectScript(); expect(ISSUE6194_TUI_TIMEOUT_SEC).toBe(240); expect(ISSUE6194_TUI_SESSION_PREFIX).toBe("issue-6194-tui"); - expect(ISSUE6194_NETWORK_APPROVAL_ENDPOINT).toBe( - "https://api.atlassian.com/oauth/token/accessible-resources", - ); expect(script).toContain("log_file -noappend $capture"); expect(script).toContain("set session $env(NEMOCLAW_ISSUE_6194_SESSION)"); - expect(script).toContain("set networkEndpoint $env(NEMOCLAW_ISSUE_6194_NETWORK_ENDPOINT)"); expect(script).toContain("spawn openshell sandbox exec --name $sandbox --tty"); expect(script).toContain("openclaw tui --session $session"); expect(script).toContain('puts "ISSUE6194_MARK $name"'); @@ -40,15 +38,6 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect(script).toContain( "expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_status 32 33", ); - expect(script).toContain("call $networkEndpoint now"); - expect(script).toContain("set networkApprovalPattern [format"); - expect(script).toContain("Network Rules[^\\r\\n]*(approve|allow)"); - expect(script).toContain("-nocase -re $networkApprovalPattern"); - expect(script).toContain("mark network_approval_prompt"); - expect(script).toContain("mark network_approval_processed"); - expect(script).toContain( - "expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_network_approval 56 57", - ); expect(script).toContain("mark clean_exit"); const markers = [ @@ -57,9 +46,6 @@ describe("live TUI post-idle coverage contract (#6194)", () => { "connected_idle_after_chat", "slash_status_output", "connected_idle_after_status", - "network_approval_prompt", - "network_approval_processed", - "connected_idle_after_network_approval", "clean_exit", ]; for (const marker of markers) { @@ -70,6 +56,96 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect([...order].sort((a, b) => a - b)).toEqual(order); }); + it("drives a direct blocked request through the real OpenShell approval surface", () => { + const script = buildIssue6194OpenShellApprovalExpectScript(); + + expect(ISSUE6194_NETWORK_APPROVAL_ENDPOINT).toBe( + "https://api.atlassian.com/oauth/token/accessible-resources", + ); + expect(ISSUE6194_NETWORK_APPROVAL_HOST).toBe("api.atlassian.com"); + expect(script).toContain("spawn openshell term"); + expect(script).not.toContain("openshell rule clear"); + expect(script).toContain("exec openshell sandbox list --names"); + expect(script).toContain("exec openshell rule get $sandbox --status pending"); + expect(script).toContain("set expectedEmpty \"No network rules for sandbox '$sandbox'\""); + expect(script).toContain("set termSpawn $spawn_id"); + expect(script).toContain( + "expect_exact_or_exit $termSpawn {Sandboxes} openshell_dashboard 64 65", + ); + expect(script.match(/send -i \$termSpawn -- "\\t"/gu) ?? []).toHaveLength(2); + expect(script).toContain('send -i $termSpawn -- "r"'); + expect(script).toContain( + "expect_exact_or_exit $termSpawn {Network Rules} network_rules_focused", + ); + expect(script).toContain( + "spawn -noecho openshell sandbox exec --name $sandbox --no-tty --timeout 40 -- /usr/bin/curl -sS --connect-timeout 5 --max-time 30 -o /dev/null $networkEndpoint", + ); + expect(script).toContain("set curlSpawn $spawn_id"); + expect(script).toContain("expect -i $curlSpawn"); + expect(script).toContain("catch {wait -i $curlSpawn} curlWait"); + expect(script).toContain( + "set chunkCount [regexp -all -line {^[[:space:]]*Chunk:} $pendingOutput]", + ); + expect(script).toContain("Network Rules:[^\\r\\n]*1 chunk"); + expect(script).toContain("Status:[[:space:]]*pending"); + expect(script).toContain("Binary:[[:space:]]*/usr/bin/curl"); + expect(script).toContain( + "expect_or_exit $termSpawn {Status:[^\\r\\n]*pending} network_rule_detail", + ); + expect(script).toContain( + "expect_or_exit $termSpawn {Binary:[^\\r\\n]*/usr/bin/curl} network_rule_detail_binary", + ); + expect(script).toContain( + "expect_or_exit $termSpawn {\\[a\\][^\\r\\n]*Approve} network_rule_approve_action", + ); + expect(script).toContain('send -i $termSpawn -- "a"'); + expect(script).toContain( + "expect_or_exit $termSpawn {Approved '[^']+'[^\\r\\n]*policy v[0-9]+} network_approval_processed", + ); + expect(script).not.toContain('send -i $termSpawn -- "A"'); + expect(script).not.toContain('send -i $termSpawn -- "y"'); + + const markers = [ + "sole_sandbox_verified", + "pending_queue_empty", + "openshell_dashboard", + "openshell_sandbox_listed", + "openshell_sandbox_detail", + "openshell_sandbox_detail_name", + "network_rules_focused", + "network_request_triggered", + "network_request_completed", + "network_rule_singleton", + "network_rule_endpoint", + "network_rule_detail", + "network_rule_detail_binary", + "network_rule_detail_endpoint", + "network_rule_approve_action", + "network_approval_processed", + "openshell_clean_exit", + ]; + const order = markers.map((marker) => script.indexOf(marker)); + expect(order.every((index) => index >= 0)).toBe(true); + expect([...order].sort((a, b) => a - b)).toEqual(order); + }); + + it.each([ + "blocked", + "denied", + "rejected", + ])("does not map assistant prose containing '%s' to the former refusal exit", (word) => { + const tuiScript = buildIssue6194TuiExpectScript(); + const approvalScript = buildIssue6194OpenShellApprovalExpectScript(); + const assistantTranscript = `The request was ${word} because this model has no network tools.`; + + expect(assistantTranscript).toContain(word); + expect(tuiScript).not.toContain("Use an available tool"); + expect(tuiScript).not.toContain("NEMOCLAW_ISSUE_6194_NETWORK_ENDPOINT"); + expect(tuiScript).not.toContain("(blocked|denied|rejected)"); + expect(`${tuiScript}\n${approvalScript}`).not.toMatch(/exit 50\b/u); + expect(approvalScript).not.toContain("assistantTranscript"); + }); + it("redacts secrets from ANSI terminal captures before artifact publication", () => { const secret = "nvapi-secret-issue-6194"; const secrets = new SecretStore({ NVIDIA_INFERENCE_API_KEY: secret }, (note?: string) => { From d1f7a7f2f6701e095e3ebff141fb8c3ab6eb5b2a Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Thu, 9 Jul 2026 01:26:33 -0700 Subject: [PATCH 25/37] test(openclaw): harden TUI exit diagnostics Signed-off-by: Carlos Villela --- test/e2e/live/issue-6194-tui-expect.ts | 24 +++++- .../openclaw-tui-chat-correlation.test.ts | 62 +++++++++------ .../issue-6194-tui-post-idle-contract.test.ts | 78 +++++++++++++++++++ 3 files changed, 141 insertions(+), 23 deletions(-) diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index a40cb6399f..a89e2a0f56 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -1,12 +1,29 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { existsSync, readFileSync, writeFileSync } from "node:fs"; + export const ISSUE6194_TUI_TIMEOUT_SEC = 240; +export const ISSUE6194_TUI_EXIT_TIMEOUT_SEC = 10; export const ISSUE6194_TUI_SESSION_PREFIX = "issue-6194-tui"; export const ISSUE6194_NETWORK_APPROVAL_ENDPOINT = "https://api.atlassian.com/oauth/token/accessible-resources"; export const ISSUE6194_NETWORK_APPROVAL_HOST = "api.atlassian.com"; +export type Issue6194Capture = { + exists: boolean; + contents: string; +}; + +export function precreateIssue6194Capture(path: string): void { + writeFileSync(path, "", { mode: 0o600 }); +} + +export function readIssue6194Capture(path: string): Issue6194Capture { + if (!existsSync(path)) return { exists: false, contents: "" }; + return { exists: true, contents: readFileSync(path, "utf8") }; +} + export function buildIssue6194TuiExpectScript(): string { return `set timeout $env(NEMOCLAW_ISSUE_6194_TUI_TIMEOUT) set sandbox $env(NEMOCLAW_ISSUE_6194_SANDBOX) @@ -39,16 +56,21 @@ expect_or_exit {connected[^\\r\\n]*idle} connected_idle_after_status 32 33 # this OpenClaw TUI regression scoped to inputs it can perform directly so a # tool-less hosted model cannot turn assistant prose into a test oracle. send "\\003" +set savedTimeout $timeout +set timeout ${ISSUE6194_TUI_EXIT_TIMEOUT_SEC} expect { eof {} - timeout { + -nocase -re {press ctrl\\+c again to exit} { + mark exit_confirmation send "\\003" expect { eof {} timeout { exit 40 } } } + timeout { exit 39 } } +set timeout $savedTimeout mark clean_exit exit 0 `; diff --git a/test/e2e/live/openclaw-tui-chat-correlation.test.ts b/test/e2e/live/openclaw-tui-chat-correlation.test.ts index 85a21ed862..4edd9a039b 100644 --- a/test/e2e/live/openclaw-tui-chat-correlation.test.ts +++ b/test/e2e/live/openclaw-tui-chat-correlation.test.ts @@ -36,6 +36,8 @@ import { ISSUE6194_NETWORK_APPROVAL_HOST, ISSUE6194_TUI_SESSION_PREFIX, ISSUE6194_TUI_TIMEOUT_SEC, + precreateIssue6194Capture, + readIssue6194Capture, } from "./issue-6194-tui-expect.ts"; // Reuses the standard ubuntu-repo-docker environment with the @@ -556,6 +558,7 @@ test( const captureFile = join(captureDir, "openclaw-tui-capture.log"); const expectScript = artifacts.pathFor("issue6194-openclaw-tui.expect"); const tuiSession = `${ISSUE6194_TUI_SESSION_PREFIX}-${instance.sandboxName}-${Date.now()}-${randomUUID()}`; + precreateIssue6194Capture(captureFile); writeFileSync(expectScript, buildIssue6194TuiExpectScript(), { mode: 0o700 }); try { const tui = await host.command("expect", [expectScript], { @@ -570,14 +573,10 @@ test( redactionValues: [apiKey], timeoutMs: (ISSUE6194_TUI_TIMEOUT_SEC + 30) * 1000, }); - const rawCapture = readFileSync(captureFile, "utf8"); + const tuiCapture = readIssue6194Capture(captureFile); + const rawCapture = tuiCapture.contents; const redactedCapture = secrets.redact(rawCapture, [apiKey]); const plainCapture = stripTerminalControl(redactedCapture); - expect(plainCapture.length, "TUI expect capture must not be empty").toBeGreaterThan(0); - expect(plainCapture, "TUI expect capture must include expect-script markers").toContain( - "ISSUE6194_MARK", - ); - const combined = `${resultText(tui)}\n${plainCapture}`; const redactedArtifact = await artifacts.writeText( "issue6194-openclaw-tui-capture.log", @@ -587,17 +586,12 @@ test( "issue6194-openclaw-tui-capture.plain.log", plainCapture, ); - expect( - readFileSync(redactedArtifact, "utf8"), - "published ANSI capture must redact API key", - ).not.toContain(apiKey); - expect( - readFileSync(plainArtifact, "utf8"), - "published plain capture must redact API key", - ).not.toContain(apiKey); await artifacts.writeJson("issue6194-target-result.json", { id: "issue-6194-tui-post-connected-idle", expectExitCode: tui.exitCode, + captureExists: tuiCapture.exists, + captureNonEmpty: plainCapture.length > 0, + captureHasMarkers: plainCapture.includes("ISSUE6194_MARK"), connectedIdleInitial: combined.includes("ISSUE6194_MARK connected_idle_initial"), chatReply: combined.includes("ISSUE6194_MARK chat_reply"), connectedIdleAfterChat: combined.includes("ISSUE6194_MARK connected_idle_after_chat"), @@ -606,6 +600,19 @@ test( cleanExit: combined.includes("ISSUE6194_MARK clean_exit"), }); + expect(tuiCapture.exists, "TUI expect capture must exist").toBe(true); + expect(plainCapture.length, "TUI expect capture must not be empty").toBeGreaterThan(0); + expect(plainCapture, "TUI expect capture must include expect-script markers").toContain( + "ISSUE6194_MARK", + ); + expect( + readFileSync(redactedArtifact, "utf8"), + "published ANSI capture must redact API key", + ).not.toContain(apiKey); + expect( + readFileSync(plainArtifact, "utf8"), + "published plain capture must redact API key", + ).not.toContain(apiKey); expect(tui.exitCode, combined).toBe(0); expect(combined, "TUI must reach connected idle before post-idle input").toContain( "ISSUE6194_MARK connected_idle_initial", @@ -638,8 +645,9 @@ test( const triggerCaptureFile = join(captureDir, "openshell-network-trigger.log"); const ruleCaptureFile = join(captureDir, "openshell-pending-rule.log"); const approvalExpectScript = artifacts.pathFor("issue6194-openshell-approval.expect"); - writeFileSync(triggerCaptureFile, "", { mode: 0o600 }); - writeFileSync(ruleCaptureFile, "", { mode: 0o600 }); + precreateIssue6194Capture(approvalCaptureFile); + precreateIssue6194Capture(triggerCaptureFile); + precreateIssue6194Capture(ruleCaptureFile); writeFileSync(approvalExpectScript, buildIssue6194OpenShellApprovalExpectScript(), { mode: 0o700, }); @@ -658,12 +666,12 @@ test( redactionValues: [apiKey], timeoutMs: (ISSUE6194_TUI_TIMEOUT_SEC + 30) * 1000, }); - const rawApprovalCapture = readFileSync(approvalCaptureFile, "utf8"); - const rawTriggerCapture = readFileSync(triggerCaptureFile, "utf8"); - const rawRuleCapture = readFileSync(ruleCaptureFile, "utf8"); - const redactedApprovalCapture = secrets.redact(rawApprovalCapture, [apiKey]); - const redactedTriggerCapture = secrets.redact(rawTriggerCapture, [apiKey]); - const redactedRuleCapture = secrets.redact(rawRuleCapture, [apiKey]); + const approvalCapture = readIssue6194Capture(approvalCaptureFile); + const triggerCapture = readIssue6194Capture(triggerCaptureFile); + const ruleCapture = readIssue6194Capture(ruleCaptureFile); + const redactedApprovalCapture = secrets.redact(approvalCapture.contents, [apiKey]); + const redactedTriggerCapture = secrets.redact(triggerCapture.contents, [apiKey]); + const redactedRuleCapture = secrets.redact(ruleCapture.contents, [apiKey]); const plainApprovalCapture = stripTerminalControl(redactedApprovalCapture); const approvalCombined = `${resultText(approval)}\n${plainApprovalCapture}\n${redactedTriggerCapture}\n${redactedRuleCapture}`; await artifacts.writeText( @@ -679,6 +687,11 @@ test( await artifacts.writeJson("issue6194-approval-result.json", { id: "issue-6194-openshell-network-approval", expectExitCode: approval.exitCode, + approvalCaptureExists: approvalCapture.exists, + approvalCaptureNonEmpty: plainApprovalCapture.length > 0, + triggerCaptureExists: triggerCapture.exists, + ruleCaptureExists: ruleCapture.exists, + ruleCaptureNonEmpty: redactedRuleCapture.length > 0, pendingQueueEmpty: approvalCombined.includes("ISSUE6194_MARK pending_queue_empty"), requestTriggered: approvalCombined.includes("ISSUE6194_MARK network_request_triggered"), requestCompleted: approvalCombined.includes("ISSUE6194_MARK network_request_completed"), @@ -690,6 +703,11 @@ test( cleanExit: approvalCombined.includes("ISSUE6194_MARK openshell_clean_exit"), }); + expect(approvalCapture.exists, "OpenShell approval capture must exist").toBe(true); + expect( + plainApprovalCapture.length, + "OpenShell approval capture must not be empty", + ).toBeGreaterThan(0); expect(approval.exitCode, approvalCombined).toBe(0); expect( approvalCombined, diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index 3f0f1a1af5..c7fc127f08 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -1,6 +1,9 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { SecretStore } from "../fixtures/secrets.ts"; import { @@ -8,8 +11,11 @@ import { buildIssue6194TuiExpectScript, ISSUE6194_NETWORK_APPROVAL_ENDPOINT, ISSUE6194_NETWORK_APPROVAL_HOST, + ISSUE6194_TUI_EXIT_TIMEOUT_SEC, ISSUE6194_TUI_SESSION_PREFIX, ISSUE6194_TUI_TIMEOUT_SEC, + precreateIssue6194Capture, + readIssue6194Capture, } from "../live/issue-6194-tui-expect.ts"; import { stripTerminalControl } from "./issue-4434-tui-capture.ts"; @@ -56,6 +62,26 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect([...order].sort((a, b) => a - b)).toEqual(order); }); + it("confirms the two-step Ctrl+C exit without waiting for the global timeout", () => { + const script = buildIssue6194TuiExpectScript(); + const exitFlow = script.slice(script.indexOf("# Network-rule approvals belong")); + const firstCtrlC = exitFlow.indexOf('send "\\003"'); + const shortTimeout = exitFlow.indexOf(`set timeout ${ISSUE6194_TUI_EXIT_TIMEOUT_SEC}`); + const confirmation = exitFlow.indexOf("press ctrl\\+c again to exit"); + const secondCtrlC = exitFlow.indexOf('send "\\003"', firstCtrlC + 1); + + expect(ISSUE6194_TUI_EXIT_TIMEOUT_SEC).toBe(10); + expect(firstCtrlC).toBeGreaterThanOrEqual(0); + expect(shortTimeout).toBeGreaterThan(firstCtrlC); + expect(confirmation).toBeGreaterThan(shortTimeout); + expect(secondCtrlC).toBeGreaterThan(confirmation); + expect(exitFlow.split('send "\\003"')).toHaveLength(3); + expect(exitFlow).toContain("eof {}"); + expect(exitFlow).toContain("timeout { exit 39 }"); + expect(exitFlow).toContain("timeout { exit 40 }"); + expect(exitFlow).toContain("set timeout $savedTimeout"); + }); + it("drives a direct blocked request through the real OpenShell approval surface", () => { const script = buildIssue6194OpenShellApprovalExpectScript(); @@ -160,4 +186,56 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect(plainCapture).not.toContain(secret); expect(plainCapture).toContain("[REDACTED]"); }); + + it("precreates captures and writes structured diagnostics before capture assertions", () => { + const captureDir = mkdtempSync(join(tmpdir(), "nemoclaw-issue6194-contract-")); + const captureFile = join(captureDir, "capture.log"); + const missingFile = join(captureDir, "missing.log"); + + try { + expect(readIssue6194Capture(missingFile)).toEqual({ exists: false, contents: "" }); + + precreateIssue6194Capture(captureFile); + expect(statSync(captureFile).mode & 0o777).toBe(0o600); + expect(readIssue6194Capture(captureFile)).toEqual({ exists: true, contents: "" }); + + writeFileSync(captureFile, "ISSUE6194_MARK diagnostic"); + expect(readIssue6194Capture(captureFile)).toEqual({ + exists: true, + contents: "ISSUE6194_MARK diagnostic", + }); + + const liveSource = readFileSync( + new URL("../live/openclaw-tui-chat-correlation.test.ts", import.meta.url), + "utf8", + ); + const tuiPrecreate = liveSource.indexOf("precreateIssue6194Capture(captureFile)"); + const tuiCommand = liveSource.indexOf('host.command("expect", [expectScript]'); + const tuiResult = liveSource.indexOf('artifacts.writeJson("issue6194-target-result.json"'); + const tuiCaptureAssertion = liveSource.indexOf( + 'expect(tuiCapture.exists, "TUI expect capture must exist")', + ); + const approvalPrecreate = liveSource.indexOf( + "precreateIssue6194Capture(approvalCaptureFile)", + ); + const approvalCommand = liveSource.indexOf('host.command("expect", [approvalExpectScript]'); + const approvalResult = liveSource.indexOf( + 'artifacts.writeJson("issue6194-approval-result.json"', + ); + const approvalCaptureAssertion = liveSource.indexOf( + 'expect(approvalCapture.exists, "OpenShell approval capture must exist")', + ); + + expect(tuiPrecreate).toBeGreaterThanOrEqual(0); + expect(tuiCommand).toBeGreaterThan(tuiPrecreate); + expect(tuiResult).toBeGreaterThan(tuiCommand); + expect(tuiCaptureAssertion).toBeGreaterThan(tuiResult); + expect(approvalPrecreate).toBeGreaterThan(tuiCaptureAssertion); + expect(approvalCommand).toBeGreaterThan(approvalPrecreate); + expect(approvalResult).toBeGreaterThan(approvalCommand); + expect(approvalCaptureAssertion).toBeGreaterThan(approvalResult); + } finally { + rmSync(captureDir, { recursive: true, force: true }); + } + }); }); From c0afe5727a38f95bfbdfc7d55c5c66707d004000 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Thu, 9 Jul 2026 01:34:12 -0700 Subject: [PATCH 26/37] test(openclaw): avoid capture read race Signed-off-by: Carlos Villela --- test/e2e/live/issue-6194-tui-expect.ts | 12 +++++++++--- .../issue-6194-tui-post-idle-contract.test.ts | 1 + 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index a89e2a0f56..2a31b1c070 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { readFileSync, writeFileSync } from "node:fs"; export const ISSUE6194_TUI_TIMEOUT_SEC = 240; export const ISSUE6194_TUI_EXIT_TIMEOUT_SEC = 10; @@ -20,8 +20,14 @@ export function precreateIssue6194Capture(path: string): void { } export function readIssue6194Capture(path: string): Issue6194Capture { - if (!existsSync(path)) return { exists: false, contents: "" }; - return { exists: true, contents: readFileSync(path, "utf8") }; + try { + return { exists: true, contents: readFileSync(path, "utf8") }; + } catch (error) { + if ((error as { code?: unknown }).code === "ENOENT") { + return { exists: false, contents: "" }; + } + throw error; + } } export function buildIssue6194TuiExpectScript(): string { diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index c7fc127f08..0891020986 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -194,6 +194,7 @@ describe("live TUI post-idle coverage contract (#6194)", () => { try { expect(readIssue6194Capture(missingFile)).toEqual({ exists: false, contents: "" }); + expect(() => readIssue6194Capture("\0")).toThrow(); precreateIssue6194Capture(captureFile); expect(statSync(captureFile).mode & 0o777).toBe(0o600); From d5f4002410ee643f810bd04bd1500777c635ca91 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Thu, 9 Jul 2026 01:40:35 -0700 Subject: [PATCH 27/37] test(openclaw): avoid contract file race Signed-off-by: Carlos Villela --- test/e2e/support/issue-6194-tui-post-idle-contract.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index 0891020986..4572b89708 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -197,10 +197,10 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect(() => readIssue6194Capture("\0")).toThrow(); precreateIssue6194Capture(captureFile); - expect(statSync(captureFile).mode & 0o777).toBe(0o600); expect(readIssue6194Capture(captureFile)).toEqual({ exists: true, contents: "" }); writeFileSync(captureFile, "ISSUE6194_MARK diagnostic"); + expect(statSync(captureFile).mode & 0o777).toBe(0o600); expect(readIssue6194Capture(captureFile)).toEqual({ exists: true, contents: "ISSUE6194_MARK diagnostic", From b692e146b547f5f8f1afea3d3e095d9b4c9a9920 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Thu, 9 Jul 2026 02:09:08 -0700 Subject: [PATCH 28/37] test(e2e): stabilize OpenShell approval PTY Signed-off-by: Carlos Villela --- test/e2e/live/issue-6194-tui-expect.ts | 11 ++++- .../issue-6194-tui-post-idle-contract.test.ts | 42 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index 2a31b1c070..b440787efd 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -5,6 +5,7 @@ import { readFileSync, writeFileSync } from "node:fs"; export const ISSUE6194_TUI_TIMEOUT_SEC = 240; export const ISSUE6194_TUI_EXIT_TIMEOUT_SEC = 10; +export const ISSUE6194_OPENSHELL_DASHBOARD_TIMEOUT_SEC = 30; export const ISSUE6194_TUI_SESSION_PREFIX = "issue-6194-tui"; export const ISSUE6194_NETWORK_APPROVAL_ENDPOINT = "https://api.atlassian.com/oauth/token/accessible-resources"; @@ -98,7 +99,7 @@ proc mark {name} { proc stop_spawn {target} { catch {send -i $target "\\003"} catch {close -i $target} - catch {wait -i $target} + catch {wait -i $target -nowait} } proc write_capture {path value} { set handle [open $path w] @@ -154,9 +155,17 @@ if {[string trim $pendingBefore] ne $expectedEmpty} { exit 63 } mark pending_queue_empty +# ShellProbe has no controlling TTY. Pin the terminal type and Expect-owned +# PTY geometry before OpenShell performs its first full-screen render. +set env(TERM) xterm-256color spawn openshell term set termSpawn $spawn_id +set termPty $spawn_out(slave,name) +stty rows 40 columns 120 < $termPty +set dashboardTimeout $timeout +set timeout ${ISSUE6194_OPENSHELL_DASHBOARD_TIMEOUT_SEC} expect_exact_or_exit $termSpawn {Sandboxes} openshell_dashboard 64 65 +set timeout $dashboardTimeout # Gateways -> Providers -> Sandboxes. send -i $termSpawn -- "\\t" after 200 diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index 4572b89708..254f7e3d82 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -11,6 +11,7 @@ import { buildIssue6194TuiExpectScript, ISSUE6194_NETWORK_APPROVAL_ENDPOINT, ISSUE6194_NETWORK_APPROVAL_HOST, + ISSUE6194_OPENSHELL_DASHBOARD_TIMEOUT_SEC, ISSUE6194_TUI_EXIT_TIMEOUT_SEC, ISSUE6194_TUI_SESSION_PREFIX, ISSUE6194_TUI_TIMEOUT_SEC, @@ -82,6 +83,47 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect(exitFlow).toContain("set timeout $savedTimeout"); }); + it("stabilizes the OpenShell PTY before a bounded dashboard wait", () => { + const script = buildIssue6194OpenShellApprovalExpectScript(); + const term = script.indexOf("set env(TERM) xterm-256color"); + const spawn = script.indexOf("spawn openshell term"); + const spawnId = script.indexOf("set termSpawn $spawn_id"); + const termPty = script.indexOf("set termPty $spawn_out(slave,name)"); + const geometry = script.indexOf("stty rows 40 columns 120 < $termPty"); + const saveTimeout = script.indexOf("set dashboardTimeout $timeout"); + const boundedTimeout = script.indexOf( + `set timeout ${ISSUE6194_OPENSHELL_DASHBOARD_TIMEOUT_SEC}`, + ); + const dashboard = script.indexOf( + "expect_exact_or_exit $termSpawn {Sandboxes} openshell_dashboard 64 65", + ); + const restoreTimeout = script.indexOf("set timeout $dashboardTimeout"); + const stopSpawn = script.slice( + script.indexOf("proc stop_spawn"), + script.indexOf("proc write_capture"), + ); + const setupOrder = [ + term, + spawn, + spawnId, + termPty, + geometry, + saveTimeout, + boundedTimeout, + dashboard, + ]; + + expect(ISSUE6194_OPENSHELL_DASHBOARD_TIMEOUT_SEC).toBe(30); + expect(setupOrder.every((index) => index >= 0)).toBe(true); + expect([...setupOrder].sort((a, b) => a - b)).toEqual(setupOrder); + expect(restoreTimeout).toBeGreaterThan(dashboard); + expect(script).not.toContain("stty -i"); + expect(stopSpawn).toContain("catch {wait -i $target -nowait}"); + expect(stopSpawn).not.toContain("catch {wait -i $target}"); + expect(script).toContain("catch {wait -i $curlSpawn} curlWait"); + expect(script).toContain("catch {wait -i $termSpawn} termWait"); + }); + it("drives a direct blocked request through the real OpenShell approval surface", () => { const script = buildIssue6194OpenShellApprovalExpectScript(); From 56f9ae305ba965f01e01b859b8e43ed1e5a16b71 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Thu, 9 Jul 2026 02:36:50 -0700 Subject: [PATCH 29/37] test(e2e): bind Expect waits to approval PTY Signed-off-by: Carlos Villela --- test/e2e/live/issue-6194-tui-expect.ts | 17 +++++++++++------ .../issue-6194-tui-post-idle-contract.test.ts | 12 +++++++++++- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index b440787efd..e3ce902839 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -107,7 +107,8 @@ proc write_capture {path value} { close $handle } proc expect_or_exit {target pattern markName timeoutExit eofExit} { - expect -i $target { + expect { + -i $target -nocase -re $pattern { mark $markName } timeout { stop_spawn $target @@ -120,8 +121,9 @@ proc expect_or_exit {target pattern markName timeoutExit eofExit} { } } proc expect_exact_or_exit {target value markName timeoutExit eofExit} { - expect -i $target { - -nocase -exact $value { mark $markName } + expect { + -i $target + -nocase -ex $value { mark $markName } timeout { stop_spawn $target exit $timeoutExit @@ -185,7 +187,8 @@ set curlSpawn $spawn_id mark network_request_triggered set savedTimeout $timeout set timeout 45 -expect -i $curlSpawn { +expect { + -i $curlSpawn eof { set triggerOutput $expect_out(buffer) } timeout { stop_spawn $curlSpawn @@ -236,11 +239,13 @@ expect_or_exit $termSpawn {\\[a\\][^\\r\\n]*Approve} network_rule_approve_action send -i $termSpawn -- "a" expect_or_exit $termSpawn {Approved '[^']+'[^\\r\\n]*policy v[0-9]+} network_approval_processed 86 87 send -i $termSpawn -- "q" -expect -i $termSpawn { +expect { + -i $termSpawn eof {} timeout { send -i $termSpawn "\\003" - expect -i $termSpawn { + expect { + -i $termSpawn eof {} timeout { stop_spawn $termSpawn diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index 254f7e3d82..dee474b4c2 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -149,7 +149,7 @@ describe("live TUI post-idle coverage contract (#6194)", () => { "spawn -noecho openshell sandbox exec --name $sandbox --no-tty --timeout 40 -- /usr/bin/curl -sS --connect-timeout 5 --max-time 30 -o /dev/null $networkEndpoint", ); expect(script).toContain("set curlSpawn $spawn_id"); - expect(script).toContain("expect -i $curlSpawn"); + expect(script).toContain("expect {\n -i $curlSpawn\n"); expect(script).toContain("catch {wait -i $curlSpawn} curlWait"); expect(script).toContain( "set chunkCount [regexp -all -line {^[[:space:]]*Chunk:} $pendingOutput]", @@ -197,6 +197,16 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect([...order].sort((a, b) => a - b)).toEqual(order); }); + it("binds multi-pattern waits to their intended Expect spawn (#6194)", () => { + const script = buildIssue6194OpenShellApprovalExpectScript(); + const spawnScopedWaits = script.match(/expect \{\n\s+-i \$(?:target|curlSpawn|termSpawn)\n/gu); + + expect(spawnScopedWaits).toHaveLength(5); + expect(script).not.toMatch(/\bexpect -i \$/u); + expect(script).toContain("-nocase -ex $value { mark $markName }"); + expect(script).not.toContain("-nocase -exact"); + }); + it.each([ "blocked", "denied", From 25bd2e6ba8e8ae2e531374f0b23012a7eab4a732 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Thu, 9 Jul 2026 02:49:05 -0700 Subject: [PATCH 30/37] test(e2e): match stable sandbox detail label Signed-off-by: Carlos Villela --- test/e2e/live/issue-6194-tui-expect.ts | 2 +- test/e2e/support/issue-6194-tui-post-idle-contract.test.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index e3ce902839..e2c7c15318 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -175,7 +175,7 @@ send -i $termSpawn -- "\\t" after 200 expect_exact_or_exit $termSpawn $sandbox openshell_sandbox_listed 66 67 send -i $termSpawn -- "\\r" -expect_or_exit $termSpawn {(Dashboard-)?Sandbox:} openshell_sandbox_detail 68 69 +expect_exact_or_exit $termSpawn {Name:} openshell_sandbox_detail 68 69 expect_exact_or_exit $termSpawn $sandbox openshell_sandbox_detail_name 70 71 # OpenShell documents 'r' as the Network Rules focus key in sandbox detail. send -i $termSpawn -- "r" diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index dee474b4c2..54f38a71b0 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -141,6 +141,10 @@ describe("live TUI post-idle coverage contract (#6194)", () => { "expect_exact_or_exit $termSpawn {Sandboxes} openshell_dashboard 64 65", ); expect(script.match(/send -i \$termSpawn -- "\\t"/gu) ?? []).toHaveLength(2); + expect(script).toContain( + "expect_exact_or_exit $termSpawn {Name:} openshell_sandbox_detail 68 69", + ); + expect(script).not.toContain("(Dashboard-)?Sandbox:"); expect(script).toContain('send -i $termSpawn -- "r"'); expect(script).toContain( "expect_exact_or_exit $termSpawn {Network Rules} network_rules_focused", From 522186cc43f510b782688ac1e51682636a1eb9ff Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Thu, 9 Jul 2026 02:56:21 -0700 Subject: [PATCH 31/37] test(e2e): declare openshell approval boundary --- .../live/openclaw-tui-chat-correlation.test.ts | 6 +++++- .../issue-6194-tui-post-idle-contract.test.ts | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/test/e2e/live/openclaw-tui-chat-correlation.test.ts b/test/e2e/live/openclaw-tui-chat-correlation.test.ts index 4edd9a039b..50641c1a5f 100644 --- a/test/e2e/live/openclaw-tui-chat-correlation.test.ts +++ b/test/e2e/live/openclaw-tui-chat-correlation.test.ts @@ -508,7 +508,11 @@ test( await artifacts.target.declare({ id: "openclaw-tui-chat-correlation", - boundary: ["openclaw-gateway-websocket", "openclaw-tui-terminal-after-connected-idle"], + boundary: [ + "openclaw-gateway-websocket", + "openclaw-tui-terminal-after-connected-idle", + "openshell-network-rule-terminal-approval", + ], issues: ["#2603", "#3145", "#6194"], ownerIssue: "#4347", pinnedOpenClawVersion: EXPECTED_OPENCLAW_VERSION, diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index 54f38a71b0..3092786af0 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -21,6 +21,24 @@ import { import { stripTerminalControl } from "./issue-4434-tui-capture.ts"; describe("live TUI post-idle coverage contract (#6194)", () => { + it("declares every combined OpenClaw and OpenShell live boundary", () => { + const liveSource = readFileSync( + new URL("../live/openclaw-tui-chat-correlation.test.ts", import.meta.url), + "utf8", + ); + const declarationStart = liveSource.indexOf("await artifacts.target.declare({"); + const declarationEnd = liveSource.indexOf(" });", declarationStart); + const declaration = liveSource.slice(declarationStart, declarationEnd); + + expect(declarationStart).toBeGreaterThanOrEqual(0); + expect(declarationEnd).toBeGreaterThan(declarationStart); + expect(declaration).toContain('"openclaw-gateway-websocket"'); + expect(declaration).toContain('"openclaw-tui-terminal-after-connected-idle"'); + expect(declaration).toContain('"openshell-network-rule-terminal-approval"'); + expect(liveSource).toContain('artifactName: "issue6194-openshell-network-approval"'); + expect(liveSource).toContain('artifacts.writeJson("issue6194-approval-result.json"'); + }); + it("builds an expect flow for chat, slash status, return to idle, and clean exit", () => { const script = buildIssue6194TuiExpectScript(); From 6b0e91ad6c43782e93f468455572fff0eafa19b8 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Thu, 9 Jul 2026 03:02:02 -0700 Subject: [PATCH 32/37] test(e2e): normalize approval rule output --- test/e2e/live/issue-6194-tui-expect.ts | 10 ++++++---- .../support/issue-6194-tui-post-idle-contract.test.ts | 8 ++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index e2c7c15318..e9a4c62ec6 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -205,10 +205,12 @@ mark network_request_completed set pendingOutput "" set pendingReady 0 for {set attempt 0} {$attempt < 20} {incr attempt} { - if {[catch {exec openshell rule get $sandbox --status pending} candidate]} { - set pendingOutput $candidate - } else { - set pendingOutput $candidate + set pendingGetFailed [catch {exec openshell rule get $sandbox --status pending} candidate] + # OpenShell may color labels even when output is captured. Strip SGR before + # parsing and publishing the rule evidence so label/value matches remain + # deterministic across runner terminals and OpenShell render modes. + regsub -all {\\x1b\\[[0-9;]*m} $candidate "" pendingOutput + if {!$pendingGetFailed} { set chunkCount [regexp -all -line {^[[:space:]]*Chunk:} $pendingOutput] set oneChunk [regexp -nocase {Network Rules:[^\\r\\n]*1 chunk} $pendingOutput] set pendingStatus [regexp -nocase {Status:[[:space:]]*pending} $pendingOutput] diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index 3092786af0..744fa9f813 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -176,6 +176,14 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect(script).toContain( "set chunkCount [regexp -all -line {^[[:space:]]*Chunk:} $pendingOutput]", ); + const stripRuleSgr = script.indexOf( + 'regsub -all {\\x1b\\[[0-9;]*m} $candidate "" pendingOutput', + ); + const parseRuleLabels = script.indexOf( + "set chunkCount [regexp -all -line {^[[:space:]]*Chunk:} $pendingOutput]", + ); + expect(stripRuleSgr).toBeGreaterThanOrEqual(0); + expect(parseRuleLabels).toBeGreaterThan(stripRuleSgr); expect(script).toContain("Network Rules:[^\\r\\n]*1 chunk"); expect(script).toContain("Status:[[:space:]]*pending"); expect(script).toContain("Binary:[[:space:]]*/usr/bin/curl"); From db9ffcaf073e4ced21710b4699f4cbbdb23f499e Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Thu, 9 Jul 2026 03:16:42 -0700 Subject: [PATCH 33/37] test(e2e): tolerate styled approval acknowledgement Signed-off-by: Carlos Villela --- test/e2e/live/issue-6194-tui-expect.ts | 2 +- test/e2e/support/issue-6194-tui-post-idle-contract.test.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index e9a4c62ec6..ef40b3f69e 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -239,7 +239,7 @@ expect_or_exit $termSpawn {Binary:[^\\r\\n]*/usr/bin/curl} network_rule_detail_b expect_exact_or_exit $termSpawn $networkHost network_rule_detail_endpoint 82 83 expect_or_exit $termSpawn {\\[a\\][^\\r\\n]*Approve} network_rule_approve_action 84 85 send -i $termSpawn -- "a" -expect_or_exit $termSpawn {Approved '[^']+'[^\\r\\n]*policy v[0-9]+} network_approval_processed 86 87 +expect_or_exit $termSpawn {Approved[^\\r\\n]*'[^']+'[^\\r\\n]*policy v[0-9]+} network_approval_processed 86 87 send -i $termSpawn -- "q" expect { -i $termSpawn diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index 744fa9f813..eb15b064f9 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -198,8 +198,9 @@ describe("live TUI post-idle coverage contract (#6194)", () => { ); expect(script).toContain('send -i $termSpawn -- "a"'); expect(script).toContain( - "expect_or_exit $termSpawn {Approved '[^']+'[^\\r\\n]*policy v[0-9]+} network_approval_processed", + "expect_or_exit $termSpawn {Approved[^\\r\\n]*'[^']+'[^\\r\\n]*policy v[0-9]+} network_approval_processed", ); + expect(script).not.toContain("{Approved '[^']+'"); expect(script).not.toContain('send -i $termSpawn -- "A"'); expect(script).not.toContain('send -i $termSpawn -- "y"'); From df8ed1ea15f231c66444235111be834e4c31999d Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Thu, 9 Jul 2026 09:27:21 -0700 Subject: [PATCH 34/37] test(e2e): lock phase artifact ordering Signed-off-by: Carlos Villela --- .../issue-6194-tui-post-idle-contract.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index eb15b064f9..34b012a58d 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -37,6 +37,8 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect(declaration).toContain('"openshell-network-rule-terminal-approval"'); expect(liveSource).toContain('artifactName: "issue6194-openshell-network-approval"'); expect(liveSource).toContain('artifacts.writeJson("issue6194-approval-result.json"'); + expect(liveSource).toContain('artifactName: "live-issue2603-repro"'); + expect(liveSource).toContain('artifacts.writeJson("issue2603-trace.json"'); }); it("builds an expect flow for chat, slash status, return to idle, and clean exit", () => { @@ -309,6 +311,15 @@ describe("live TUI post-idle coverage contract (#6194)", () => { const approvalCaptureAssertion = liveSource.indexOf( 'expect(approvalCapture.exists, "OpenShell approval capture must exist")', ); + const websocketCommand = liveSource.indexOf( + "const { repro, attempts } = await runLiveIssue2603ReproWithEventCaptureRetry", + approvalCaptureAssertion, + ); + const websocketResult = liveSource.indexOf( + 'artifacts.writeJson("issue2603-trace.json"', + websocketCommand, + ); + const websocketAssertion = liveSource.indexOf("if (repro.error)", websocketResult); expect(tuiPrecreate).toBeGreaterThanOrEqual(0); expect(tuiCommand).toBeGreaterThan(tuiPrecreate); @@ -318,6 +329,9 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect(approvalCommand).toBeGreaterThan(approvalPrecreate); expect(approvalResult).toBeGreaterThan(approvalCommand); expect(approvalCaptureAssertion).toBeGreaterThan(approvalResult); + expect(websocketCommand).toBeGreaterThan(approvalCaptureAssertion); + expect(websocketResult).toBeGreaterThan(websocketCommand); + expect(websocketAssertion).toBeGreaterThan(websocketResult); } finally { rmSync(captureDir, { recursive: true, force: true }); } From 20e757ad16fc4d3419d141baaef56c4134a064ba Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Thu, 9 Jul 2026 09:40:09 -0700 Subject: [PATCH 35/37] test(e2e): prove approved policy is active Signed-off-by: Carlos Villela --- test/e2e/live/issue-6194-tui-expect.ts | 28 +++++++++++++++ .../openclaw-tui-chat-correlation.test.ts | 29 ++++++++++++++- .../issue-6194-tui-post-idle-contract.test.ts | 35 +++++++++++++++++-- 3 files changed, 88 insertions(+), 4 deletions(-) diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index ef40b3f69e..9049d47c0f 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -89,6 +89,7 @@ set sandbox $env(NEMOCLAW_ISSUE_6194_SANDBOX) set capture $env(NEMOCLAW_ISSUE_6194_CAPTURE) set triggerCapture $env(NEMOCLAW_ISSUE_6194_TRIGGER_CAPTURE) set ruleCapture $env(NEMOCLAW_ISSUE_6194_RULE_CAPTURE) +set policyCapture $env(NEMOCLAW_ISSUE_6194_POLICY_CAPTURE) set networkEndpoint $env(NEMOCLAW_ISSUE_6194_NETWORK_ENDPOINT) set networkHost $env(NEMOCLAW_ISSUE_6194_NETWORK_HOST) log_file -noappend $capture @@ -240,6 +241,33 @@ expect_exact_or_exit $termSpawn $networkHost network_rule_detail_endpoint 82 83 expect_or_exit $termSpawn {\\[a\\][^\\r\\n]*Approve} network_rule_approve_action 84 85 send -i $termSpawn -- "a" expect_or_exit $termSpawn {Approved[^\\r\\n]*'[^']+'[^\\r\\n]*policy v[0-9]+} network_approval_processed 86 87 +# The terminal acknowledgement only proves that the approval action was +# accepted. Retry the exact documented Atlassian probe so the target also +# proves that the running policy now permits the request. An unauthenticated +# 401 is the expected success signal for this endpoint. +spawn -noecho openshell sandbox exec --name $sandbox --no-tty --timeout 40 -- /usr/bin/curl -sS --connect-timeout 5 --max-time 30 -o /dev/null -w {ISSUE6194_POLICY_HTTP_STATUS=%{http_code}\\n} $networkEndpoint +set policySpawn $spawn_id +set policyOutput "" +set savedTimeout $timeout +set timeout 45 +expect { + -i $policySpawn + eof { set policyOutput $expect_out(buffer) } + timeout { + stop_spawn $policySpawn + stop_spawn $termSpawn + exit 88 + } +} +set timeout $savedTimeout +catch {wait -i $policySpawn} policyWait +write_capture $policyCapture $policyOutput +if {![regexp {ISSUE6194_POLICY_HTTP_STATUS=401(\\r?\\n|$)} $policyOutput]} { + puts "ISSUE6194_DIAGNOSTIC approved policy did not permit the exact endpoint: $policyOutput" + stop_spawn $termSpawn + exit 90 +} +mark network_policy_updated send -i $termSpawn -- "q" expect { -i $termSpawn diff --git a/test/e2e/live/openclaw-tui-chat-correlation.test.ts b/test/e2e/live/openclaw-tui-chat-correlation.test.ts index 50641c1a5f..4a5e6b80c8 100644 --- a/test/e2e/live/openclaw-tui-chat-correlation.test.ts +++ b/test/e2e/live/openclaw-tui-chat-correlation.test.ts @@ -648,10 +648,12 @@ test( const approvalCaptureFile = join(captureDir, "openshell-approval-capture.log"); const triggerCaptureFile = join(captureDir, "openshell-network-trigger.log"); const ruleCaptureFile = join(captureDir, "openshell-pending-rule.log"); + const policyCaptureFile = join(captureDir, "openshell-policy-retry.log"); const approvalExpectScript = artifacts.pathFor("issue6194-openshell-approval.expect"); precreateIssue6194Capture(approvalCaptureFile); precreateIssue6194Capture(triggerCaptureFile); precreateIssue6194Capture(ruleCaptureFile); + precreateIssue6194Capture(policyCaptureFile); writeFileSync(approvalExpectScript, buildIssue6194OpenShellApprovalExpectScript(), { mode: 0o700, }); @@ -663,6 +665,7 @@ test( NEMOCLAW_ISSUE_6194_CAPTURE: approvalCaptureFile, NEMOCLAW_ISSUE_6194_TRIGGER_CAPTURE: triggerCaptureFile, NEMOCLAW_ISSUE_6194_RULE_CAPTURE: ruleCaptureFile, + NEMOCLAW_ISSUE_6194_POLICY_CAPTURE: policyCaptureFile, NEMOCLAW_ISSUE_6194_NETWORK_ENDPOINT: ISSUE6194_NETWORK_APPROVAL_ENDPOINT, NEMOCLAW_ISSUE_6194_NETWORK_HOST: ISSUE6194_NETWORK_APPROVAL_HOST, NEMOCLAW_ISSUE_6194_TUI_TIMEOUT: String(ISSUE6194_TUI_TIMEOUT_SEC), @@ -673,11 +676,13 @@ test( const approvalCapture = readIssue6194Capture(approvalCaptureFile); const triggerCapture = readIssue6194Capture(triggerCaptureFile); const ruleCapture = readIssue6194Capture(ruleCaptureFile); + const policyCapture = readIssue6194Capture(policyCaptureFile); const redactedApprovalCapture = secrets.redact(approvalCapture.contents, [apiKey]); const redactedTriggerCapture = secrets.redact(triggerCapture.contents, [apiKey]); const redactedRuleCapture = secrets.redact(ruleCapture.contents, [apiKey]); + const redactedPolicyCapture = secrets.redact(policyCapture.contents, [apiKey]); const plainApprovalCapture = stripTerminalControl(redactedApprovalCapture); - const approvalCombined = `${resultText(approval)}\n${plainApprovalCapture}\n${redactedTriggerCapture}\n${redactedRuleCapture}`; + const approvalCombined = `${resultText(approval)}\n${plainApprovalCapture}\n${redactedTriggerCapture}\n${redactedRuleCapture}\n${redactedPolicyCapture}`; await artifacts.writeText( "issue6194-openshell-approval-capture.log", redactedApprovalCapture, @@ -688,6 +693,7 @@ test( ); await artifacts.writeText("issue6194-openshell-network-trigger.log", redactedTriggerCapture); await artifacts.writeText("issue6194-openshell-pending-rule.log", redactedRuleCapture); + await artifacts.writeText("issue6194-openshell-policy-retry.log", redactedPolicyCapture); await artifacts.writeJson("issue6194-approval-result.json", { id: "issue-6194-openshell-network-approval", expectExitCode: approval.exitCode, @@ -696,6 +702,13 @@ test( triggerCaptureExists: triggerCapture.exists, ruleCaptureExists: ruleCapture.exists, ruleCaptureNonEmpty: redactedRuleCapture.length > 0, + policyCaptureExists: policyCapture.exists, + policyCaptureNonEmpty: redactedPolicyCapture.length > 0, + postApprovalEndpoint: ISSUE6194_NETWORK_APPROVAL_ENDPOINT, + postApprovalExpectedHttpStatus: 401, + postApprovalHttpStatus401: redactedPolicyCapture.includes( + "ISSUE6194_POLICY_HTTP_STATUS=401", + ), pendingQueueEmpty: approvalCombined.includes("ISSUE6194_MARK pending_queue_empty"), requestTriggered: approvalCombined.includes("ISSUE6194_MARK network_request_triggered"), requestCompleted: approvalCombined.includes("ISSUE6194_MARK network_request_completed"), @@ -704,6 +717,7 @@ test( endpointRendered: approvalCombined.includes("ISSUE6194_MARK network_rule_endpoint"), detailBinary: approvalCombined.includes("ISSUE6194_MARK network_rule_detail_binary"), approvalProcessed: approvalCombined.includes("ISSUE6194_MARK network_approval_processed"), + policyUpdated: approvalCombined.includes("ISSUE6194_MARK network_policy_updated"), cleanExit: approvalCombined.includes("ISSUE6194_MARK openshell_clean_exit"), }); @@ -712,6 +726,11 @@ test( plainApprovalCapture.length, "OpenShell approval capture must not be empty", ).toBeGreaterThan(0); + expect(policyCapture.exists, "post-approval policy retry capture must exist").toBe(true); + expect( + redactedPolicyCapture.length, + "post-approval policy retry capture must not be empty", + ).toBeGreaterThan(0); expect(approval.exitCode, approvalCombined).toBe(0); expect( approvalCombined, @@ -727,6 +746,14 @@ test( expect(approvalCombined, "OpenShell approval input must be processed").toContain( "ISSUE6194_MARK network_approval_processed", ); + expect( + redactedPolicyCapture, + "approved running policy must allow the exact post-approval Atlassian probe", + ).toContain("ISSUE6194_POLICY_HTTP_STATUS=401"); + expect( + approvalCombined, + "post-approval probe must independently prove the running policy was updated", + ).toContain("ISSUE6194_MARK network_policy_updated"); expect(approvalCombined, "OpenShell terminal must exit cleanly after approval").toContain( "ISSUE6194_MARK openshell_clean_exit", ); diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index 34b012a58d..a1f93bbcdc 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -37,6 +37,9 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect(declaration).toContain('"openshell-network-rule-terminal-approval"'); expect(liveSource).toContain('artifactName: "issue6194-openshell-network-approval"'); expect(liveSource).toContain('artifacts.writeJson("issue6194-approval-result.json"'); + expect(liveSource).toContain('artifacts.writeText("issue6194-openshell-policy-retry.log"'); + expect(liveSource).toContain("postApprovalEndpoint: ISSUE6194_NETWORK_APPROVAL_ENDPOINT"); + expect(liveSource).toContain("postApprovalExpectedHttpStatus: 401"); expect(liveSource).toContain('artifactName: "live-issue2603-repro"'); expect(liveSource).toContain('artifacts.writeJson("issue2603-trace.json"'); }); @@ -202,10 +205,31 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect(script).toContain( "expect_or_exit $termSpawn {Approved[^\\r\\n]*'[^']+'[^\\r\\n]*policy v[0-9]+} network_approval_processed", ); + expect(script).toContain( + "spawn -noecho openshell sandbox exec --name $sandbox --no-tty --timeout 40 -- /usr/bin/curl -sS --connect-timeout 5 --max-time 30 -o /dev/null -w {ISSUE6194_POLICY_HTTP_STATUS=%{http_code}\\n} $networkEndpoint", + ); + expect(script).toContain("set policySpawn $spawn_id"); + expect(script).toContain("expect {\n -i $policySpawn\n"); + expect(script).toContain("catch {wait -i $policySpawn} policyWait"); + expect(script).toContain("write_capture $policyCapture $policyOutput"); + expect(script).toContain("regexp {ISSUE6194_POLICY_HTTP_STATUS=401(\\r?\\n|$)} $policyOutput"); expect(script).not.toContain("{Approved '[^']+'"); expect(script).not.toContain('send -i $termSpawn -- "A"'); expect(script).not.toContain('send -i $termSpawn -- "y"'); + const approvalAcknowledged = script.indexOf("network_approval_processed"); + const postApprovalRetry = script.indexOf("ISSUE6194_POLICY_HTTP_STATUS=%{http_code}"); + const postApprovalCapture = script.indexOf("write_capture $policyCapture $policyOutput"); + const postApprovalVerified = script.indexOf("mark network_policy_updated"); + const postApprovalOrder = [ + approvalAcknowledged, + postApprovalRetry, + postApprovalCapture, + postApprovalVerified, + ]; + expect(postApprovalOrder.every((index) => index >= 0)).toBe(true); + expect([...postApprovalOrder].sort((a, b) => a - b)).toEqual(postApprovalOrder); + const markers = [ "sole_sandbox_verified", "pending_queue_empty", @@ -223,6 +247,7 @@ describe("live TUI post-idle coverage contract (#6194)", () => { "network_rule_detail_endpoint", "network_rule_approve_action", "network_approval_processed", + "network_policy_updated", "openshell_clean_exit", ]; const order = markers.map((marker) => script.indexOf(marker)); @@ -232,9 +257,11 @@ describe("live TUI post-idle coverage contract (#6194)", () => { it("binds multi-pattern waits to their intended Expect spawn (#6194)", () => { const script = buildIssue6194OpenShellApprovalExpectScript(); - const spawnScopedWaits = script.match(/expect \{\n\s+-i \$(?:target|curlSpawn|termSpawn)\n/gu); + const spawnScopedWaits = script.match( + /expect \{\n\s+-i \$(?:target|curlSpawn|policySpawn|termSpawn)\n/gu, + ); - expect(spawnScopedWaits).toHaveLength(5); + expect(spawnScopedWaits).toHaveLength(6); expect(script).not.toMatch(/\bexpect -i \$/u); expect(script).toContain("-nocase -ex $value { mark $markName }"); expect(script).not.toContain("-nocase -exact"); @@ -304,6 +331,7 @@ describe("live TUI post-idle coverage contract (#6194)", () => { const approvalPrecreate = liveSource.indexOf( "precreateIssue6194Capture(approvalCaptureFile)", ); + const policyPrecreate = liveSource.indexOf("precreateIssue6194Capture(policyCaptureFile)"); const approvalCommand = liveSource.indexOf('host.command("expect", [approvalExpectScript]'); const approvalResult = liveSource.indexOf( 'artifacts.writeJson("issue6194-approval-result.json"', @@ -326,7 +354,8 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect(tuiResult).toBeGreaterThan(tuiCommand); expect(tuiCaptureAssertion).toBeGreaterThan(tuiResult); expect(approvalPrecreate).toBeGreaterThan(tuiCaptureAssertion); - expect(approvalCommand).toBeGreaterThan(approvalPrecreate); + expect(policyPrecreate).toBeGreaterThan(approvalPrecreate); + expect(approvalCommand).toBeGreaterThan(policyPrecreate); expect(approvalResult).toBeGreaterThan(approvalCommand); expect(approvalCaptureAssertion).toBeGreaterThan(approvalResult); expect(websocketCommand).toBeGreaterThan(approvalCaptureAssertion); From 9f658aa88b7abb3327fbc4cd5cf50b6884ca7882 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Thu, 9 Jul 2026 09:55:51 -0700 Subject: [PATCH 36/37] test(e2e): wait for approved policy revision Signed-off-by: Carlos Villela --- test/e2e/live/issue-6194-tui-expect.ts | 66 ++++++++++++++++--- .../openclaw-tui-chat-correlation.test.ts | 32 +++++++++ .../issue-6194-tui-post-idle-contract.test.ts | 51 ++++++++++++-- 3 files changed, 135 insertions(+), 14 deletions(-) diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index 9049d47c0f..67bfd2d786 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -240,20 +240,68 @@ expect_or_exit $termSpawn {Binary:[^\\r\\n]*/usr/bin/curl} network_rule_detail_b expect_exact_or_exit $termSpawn $networkHost network_rule_detail_endpoint 82 83 expect_or_exit $termSpawn {\\[a\\][^\\r\\n]*Approve} network_rule_approve_action 84 85 send -i $termSpawn -- "a" -expect_or_exit $termSpawn {Approved[^\\r\\n]*'[^']+'[^\\r\\n]*policy v[0-9]+} network_approval_processed 86 87 -# The terminal acknowledgement only proves that the approval action was -# accepted. Retry the exact documented Atlassian probe so the target also -# proves that the running policy now permits the request. An unauthenticated -# 401 is the expected success signal for this endpoint. -spawn -noecho openshell sandbox exec --name $sandbox --no-tty --timeout 40 -- /usr/bin/curl -sS --connect-timeout 5 --max-time 30 -o /dev/null -w {ISSUE6194_POLICY_HTTP_STATUS=%{http_code}\\n} $networkEndpoint +expect { + -i $termSpawn + -nocase -re {Approved[^\\r\\n]*'[^']+'[^\\r\\n]*policy v([0-9]+)} { + set approvedPolicyVersion $expect_out(1,string) + mark network_approval_processed + } + timeout { + stop_spawn $termSpawn + exit 86 + } + eof { + catch {wait -i $termSpawn} + exit 87 + } +} +# The approval RPC assigns a policy revision before the sandbox loads it. +# Poll that exact revision through the read-only policy API until both its +# status and the active version prove convergence. Preserve every bounded +# attempt so timeout and failed-revision diagnostics remain reviewable. +set policyStatusOutput "ISSUE6194_APPROVED_POLICY_VERSION=$approvedPolicyVersion\\n" +set policyLoaded 0 +set policyTerminalStatus timeout +for {set attempt 1} {$attempt <= 10} {incr attempt} { + set policyGetFailed [catch {exec timeout 2 openshell policy get $sandbox --rev $approvedPolicyVersion --output json} candidate] + append policyStatusOutput "ISSUE6194_POLICY_STATUS_ATTEMPT=$attempt\\n$candidate\\n" + set versionPattern [format {"version"[[:space:]]*:[[:space:]]*%s([[:space:]]|,)} $approvedPolicyVersion] + set activePattern [format {"active_version"[[:space:]]*:[[:space:]]*%s([[:space:]]|,)} $approvedPolicyVersion] + set versionMatches [regexp $versionPattern $candidate] + set statusLoaded [regexp {"status"[[:space:]]*:[[:space:]]*"loaded"} $candidate] + set activeMatches [regexp $activePattern $candidate] + if {!$policyGetFailed && $versionMatches && $statusLoaded && $activeMatches} { + append policyStatusOutput "ISSUE6194_ACTIVE_POLICY_VERSION=$approvedPolicyVersion\\n" + append policyStatusOutput "ISSUE6194_POLICY_STATUS=loaded\\n" + set policyLoaded 1 + break + } + if {!$policyGetFailed && [regexp {"status"[[:space:]]*:[[:space:]]*"(failed|superseded)"} $candidate _ terminalStatus]} { + set policyTerminalStatus $terminalStatus + break + } + after 500 +} +if {!$policyLoaded} { + append policyStatusOutput "ISSUE6194_POLICY_STATUS=$policyTerminalStatus\\n" + write_capture $policyCapture $policyStatusOutput + puts "ISSUE6194_DIAGNOSTIC approved policy revision did not become active: $policyStatusOutput" + stop_spawn $termSpawn + exit 90 +} +mark network_policy_loaded +# Retry the exact documented Atlassian probe once the acknowledged policy +# revision is active. An unauthenticated 401 is the expected success signal. +spawn -noecho openshell sandbox exec --name $sandbox --no-tty --timeout 20 -- /usr/bin/curl -sS --connect-timeout 5 --max-time 10 -o /dev/null -w {ISSUE6194_POLICY_HTTP_STATUS=%{http_code}\\n} $networkEndpoint set policySpawn $spawn_id set policyOutput "" set savedTimeout $timeout -set timeout 45 +set timeout 25 expect { -i $policySpawn eof { set policyOutput $expect_out(buffer) } timeout { + write_capture $policyCapture $policyStatusOutput stop_spawn $policySpawn stop_spawn $termSpawn exit 88 @@ -261,11 +309,11 @@ expect { } set timeout $savedTimeout catch {wait -i $policySpawn} policyWait -write_capture $policyCapture $policyOutput +write_capture $policyCapture "$policyStatusOutput$policyOutput" if {![regexp {ISSUE6194_POLICY_HTTP_STATUS=401(\\r?\\n|$)} $policyOutput]} { puts "ISSUE6194_DIAGNOSTIC approved policy did not permit the exact endpoint: $policyOutput" stop_spawn $termSpawn - exit 90 + exit 91 } mark network_policy_updated send -i $termSpawn -- "q" diff --git a/test/e2e/live/openclaw-tui-chat-correlation.test.ts b/test/e2e/live/openclaw-tui-chat-correlation.test.ts index 4a5e6b80c8..f2af92f64e 100644 --- a/test/e2e/live/openclaw-tui-chat-correlation.test.ts +++ b/test/e2e/live/openclaw-tui-chat-correlation.test.ts @@ -682,6 +682,14 @@ test( const redactedRuleCapture = secrets.redact(ruleCapture.contents, [apiKey]); const redactedPolicyCapture = secrets.redact(policyCapture.contents, [apiKey]); const plainApprovalCapture = stripTerminalControl(redactedApprovalCapture); + const approvedPolicyVersion = + redactedPolicyCapture.match(/ISSUE6194_APPROVED_POLICY_VERSION=([0-9]+)/u)?.[1] ?? null; + const activePolicyVersion = + redactedPolicyCapture.match(/ISSUE6194_ACTIVE_POLICY_VERSION=([0-9]+)/u)?.[1] ?? null; + const observedPolicyStatus = + redactedPolicyCapture.match(/ISSUE6194_POLICY_STATUS=([a-z]+)/u)?.[1] ?? null; + const policyStatusAttempts = + redactedPolicyCapture.match(/ISSUE6194_POLICY_STATUS_ATTEMPT=/gu)?.length ?? 0; const approvalCombined = `${resultText(approval)}\n${plainApprovalCapture}\n${redactedTriggerCapture}\n${redactedRuleCapture}\n${redactedPolicyCapture}`; await artifacts.writeText( "issue6194-openshell-approval-capture.log", @@ -704,6 +712,13 @@ test( ruleCaptureNonEmpty: redactedRuleCapture.length > 0, policyCaptureExists: policyCapture.exists, policyCaptureNonEmpty: redactedPolicyCapture.length > 0, + approvedPolicyVersion, + activePolicyVersion, + observedPolicyStatus, + policyStatusAttempts, + policyStatusLoaded: redactedPolicyCapture.includes("ISSUE6194_POLICY_STATUS=loaded"), + policyVersionActive: + approvedPolicyVersion !== null && approvedPolicyVersion === activePolicyVersion, postApprovalEndpoint: ISSUE6194_NETWORK_APPROVAL_ENDPOINT, postApprovalExpectedHttpStatus: 401, postApprovalHttpStatus401: redactedPolicyCapture.includes( @@ -717,6 +732,7 @@ test( endpointRendered: approvalCombined.includes("ISSUE6194_MARK network_rule_endpoint"), detailBinary: approvalCombined.includes("ISSUE6194_MARK network_rule_detail_binary"), approvalProcessed: approvalCombined.includes("ISSUE6194_MARK network_approval_processed"), + policyLoaded: approvalCombined.includes("ISSUE6194_MARK network_policy_loaded"), policyUpdated: approvalCombined.includes("ISSUE6194_MARK network_policy_updated"), cleanExit: approvalCombined.includes("ISSUE6194_MARK openshell_clean_exit"), }); @@ -746,6 +762,22 @@ test( expect(approvalCombined, "OpenShell approval input must be processed").toContain( "ISSUE6194_MARK network_approval_processed", ); + expect( + approvedPolicyVersion, + "approval acknowledgement must identify its assigned policy revision", + ).not.toBeNull(); + expect( + redactedPolicyCapture, + "acknowledged policy revision must reach loaded status before the retry", + ).toContain("ISSUE6194_POLICY_STATUS=loaded"); + expect( + activePolicyVersion, + "loaded active policy revision must match the approval acknowledgement", + ).toBe(approvedPolicyVersion); + expect( + approvalCombined, + "post-approval probe must wait for the acknowledged policy revision to become active", + ).toContain("ISSUE6194_MARK network_policy_loaded"); expect( redactedPolicyCapture, "approved running policy must allow the exact post-approval Atlassian probe", diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index a1f93bbcdc..fa5614b439 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -40,6 +40,12 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect(liveSource).toContain('artifacts.writeText("issue6194-openshell-policy-retry.log"'); expect(liveSource).toContain("postApprovalEndpoint: ISSUE6194_NETWORK_APPROVAL_ENDPOINT"); expect(liveSource).toContain("postApprovalExpectedHttpStatus: 401"); + expect(liveSource).toContain("approvedPolicyVersion,"); + expect(liveSource).toContain("activePolicyVersion,"); + expect(liveSource).toContain("observedPolicyStatus,"); + expect(liveSource).toContain("policyStatusAttempts,"); + expect(liveSource).toContain("policyStatusLoaded:"); + expect(liveSource).toContain("policyVersionActive:"); expect(liveSource).toContain('artifactName: "live-issue2603-repro"'); expect(liveSource).toContain('artifacts.writeJson("issue2603-trace.json"'); }); @@ -202,27 +208,61 @@ describe("live TUI post-idle coverage contract (#6194)", () => { "expect_or_exit $termSpawn {\\[a\\][^\\r\\n]*Approve} network_rule_approve_action", ); expect(script).toContain('send -i $termSpawn -- "a"'); + expect(script).toContain("-nocase -re {Approved[^\\r\\n]*'[^']+'[^\\r\\n]*policy v([0-9]+)}"); + expect(script).toContain("set approvedPolicyVersion $expect_out(1,string)"); expect(script).toContain( - "expect_or_exit $termSpawn {Approved[^\\r\\n]*'[^']+'[^\\r\\n]*policy v[0-9]+} network_approval_processed", + 'set policyStatusOutput "ISSUE6194_APPROVED_POLICY_VERSION=$approvedPolicyVersion\\n"', ); + expect(script).toContain("for {set attempt 1} {$attempt <= 10} {incr attempt}"); expect(script).toContain( - "spawn -noecho openshell sandbox exec --name $sandbox --no-tty --timeout 40 -- /usr/bin/curl -sS --connect-timeout 5 --max-time 30 -o /dev/null -w {ISSUE6194_POLICY_HTTP_STATUS=%{http_code}\\n} $networkEndpoint", + "exec timeout 2 openshell policy get $sandbox --rev $approvedPolicyVersion --output json", + ); + expect(script).toContain( + 'append policyStatusOutput "ISSUE6194_POLICY_STATUS_ATTEMPT=$attempt\\n$candidate\\n"', + ); + expect(script).toContain("set policyTerminalStatus timeout"); + expect(script).toContain( + 'set versionPattern [format {"version"[[:space:]]*:[[:space:]]*%s([[:space:]]|,)} $approvedPolicyVersion]', + ); + expect(script).toContain( + 'set activePattern [format {"active_version"[[:space:]]*:[[:space:]]*%s([[:space:]]|,)} $approvedPolicyVersion]', + ); + expect(script).toContain( + 'append policyStatusOutput "ISSUE6194_ACTIVE_POLICY_VERSION=$approvedPolicyVersion\\n"', + ); + expect(script).toContain('append policyStatusOutput "ISSUE6194_POLICY_STATUS=loaded\\n"'); + expect(script).toContain( + 'append policyStatusOutput "ISSUE6194_POLICY_STATUS=$policyTerminalStatus\\n"', + ); + expect(script).toContain('regexp {"status"[[:space:]]*:[[:space:]]*"loaded"} $candidate'); + expect(script).toContain("write_capture $policyCapture $policyStatusOutput"); + expect(script).toContain("ISSUE6194_DIAGNOSTIC approved policy revision did not become active"); + expect(script).toContain( + "spawn -noecho openshell sandbox exec --name $sandbox --no-tty --timeout 20 -- /usr/bin/curl -sS --connect-timeout 5 --max-time 10 -o /dev/null -w {ISSUE6194_POLICY_HTTP_STATUS=%{http_code}\\n} $networkEndpoint", ); expect(script).toContain("set policySpawn $spawn_id"); expect(script).toContain("expect {\n -i $policySpawn\n"); expect(script).toContain("catch {wait -i $policySpawn} policyWait"); - expect(script).toContain("write_capture $policyCapture $policyOutput"); + expect(script).toContain('write_capture $policyCapture "$policyStatusOutput$policyOutput"'); expect(script).toContain("regexp {ISSUE6194_POLICY_HTTP_STATUS=401(\\r?\\n|$)} $policyOutput"); expect(script).not.toContain("{Approved '[^']+'"); expect(script).not.toContain('send -i $termSpawn -- "A"'); expect(script).not.toContain('send -i $termSpawn -- "y"'); const approvalAcknowledged = script.indexOf("network_approval_processed"); + const postApprovalStatus = script.indexOf( + "exec timeout 2 openshell policy get $sandbox --rev $approvedPolicyVersion --output json", + ); + const postApprovalLoaded = script.indexOf("mark network_policy_loaded"); const postApprovalRetry = script.indexOf("ISSUE6194_POLICY_HTTP_STATUS=%{http_code}"); - const postApprovalCapture = script.indexOf("write_capture $policyCapture $policyOutput"); + const postApprovalCapture = script.indexOf( + 'write_capture $policyCapture "$policyStatusOutput$policyOutput"', + ); const postApprovalVerified = script.indexOf("mark network_policy_updated"); const postApprovalOrder = [ approvalAcknowledged, + postApprovalStatus, + postApprovalLoaded, postApprovalRetry, postApprovalCapture, postApprovalVerified, @@ -247,6 +287,7 @@ describe("live TUI post-idle coverage contract (#6194)", () => { "network_rule_detail_endpoint", "network_rule_approve_action", "network_approval_processed", + "network_policy_loaded", "network_policy_updated", "openshell_clean_exit", ]; @@ -261,7 +302,7 @@ describe("live TUI post-idle coverage contract (#6194)", () => { /expect \{\n\s+-i \$(?:target|curlSpawn|policySpawn|termSpawn)\n/gu, ); - expect(spawnScopedWaits).toHaveLength(6); + expect(spawnScopedWaits).toHaveLength(7); expect(script).not.toMatch(/\bexpect -i \$/u); expect(script).toContain("-nocase -ex $value { mark $markName }"); expect(script).not.toContain("-nocase -exact"); From d1ea65a544426ed48b4824fb0bfdfd14b921934f Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Thu, 9 Jul 2026 10:04:28 -0700 Subject: [PATCH 37/37] test(e2e): allow policy reload deadline Signed-off-by: Carlos Villela --- test/e2e/live/issue-6194-tui-expect.ts | 8 ++++++-- test/e2e/live/openclaw-tui-chat-correlation.test.ts | 4 +++- .../issue-6194-tui-post-idle-contract.test.ts | 12 +++++++++++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/test/e2e/live/issue-6194-tui-expect.ts b/test/e2e/live/issue-6194-tui-expect.ts index 67bfd2d786..6439104458 100644 --- a/test/e2e/live/issue-6194-tui-expect.ts +++ b/test/e2e/live/issue-6194-tui-expect.ts @@ -6,6 +6,7 @@ import { readFileSync, writeFileSync } from "node:fs"; export const ISSUE6194_TUI_TIMEOUT_SEC = 240; export const ISSUE6194_TUI_EXIT_TIMEOUT_SEC = 10; export const ISSUE6194_OPENSHELL_DASHBOARD_TIMEOUT_SEC = 30; +export const ISSUE6194_OPENSHELL_APPROVAL_TIMEOUT_BUFFER_SEC = 120; export const ISSUE6194_TUI_SESSION_PREFIX = "issue-6194-tui"; export const ISSUE6194_NETWORK_APPROVAL_ENDPOINT = "https://api.atlassian.com/oauth/token/accessible-resources"; @@ -262,7 +263,10 @@ expect { set policyStatusOutput "ISSUE6194_APPROVED_POLICY_VERSION=$approvedPolicyVersion\\n" set policyLoaded 0 set policyTerminalStatus timeout -for {set attempt 1} {$attempt <= 10} {incr attempt} { +set policyLoadDeadline [expr {[clock milliseconds] + 60000}] +set attempt 0 +while {[clock milliseconds] < $policyLoadDeadline} { + incr attempt set policyGetFailed [catch {exec timeout 2 openshell policy get $sandbox --rev $approvedPolicyVersion --output json} candidate] append policyStatusOutput "ISSUE6194_POLICY_STATUS_ATTEMPT=$attempt\\n$candidate\\n" set versionPattern [format {"version"[[:space:]]*:[[:space:]]*%s([[:space:]]|,)} $approvedPolicyVersion] @@ -280,7 +284,7 @@ for {set attempt 1} {$attempt <= 10} {incr attempt} { set policyTerminalStatus $terminalStatus break } - after 500 + after 1000 } if {!$policyLoaded} { append policyStatusOutput "ISSUE6194_POLICY_STATUS=$policyTerminalStatus\\n" diff --git a/test/e2e/live/openclaw-tui-chat-correlation.test.ts b/test/e2e/live/openclaw-tui-chat-correlation.test.ts index f2af92f64e..8f7cd1441a 100644 --- a/test/e2e/live/openclaw-tui-chat-correlation.test.ts +++ b/test/e2e/live/openclaw-tui-chat-correlation.test.ts @@ -34,6 +34,7 @@ import { buildIssue6194TuiExpectScript, ISSUE6194_NETWORK_APPROVAL_ENDPOINT, ISSUE6194_NETWORK_APPROVAL_HOST, + ISSUE6194_OPENSHELL_APPROVAL_TIMEOUT_BUFFER_SEC, ISSUE6194_TUI_SESSION_PREFIX, ISSUE6194_TUI_TIMEOUT_SEC, precreateIssue6194Capture, @@ -671,7 +672,8 @@ test( NEMOCLAW_ISSUE_6194_TUI_TIMEOUT: String(ISSUE6194_TUI_TIMEOUT_SEC), }, redactionValues: [apiKey], - timeoutMs: (ISSUE6194_TUI_TIMEOUT_SEC + 30) * 1000, + timeoutMs: + (ISSUE6194_TUI_TIMEOUT_SEC + ISSUE6194_OPENSHELL_APPROVAL_TIMEOUT_BUFFER_SEC) * 1000, }); const approvalCapture = readIssue6194Capture(approvalCaptureFile); const triggerCapture = readIssue6194Capture(triggerCaptureFile); diff --git a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts index fa5614b439..d27c528721 100644 --- a/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts +++ b/test/e2e/support/issue-6194-tui-post-idle-contract.test.ts @@ -11,6 +11,7 @@ import { buildIssue6194TuiExpectScript, ISSUE6194_NETWORK_APPROVAL_ENDPOINT, ISSUE6194_NETWORK_APPROVAL_HOST, + ISSUE6194_OPENSHELL_APPROVAL_TIMEOUT_BUFFER_SEC, ISSUE6194_OPENSHELL_DASHBOARD_TIMEOUT_SEC, ISSUE6194_TUI_EXIT_TIMEOUT_SEC, ISSUE6194_TUI_SESSION_PREFIX, @@ -213,7 +214,9 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect(script).toContain( 'set policyStatusOutput "ISSUE6194_APPROVED_POLICY_VERSION=$approvedPolicyVersion\\n"', ); - expect(script).toContain("for {set attempt 1} {$attempt <= 10} {incr attempt}"); + expect(script).toContain("set policyLoadDeadline [expr {[clock milliseconds] + 60000}]"); + expect(script).toContain("while {[clock milliseconds] < $policyLoadDeadline}"); + expect(script).toContain("incr attempt"); expect(script).toContain( "exec timeout 2 openshell policy get $sandbox --rev $approvedPolicyVersion --output json", ); @@ -237,6 +240,7 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect(script).toContain('regexp {"status"[[:space:]]*:[[:space:]]*"loaded"} $candidate'); expect(script).toContain("write_capture $policyCapture $policyStatusOutput"); expect(script).toContain("ISSUE6194_DIAGNOSTIC approved policy revision did not become active"); + expect(ISSUE6194_OPENSHELL_APPROVAL_TIMEOUT_BUFFER_SEC).toBe(120); expect(script).toContain( "spawn -noecho openshell sandbox exec --name $sandbox --no-tty --timeout 20 -- /usr/bin/curl -sS --connect-timeout 5 --max-time 10 -o /dev/null -w {ISSUE6194_POLICY_HTTP_STATUS=%{http_code}\\n} $networkEndpoint", ); @@ -374,6 +378,10 @@ describe("live TUI post-idle coverage contract (#6194)", () => { ); const policyPrecreate = liveSource.indexOf("precreateIssue6194Capture(policyCaptureFile)"); const approvalCommand = liveSource.indexOf('host.command("expect", [approvalExpectScript]'); + const approvalTimeout = liveSource.indexOf( + "ISSUE6194_OPENSHELL_APPROVAL_TIMEOUT_BUFFER_SEC", + approvalCommand, + ); const approvalResult = liveSource.indexOf( 'artifacts.writeJson("issue6194-approval-result.json"', ); @@ -397,6 +405,8 @@ describe("live TUI post-idle coverage contract (#6194)", () => { expect(approvalPrecreate).toBeGreaterThan(tuiCaptureAssertion); expect(policyPrecreate).toBeGreaterThan(approvalPrecreate); expect(approvalCommand).toBeGreaterThan(policyPrecreate); + expect(approvalTimeout).toBeGreaterThan(approvalCommand); + expect(approvalTimeout).toBeLessThan(approvalResult); expect(approvalResult).toBeGreaterThan(approvalCommand); expect(approvalCaptureAssertion).toBeGreaterThan(approvalResult); expect(websocketCommand).toBeGreaterThan(approvalCaptureAssertion);