From bfe40e9978191ce60e5d331e97f9c640727a36d4 Mon Sep 17 00:00:00 2001 From: Senthil Ravichandran Date: Mon, 10 Aug 2026 12:45:00 -0700 Subject: [PATCH 1/9] fix(cli): accept tagged trusted gateway identity Signed-off-by: Senthil Ravichandran --- src/lib/readiness/gateway-production.test.ts | 82 ++++++++++++++++++-- src/lib/readiness/gateway-production.ts | 37 +++++++-- 2 files changed, 107 insertions(+), 12 deletions(-) diff --git a/src/lib/readiness/gateway-production.test.ts b/src/lib/readiness/gateway-production.test.ts index 807e12a7239..f0e68a51fe7 100644 --- a/src/lib/readiness/gateway-production.test.ts +++ b/src/lib/readiness/gateway-production.test.ts @@ -6,6 +6,7 @@ import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { isDockerDriverGatewayProcessIdentity } from "../onboard/docker-driver-gateway-process-identity"; import type { GatewayOwner } from "../onboard/gateway-ownership"; import { resetTraceForTests, TRACE_FILE_ENV } from "../trace"; @@ -31,8 +32,8 @@ import { classifyManagedGatewayVersionDrift, classifyManagedGatewayVersionSource, createProductionGatewayReadinessDependencies, - gatewayExecutableSamplesMatchTrustedBinary, gatewayProcessIdentityMatchesTrustedBinary, + gatewayProcessSamplesMatchTrustedBinary, parseDarwinLsofExecutable, } from "./gateway-production"; @@ -151,6 +152,69 @@ describe("managed gateway port readiness (#7411)", () => { expect(subprocess.spawnSync).not.toHaveBeenCalled(); }); + it("accepts the owned gateway tag with trusted Linux executable and environment evidence (#8755)", () => { + const trusted = "/opt/openshell/bin/openshell-gateway"; + const input = { + pid: 999_999, + gatewayBin: trusted, + captureProcessArgs: () => "openshell-gateway[nemoclaw=nemoclaw;port=8080]", + processIdentityMatchesGatewayBinary: (identity: string) => + gatewayProcessIdentityMatchesTrustedBinary( + identity, + trusted, + "nemoclaw", + 8080, + trusted, + "linux", + ), + requireDockerDriverEnv: true, + hasDockerDriverGatewayEnv: () => false, + }; + + expect(isDockerDriverGatewayProcessIdentity(input)).toBe(false); + expect( + isDockerDriverGatewayProcessIdentity({ + ...input, + hasDockerDriverGatewayEnv: () => true, + }), + ).toBe(true); + }); + + it.each([ + [ + "a foreign executable", + "openshell-gateway[nemoclaw=nemoclaw;port=8080]", + "/tmp/foreign-gateway", + "linux", + ], + [ + "a different gateway target", + "openshell-gateway[nemoclaw=nemoclaw-8081;port=8081]", + "/opt/openshell/bin/openshell-gateway", + "linux", + ], + ["no executable evidence", "openshell-gateway[nemoclaw=nemoclaw;port=8080]", null, "linux"], + [ + "a macOS direct listener", + "openshell-gateway[nemoclaw=nemoclaw;port=8080]", + "/opt/openshell/bin/openshell-gateway", + "darwin", + ], + ] as const)("rejects the owned gateway tag with %s (#8755)", (_case, identity, executable, platform) => { + const trusted = "/opt/openshell/bin/openshell-gateway"; + + expect( + gatewayProcessIdentityMatchesTrustedBinary( + identity, + trusted, + "nemoclaw", + 8080, + executable, + platform, + ), + ).toBe(false); + }); + it("rejects trusted-looking argv on macOS without package-service identity", () => { const trusted = "/opt/homebrew/opt/openshell/bin/openshell-gateway"; const spoofedArgv = `${trusted} --name nemoclaw-readiness-test --port 8080`; @@ -167,16 +231,24 @@ describe("managed gateway port readiness (#7411)", () => { ).toBe(false); }); - it("rejects an executable change while a listener PID remains stable", () => { + it("rejects a listener when Linux process samples change (#8755)", () => { const trusted = "/opt/openshell/bin/openshell-gateway"; - expect(gatewayExecutableSamplesMatchTrustedBinary(trusted, trusted, trusted)).toBe(true); + expect(gatewayProcessSamplesMatchTrustedBinary("41", "41", trusted, trusted, trusted)).toBe( + true, + ); expect( - gatewayExecutableSamplesMatchTrustedBinary(trusted, "/tmp/foreign-gateway", trusted), + gatewayProcessSamplesMatchTrustedBinary("41", "41", trusted, "/tmp/foreign-gateway", trusted), ).toBe(false); expect( - gatewayExecutableSamplesMatchTrustedBinary("/tmp/foreign-gateway", trusted, trusted), + gatewayProcessSamplesMatchTrustedBinary("41", "41", "/tmp/foreign-gateway", trusted, trusted), ).toBe(false); + expect(gatewayProcessSamplesMatchTrustedBinary("41", "42", trusted, trusted, trusted)).toBe( + false, + ); + expect(gatewayProcessSamplesMatchTrustedBinary(null, null, trusted, trusted, trusted)).toBe( + false, + ); }); it("uses the main macOS executable vnode before dyld or later mappings", () => { diff --git a/src/lib/readiness/gateway-production.ts b/src/lib/readiness/gateway-production.ts index c2b0dd8e49f..d9ff360e168 100644 --- a/src/lib/readiness/gateway-production.ts +++ b/src/lib/readiness/gateway-production.ts @@ -44,6 +44,7 @@ import { gatewayProcessCmdlineMatches, OPENSHELL_GATEWAY_PROCESS_NAMES, } from "../onboard/gateway-process-identity"; +import { ownedHostGatewayTarget } from "../onboard/gateway-process-target-identity"; import { resolveOpenshell } from "../onboard/openshell-cli"; import { checkPortAvailable } from "../onboard/preflight"; import type { @@ -162,6 +163,21 @@ export function gatewayExecutableSamplesMatchTrustedBinary( ); } +/** Require one Linux process generation and the trusted executable across both samples. */ +export function gatewayProcessSamplesMatchTrustedBinary( + generationBefore: string | null, + generationAfter: string | null, + executableBefore: string | null, + executableAfter: string | null, + trustedBinary: string | null, +): boolean { + return ( + generationBefore !== null && + generationAfter === generationBefore && + gatewayExecutableSamplesMatchTrustedBinary(executableBefore, executableAfter, trustedBinary) + ); +} + function resolveTrustedOpenshellBinary(env: NodeJS.ProcessEnv): string | null { const commandV = captureReadonly(["sh", "-c", 'command -v "$1"', "--", "openshell"], env); return resolveOpenshell({ @@ -187,7 +203,7 @@ function resolveTrustedGatewayBinary(openshell: string | null): string | null { return null; } -/** Require the observed argv0 to resolve to the independently selected binary. */ +/** Require the trusted Linux executable plus a trusted path or owned target tag. */ export function gatewayProcessIdentityMatchesTrustedBinary( identity: string, trustedGatewayBin: string | null, @@ -201,12 +217,19 @@ export function gatewayProcessIdentityMatchesTrustedBinary( // untrusted and only the positively identified Homebrew service is eligible. if (!trustedGatewayBin || platform !== "linux") return false; const argv0 = cleanGatewayProcessToken(identity.trim().split(/\s+/, 1)[0] ?? ""); - const actual = argv0 ? normalizeExecutablePath(argv0) : null; const expected = normalizeExecutablePath(trustedGatewayBin); - if (!actual || !expected || actual !== expected) return false; - if (!actualExecutablePath || normalizeExecutablePath(actualExecutablePath) !== expected) { + if ( + !expected || + !actualExecutablePath || + normalizeExecutablePath(actualExecutablePath) !== expected + ) { return false; } + const taggedTarget = ownedHostGatewayTarget(path.basename(argv0)); + if (!taggedTarget) { + const actual = argv0 ? normalizeExecutablePath(argv0) : null; + if (!actual || !expected || actual !== expected) return false; + } return gatewayProcessCmdlineMatches(identity, trustedGatewayBin, { expectedOpenShellGateway: { name: gatewayName, port: gatewayPort }, processNames: OPENSHELL_GATEWAY_PROCESS_NAMES, @@ -555,9 +578,9 @@ export function createProductionGatewayReadinessDependencies( const executableAfter = readLinuxProcessExecutable(pid); const generationAfter = readLinuxProcessStartTime(pid); return exactTrustedBinary && - generationBefore !== null && - generationAfter === generationBefore && - gatewayExecutableSamplesMatchTrustedBinary( + gatewayProcessSamplesMatchTrustedBinary( + generationBefore, + generationAfter, executableBefore, executableAfter, trustedGatewayBin, From e06614370d56cee495113bf7f2045eec05758c4c Mon Sep 17 00:00:00 2001 From: Senthil Ravichandran Date: Mon, 10 Aug 2026 13:04:11 -0700 Subject: [PATCH 2/9] fix(cli): trust target-bound gateway listener Signed-off-by: Senthil Ravichandran --- src/lib/readiness/gateway-production.test.ts | 11 +++++++ src/lib/readiness/gateway-production.ts | 32 +++++++++++++++----- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/lib/readiness/gateway-production.test.ts b/src/lib/readiness/gateway-production.test.ts index f0e68a51fe7..498a07bfbb9 100644 --- a/src/lib/readiness/gateway-production.test.ts +++ b/src/lib/readiness/gateway-production.test.ts @@ -309,6 +309,17 @@ describe("managed gateway port readiness (#7411)", () => { expect(classifyManagedGatewayPortConflict(false, listener, "healthy", false, "unknown")).toBe( "unknown", ); + expect( + classifyManagedGatewayPortConflict(false, listener, "healthy", false, "mismatch", true), + ).toBe("owner-mismatch"); + }); + + it("accepts a target-bound listener when managed endpoint text is unavailable (#8755)", () => { + const listener = { pids: [41], unverifiedPids: [], complete: true }; + + expect( + classifyManagedGatewayPortConflict(false, listener, "healthy", false, "unknown", true), + ).toBe("none"); }); it("accepts one legacy Docker proxy only when the exact cluster endpoint owns the port", () => { diff --git a/src/lib/readiness/gateway-production.ts b/src/lib/readiness/gateway-production.ts index d9ff360e168..d78db04c70d 100644 --- a/src/lib/readiness/gateway-production.ts +++ b/src/lib/readiness/gateway-production.ts @@ -450,12 +450,15 @@ export function classifyManagedGatewayPortConflict( reuseState: GatewayReuseState | "unknown", legacyClusterBound = false, endpointBinding: ManagedGatewayEndpointBinding = "not-applicable", + hasTargetBoundListenerEvidence = false, ): GatewayPortConflictState { const listenerCount = listenerScan.pids.length + listenerScan.unverifiedPids.length; if (listenerCount > 1) return "multiple-owners"; const liveManagedState = reuseState === "healthy" || reuseState === "active-unnamed"; if (liveManagedState && endpointBinding === "mismatch") return "owner-mismatch"; - if (liveManagedState && endpointBinding === "unknown") return "unknown"; + if (liveManagedState && endpointBinding === "unknown" && !hasTargetBoundListenerEvidence) { + return "unknown"; + } if (portAvailable) { if (listenerCount > 0 || liveManagedState) return "unknown"; return "none"; @@ -547,11 +550,13 @@ export function createProductionGatewayReadinessDependencies( const openshellBin = resolveTrustedOpenshellBinary(probeEnv); const trustedGatewayBin = resolveTrustedGatewayBinary(openshellBin); const trustedVersionBinaryByPid = new Map(); + const trustedTargetBoundPids = new Set(); function observeDirectGatewayBinary(pid: number): string | null { if (process.platform !== "linux" || !trustedGatewayBin) return null; const generationBefore = readLinuxProcessStartTime(pid); const executableBefore = readLinuxProcessExecutable(pid); + let targetBoundIdentity = false; const exactTrustedBinary = isDockerDriverGatewayProcessIdentity({ pid, gatewayBin: trustedGatewayBin, @@ -559,15 +564,22 @@ export function createProductionGatewayReadinessDependencies( const result = captureReadonly(["ps", "-p", String(candidatePid), "-o", "args="], probeEnv); return result.exitCode === 0 ? result.stdout : ""; }, - processIdentityMatchesGatewayBinary: (identity) => - gatewayProcessIdentityMatchesTrustedBinary( + processIdentityMatchesGatewayBinary: (identity) => { + const matches = gatewayProcessIdentityMatchesTrustedBinary( identity, trustedGatewayBin, gatewayName, gatewayPort, executableBefore, process.platform, - ), + ); + if (matches) { + const argv0 = cleanGatewayProcessToken(identity.trim().split(/\s+/, 1)[0] ?? ""); + const target = ownedHostGatewayTarget(path.basename(argv0)); + targetBoundIdentity = target?.name === gatewayName && target.port === gatewayPort; + } + return matches; + }, requireDockerDriverEnv: true, hasDockerDriverGatewayEnv: (candidatePid) => hasDockerDriverGatewayEnvironment( @@ -577,16 +589,18 @@ export function createProductionGatewayReadinessDependencies( }); const executableAfter = readLinuxProcessExecutable(pid); const generationAfter = readLinuxProcessStartTime(pid); - return exactTrustedBinary && + const stableTrustedBinary = + exactTrustedBinary && gatewayProcessSamplesMatchTrustedBinary( generationBefore, generationAfter, executableBefore, executableAfter, trustedGatewayBin, - ) - ? trustedGatewayBin - : null; + ); + if (!stableTrustedBinary) return null; + if (targetBoundIdentity) trustedTargetBoundPids.add(pid); + return trustedGatewayBin; } function observePackagedServiceGatewayBinary(pid: number): string | null { @@ -685,6 +699,7 @@ export function createProductionGatewayReadinessDependencies( ); const portCheck = await checkGatewayPortAvailable(); trustedVersionBinaryByPid.clear(); + trustedTargetBoundPids.clear(); const listenerScan = listenerHelpers.getDockerDriverGatewayPortListenerScan(portCheck, { gatewayBin: trustedGatewayBin, }); @@ -757,6 +772,7 @@ export function createProductionGatewayReadinessDependencies( reuseState, legacyClusterBound, endpointBinding, + listenerScan.pids.length === 1 && trustedTargetBoundPids.has(listenerScan.pids[0] ?? -1), ); return { reuseState, From e3074e83f8c7fdc152eef213de34896ddd8802d8 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Mon, 10 Aug 2026 16:39:25 -0400 Subject: [PATCH 3/9] fix(cli): accept OpenShell Server endpoint output Signed-off-by: Julie Yaunches --- src/lib/readiness/gateway-production.test.ts | 27 +++-- src/lib/readiness/gateway-production.ts | 38 +++--- test/e2e/live/double-onboard.test.ts | 24 +++- ...rd-gateway-port-conflict-fast-fail.test.ts | 110 ++++++++++++++++++ 4 files changed, 175 insertions(+), 24 deletions(-) diff --git a/src/lib/readiness/gateway-production.test.ts b/src/lib/readiness/gateway-production.test.ts index 498a07bfbb9..2d1952578e8 100644 --- a/src/lib/readiness/gateway-production.test.ts +++ b/src/lib/readiness/gateway-production.test.ts @@ -290,14 +290,25 @@ describe("managed gateway port readiness (#7411)", () => { }); it.each([ - ["https://127.0.0.1:8080", 8080, "match"], - ["http://localhost:8080", 8080, "match"], - ["https://127.0.0.1:9090", 8080, "mismatch"], - ["https://gateway.example:8080", 8080, "mismatch"], - ] as const)("binds managed endpoint %s to port %s as %s", (endpoint, port, expected) => { - expect(classifyManagedGatewayEndpointBinding([`Gateway endpoint: ${endpoint}`], port)).toBe( - expected, - ); + ["Gateway endpoint: https://127.0.0.1:8080", 8080, "match"], + ["Gateway endpoint: http://localhost:8080", 8080, "match"], + ["Server: https://127.0.0.1:8080", 8080, "match"], + ["Server: https://127.0.0.1:9090", 8080, "mismatch"], + ["Server: https://gateway.example:8080", 8080, "mismatch"], + ["Server: ftp://127.0.0.1:8080", 8080, "mismatch"], + ["Server: not-a-url", 8080, "mismatch"], + ["DNS Server: https://127.0.0.1:8080", 8080, "unknown"], + ] as const)("classifies managed endpoint output %s for port %s as %s", (output, port, expected) => { + expect(classifyManagedGatewayEndpointBinding([output], port)).toBe(expected); + }); + + it("rejects conflicting managed endpoint output across OpenShell probes", () => { + expect( + classifyManagedGatewayEndpointBinding( + ["Gateway endpoint: https://127.0.0.1:8080", "Server: https://127.0.0.1:9090"], + 8080, + ), + ).toBe("mismatch"); }); it("rejects healthy managed metadata bound to another endpoint", () => { diff --git a/src/lib/readiness/gateway-production.ts b/src/lib/readiness/gateway-production.ts index d78db04c70d..4435b978b4c 100644 --- a/src/lib/readiness/gateway-production.ts +++ b/src/lib/readiness/gateway-production.ts @@ -303,24 +303,32 @@ export function classifyManagedGatewayEndpointBinding( outputs: readonly string[], expectedGatewayPort: number, ): Exclude { + let observedEndpoint = false; for (const output of outputs) { - const match = stripAnsi(output).match(/^\s*Gateway endpoint:\s+(\S+)\s*$/m); - if (!match?.[1]) continue; - try { - const endpoint = new URL(match[1]); - const localHost = - endpoint.hostname === "127.0.0.1" || - endpoint.hostname === "localhost" || - endpoint.hostname === "[::1]"; - const endpointPort = - endpoint.port || - (endpoint.protocol === "https:" ? "443" : endpoint.protocol === "http:" ? "80" : ""); - return localHost && endpointPort === String(expectedGatewayPort) ? "match" : "mismatch"; - } catch { - return "mismatch"; + for (const match of stripAnsi(output).matchAll( + /^\s*(?:Gateway endpoint|Server):\s+(\S+)\s*$/gm, + )) { + if (!match[1]) continue; + observedEndpoint = true; + try { + const endpoint = new URL(match[1]); + const localProtocol = endpoint.protocol === "https:" || endpoint.protocol === "http:"; + const localHost = + endpoint.hostname === "127.0.0.1" || + endpoint.hostname === "localhost" || + endpoint.hostname === "[::1]"; + const endpointPort = + endpoint.port || + (endpoint.protocol === "https:" ? "443" : endpoint.protocol === "http:" ? "80" : ""); + if (!localProtocol || !localHost || endpointPort !== String(expectedGatewayPort)) { + return "mismatch"; + } + } catch { + return "mismatch"; + } } } - return "unknown"; + return observedEndpoint ? "match" : "unknown"; } function observeReuseState( diff --git a/test/e2e/live/double-onboard.test.ts b/test/e2e/live/double-onboard.test.ts index d25a4528b32..33c4b0d1922 100644 --- a/test/e2e/live/double-onboard.test.ts +++ b/test/e2e/live/double-onboard.test.ts @@ -260,6 +260,10 @@ function gatewayNameFromOutput(output: string): string | undefined { return stripAnsi(output).match(/^\s*Gateway:\s+([^\s]+)/m)?.[1]; } +function gatewayServerEndpointFromOutput(output: string): string | undefined { + return stripAnsi(output).match(/^\s*Server:\s+(\S+)\s*$/m)?.[1]; +} + function dashboardPortFromList(output: string, sandboxName: string): string | undefined { let current: string | undefined; for (const line of output.split("\n")) { @@ -524,6 +528,7 @@ test("double-onboard: reuses gateway, preserves sibling sandbox, and recovers st boundary: "direct-cli-openshell-lifecycle", contract: [ "first onboard creates a sandbox and NemoClaw gateway", + "OpenShell status reports the managed gateway through its Server endpoint line", "same-name recreate reuses the healthy gateway without port conflicts", "different-name onboard preserves the first sandbox and allocates distinct dashboard forwards", "stopping one sandbox releases only its dashboard forward and reports the container stopped", @@ -548,6 +553,19 @@ test("double-onboard: reuses gateway, preserves sibling sandbox, and recovers st }); expect(resultText(gatewayInfo)).toContain("nemoclaw"); + const gatewayStatus = await sandbox.openshell(["status"], { + artifactName: "phase-2-openshell-status", + env: commandEnv(), + timeoutMs: 30_000, + }); + const gatewayStatusText = resultText(gatewayStatus); + expect(gatewayStatus.exitCode, gatewayStatusText).toBe(0); + const gatewayServerEndpoint = gatewayServerEndpointFromOutput(gatewayStatusText); + expect(gatewayServerEndpoint, gatewayStatusText).toBeDefined(); + expect(new URL(gatewayServerEndpoint as string).port).toBe( + process.env.NEMOCLAW_GATEWAY_PORT ?? "8080", + ); + const sandboxAAfterFirst = await sandbox.openshell(["sandbox", "get", SANDBOX_A], { artifactName: "phase-2-openshell-sandbox-a-get", env: commandEnv(), @@ -566,6 +584,7 @@ test("double-onboard: reuses gateway, preserves sibling sandbox, and recovers st const gatewayAfterSecond = await gatewayRuntimeId(host, "phase-3-gateway-id-after"); expect(gatewayBeforeSecond, "gateway runtime id before second onboard").not.toBe(""); expect(gatewayAfterSecond).toBe(gatewayBeforeSecond); + expect(secondText).toContain("Reusing healthy NemoClaw gateway."); expect(secondText).not.toContain("Port 8080 is not available"); expect(secondText).not.toContain("Port 18789 is not available"); const sandboxAAfterSecond = await sandbox.openshell(["sandbox", "get", SANDBOX_A], { @@ -844,7 +863,10 @@ test("double-onboard: reuses gateway, preserves sibling sandbox, and recovers st fakeOpenAiRequests: fake.requests(), assertions: { firstOnboard: first.exitCode === 0, - secondOnboardReusedGateway: gatewayAfterSecond === gatewayBeforeSecond, + gatewayStatusReportedServerEndpoint: Boolean(gatewayServerEndpoint), + secondOnboardReusedGateway: + gatewayAfterSecond === gatewayBeforeSecond && + secondText.includes("Reusing healthy NemoClaw gateway."), thirdOnboardPreservedSibling: sandboxAAfterThird.exitCode === 0 && sandboxBAfterThird.exitCode === 0, distinctDashboardPorts: Boolean(portA && portB && portA !== portB), diff --git a/test/onboard-gateway-port-conflict-fast-fail.test.ts b/test/onboard-gateway-port-conflict-fast-fail.test.ts index d1b06fb504d..b08c1439421 100644 --- a/test/onboard-gateway-port-conflict-fast-fail.test.ts +++ b/test/onboard-gateway-port-conflict-fast-fail.test.ts @@ -6,6 +6,8 @@ import net from "node:net"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { getGatewayClusterContainerName } from "../src/lib/adapters/openshell/gateway-drift"; +import { resolveGatewayName } from "../src/lib/onboard/gateway-binding"; import { createOnboardProcessWorkspace, type OnboardProcessWorkspace, @@ -114,4 +116,112 @@ describe("onboard gateway port conflict readiness (#6752)", () => { ); }, ); + + it( + "accepts Server endpoint evidence in repeated onboarding invocations", + testTimeoutOptions(30_000), + () => { + const gatewayName = resolveGatewayName(gatewayPort); + const gatewayEndpoint = `https://127.0.0.1:${String(gatewayPort)}/`; + const gatewayStatus = [ + "Server Status", + "", + `Gateway: ${gatewayName}`, + `Server: ${gatewayEndpoint}`, + "Status: Connected", + "", + ].join("\n"); + const gatewayInfo = [ + "Gateway Info", + "", + `Gateway: ${gatewayName}`, + `Server: ${gatewayEndpoint}`, + "", + ].join("\n"); + + for (const component of ["openshell", "openshell-gateway", "openshell-sandbox"]) { + workspace.writeExecutable( + component, + [ + "#!/usr/bin/env bash", + "# openshell capabilities: request-body-credential-rewrite websocket-credential-rewrite allow_all_known_mcp_methods", + 'case "$*" in', + ' --version|-V) printf "%s 0.0.101\\n" "${0##*/}"; exit 0;;', + ` status|"status -g ${gatewayName}") printf ${JSON.stringify(gatewayStatus)}; exit 0;;`, + ` "gateway info"|"gateway info -g ${gatewayName}") printf ${JSON.stringify(gatewayInfo)}; exit 0;;`, + "esac", + "exit 1", + ].join("\n"), + ); + } + + const containerName = getGatewayClusterContainerName(gatewayName); + const portBindings = JSON.stringify({ + [`${String(gatewayPort)}/tcp`]: [{ HostPort: String(gatewayPort) }], + }); + workspace.writeExecutable( + "docker", + [ + "#!/usr/bin/env bash", + `if [ "$1" = info ]; then printf '%s\\n' ${JSON.stringify( + JSON.stringify({ + ServerVersion: "24.0.0", + OperatingSystem: "Docker Desktop", + NCPU: 8, + MemTotal: 17_179_869_184, + }), + )}; exit 0; fi`, + 'if [ "$1" = ps ]; then exit 0; fi', + 'if [ "$1" = inspect ] && [ "$4" = ' + JSON.stringify(containerName) + " ]; then", + ' case "$3" in', + ' "{{.State.Running}}") printf "true\\n";;', + ` "{{json .NetworkSettings.Ports}}") printf '%s\\n' ${JSON.stringify(portBindings)};;`, + ' "{{.Config.Image}}") printf "nvcr.io/nvidia/openshell/cluster:0.0.101\\n";;', + " *) exit 1;;", + " esac", + " exit 0", + "fi", + "exit 0", + ].join("\n"), + ); + workspace.writeExecutable( + "lsof", + [ + "#!/usr/bin/env bash", + `if [[ " $* " = *" :${String(gatewayPort)} "* ]]; then`, + ` if [ "$1" = -ti ]; then printf "%s\\n" ${JSON.stringify(String(process.pid))}; exit 0; fi`, + "fi", + "exit 1", + ].join("\n"), + ); + + const env = workspaceEnv(workspace, { + COMPATIBLE_API_KEY: "test-only-compatible-key", + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1", + NEMOCLAW_ENDPOINT_URL: "http://127.0.0.1:1/v1", + NEMOCLAW_GATEWAY_PORT: String(gatewayPort), + NEMOCLAW_MODEL: "test-model", + NEMOCLAW_OPENSHELL_BIN: path.join(workspace.binDir, "openshell"), + NEMOCLAW_OPENSHELL_CHANNEL: "stable", + NEMOCLAW_OPENSHELL_GATEWAY_BIN: path.join(workspace.binDir, "openshell-gateway"), + NEMOCLAW_OPENSHELL_SANDBOX_BIN: path.join(workspace.binDir, "openshell-sandbox"), + NEMOCLAW_PROVIDER: "custom", + NEMOCLAW_SKIP_HOST_DNS_PREFLIGHT: "1", + NEMOCLAW_TEST_NO_SLEEP: "1", + }); + const argv = [CLI, "onboard", "--name", "reuse-server", "--no-gpu", "--non-interactive"]; + const first = runOnboardProcess(argv, { timeoutMs: 10_000, env }); + const second = runOnboardProcess(argv, { timeoutMs: 10_000, env }); + + for (const result of [first, second]) { + expect(result.error).toBeUndefined(); + expect(result.signal).toBeNull(); + expect(result.output).toContain("Reusing healthy NemoClaw gateway."); + expect(result.output).not.toContain("gateway.port.uncontested"); + expect(result.output).not.toContain( + `Gateway port ${String(gatewayPort)} could not be observed completely.`, + ); + } + }, + ); }); From aab533e49dab8e34b6b78df2b91ae960b29f9425 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Mon, 10 Aug 2026 17:23:18 -0400 Subject: [PATCH 4/9] fix(cli): preserve scoped stale gateway readiness Signed-off-by: Julie Yaunches --- src/lib/readiness/gateway-production.test.ts | 46 ++++++++++++++++++- src/lib/readiness/gateway-production.ts | 14 +++--- ...rd-gateway-port-conflict-fast-fail.test.ts | 7 +-- 3 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/lib/readiness/gateway-production.test.ts b/src/lib/readiness/gateway-production.test.ts index 2d1952578e8..309ff687877 100644 --- a/src/lib/readiness/gateway-production.test.ts +++ b/src/lib/readiness/gateway-production.test.ts @@ -37,8 +37,8 @@ import { parseDarwinLsofExecutable, } from "./gateway-production"; -function commandResult(stdout = "", status = 1) { - return { status, stdout, stderr: "", signal: null, pid: 1, output: [] }; +function commandResult(stdout = "", status = 1, stderr = "") { + return { status, stdout, stderr, signal: null, pid: 1, output: [] }; } function managedOwner(gatewayPort: number): GatewayOwner { @@ -297,6 +297,8 @@ describe("managed gateway port readiness (#7411)", () => { ["Server: https://gateway.example:8080", 8080, "mismatch"], ["Server: ftp://127.0.0.1:8080", 8080, "mismatch"], ["Server: not-a-url", 8080, "mismatch"], + ["Gateway endpoint:", 8080, "mismatch"], + ["Server: https://127.0.0.1:8080 trailing-data", 8080, "mismatch"], ["DNS Server: https://127.0.0.1:8080", 8080, "unknown"], ] as const)("classifies managed endpoint output %s for port %s as %s", (output, port, expected) => { expect(classifyManagedGatewayEndpointBinding([output], port)).toBe(expected); @@ -377,6 +379,46 @@ describe("managed gateway port readiness (#7411)", () => { ); }); + it("preserves scoped stale gateway state from OpenShell connection errors", async () => { + const statusConnectionRefused = [ + "Error: × client error (Connect)", + " ├─▶ tcp connect error", + " ╰─▶ Connection refused (os error 111)", + ].join("\n"); + const infoConnectionRefused = [ + "Error: × transport error", + " ╰─▶ Connection refused (os error 111)", + ].join("\n"); + subprocess.spawnSync.mockImplementation((command: string, args: readonly string[] = []) => { + const resolvesOpenshell = command === "sh" && args.includes('command -v "$1"'); + if (resolvesOpenshell) return commandResult("/usr/local/bin/openshell\n", 0); + if (command === "/usr/local/bin/openshell") { + const stderr = args[0] === "status" ? statusConnectionRefused : infoConnectionRefused; + return commandResult("", 1, stderr); + } + return commandResult(); + }); + + const gatewayPort = 0; + const deps = createProductionGatewayReadinessDependencies({ + gatewayName: () => "nemoclaw-readiness-test", + gatewayPort: () => gatewayPort, + }); + + await expect(deps.observeManagedGateway(managedOwner(gatewayPort))).resolves.toMatchObject({ + reuseState: "stale", + driftState: "not-detected", + portConflictState: "none", + }); + expect(subprocess.spawnSync).toHaveBeenCalledWith( + "/usr/local/bin/openshell", + ["status", "-g", "nemoclaw-readiness-test"], + expect.objectContaining({ + env: expect.objectContaining({ OPENSHELL_GATEWAY: "nemoclaw-readiness-test" }), + }), + ); + }); + it("collects production port evidence without attempting sudo", async () => { vi.stubEnv("GITHUB_TOKEN", "github-secret"); vi.stubEnv("OPENSHELL_GATEWAY_AUTH_TOKEN", "gateway-secret"); diff --git a/src/lib/readiness/gateway-production.ts b/src/lib/readiness/gateway-production.ts index 4435b978b4c..d1eee202868 100644 --- a/src/lib/readiness/gateway-production.ts +++ b/src/lib/readiness/gateway-production.ts @@ -305,13 +305,12 @@ export function classifyManagedGatewayEndpointBinding( ): Exclude { let observedEndpoint = false; for (const output of outputs) { - for (const match of stripAnsi(output).matchAll( - /^\s*(?:Gateway endpoint|Server):\s+(\S+)\s*$/gm, - )) { - if (!match[1]) continue; + for (const match of stripAnsi(output).matchAll(/^\s*(?:Gateway endpoint|Server):(.*)$/gm)) { observedEndpoint = true; + const endpointText = match[1]?.trim() ?? ""; + if (!endpointText || /\s/u.test(endpointText)) return "mismatch"; try { - const endpoint = new URL(match[1]); + const endpoint = new URL(endpointText); const localProtocol = endpoint.protocol === "https:" || endpoint.protocol === "http:"; const localHost = endpoint.hostname === "127.0.0.1" || @@ -339,7 +338,7 @@ function observeReuseState( ): { endpointBinding: ManagedGatewayEndpointBinding; reuseState: GatewayReuseState | "unknown" } { if (!openshell) return { endpointBinding: "not-applicable", reuseState: "missing" }; - const status = captureReadonly([openshell, "status"], env); + const status = captureReadonly([openshell, "status", "-g", gatewayName], env); const named = captureReadonly([openshell, "gateway", "info", "-g", gatewayName], env); const active = captureReadonly([openshell, "gateway", "info"], env); if ([status, named, active].some(({ exitCode, timedOut }) => timedOut || exitCode === null)) { @@ -352,6 +351,7 @@ function observeReuseState( combinedOutput(named), combinedOutput(active), gatewayName, + gatewayName, ); if (status.exitCode !== 0 && reuseState === "missing") { reuseState = /\bNo active gateway\b|\bNo gateway metadata found\b/i.test(statusOutput) @@ -377,7 +377,7 @@ function inspectLegacyCluster( env: NodeJS.ProcessEnv, ): { active: boolean; imageRef: string | null } { if (!openshell) return { active: false, imageRef: null }; - const status = captureReadonly([openshell, "status"], env); + const status = captureReadonly([openshell, "status", "-g", gatewayName], env); const named = captureReadonly([openshell, "gateway", "info", "-g", gatewayName], env); const active = captureReadonly([openshell, "gateway", "info"], env); if ( diff --git a/test/onboard-gateway-port-conflict-fast-fail.test.ts b/test/onboard-gateway-port-conflict-fast-fail.test.ts index b08c1439421..e7644fd5099 100644 --- a/test/onboard-gateway-port-conflict-fast-fail.test.ts +++ b/test/onboard-gateway-port-conflict-fast-fail.test.ts @@ -119,7 +119,7 @@ describe("onboard gateway port conflict readiness (#6752)", () => { it( "accepts Server endpoint evidence in repeated onboarding invocations", - testTimeoutOptions(30_000), + testTimeoutOptions(50_000), () => { const gatewayName = resolveGatewayName(gatewayPort); const gatewayEndpoint = `https://127.0.0.1:${String(gatewayPort)}/`; @@ -194,6 +194,7 @@ describe("onboard gateway port conflict readiness (#6752)", () => { "exit 1", ].join("\n"), ); + workspace.writeExecutable("sudo", "#!/usr/bin/env bash\nexit 1\n"); const env = workspaceEnv(workspace, { COMPATIBLE_API_KEY: "test-only-compatible-key", @@ -210,8 +211,8 @@ describe("onboard gateway port conflict readiness (#6752)", () => { NEMOCLAW_TEST_NO_SLEEP: "1", }); const argv = [CLI, "onboard", "--name", "reuse-server", "--no-gpu", "--non-interactive"]; - const first = runOnboardProcess(argv, { timeoutMs: 10_000, env }); - const second = runOnboardProcess(argv, { timeoutMs: 10_000, env }); + const first = runOnboardProcess(argv, { timeoutMs: 20_000, env }); + const second = runOnboardProcess(argv, { timeoutMs: 20_000, env }); for (const result of [first, second]) { expect(result.error).toBeUndefined(); From ce6cb0d243c5ee81e578ca412dd660011924ff96 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Mon, 10 Aug 2026 17:36:37 -0400 Subject: [PATCH 5/9] test(cli): keep readiness mock linear Signed-off-by: Julie Yaunches --- src/lib/readiness/gateway-production.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib/readiness/gateway-production.test.ts b/src/lib/readiness/gateway-production.test.ts index 309ff687877..db6c543ac8b 100644 --- a/src/lib/readiness/gateway-production.test.ts +++ b/src/lib/readiness/gateway-production.test.ts @@ -391,12 +391,12 @@ describe("managed gateway port readiness (#7411)", () => { ].join("\n"); subprocess.spawnSync.mockImplementation((command: string, args: readonly string[] = []) => { const resolvesOpenshell = command === "sh" && args.includes('command -v "$1"'); - if (resolvesOpenshell) return commandResult("/usr/local/bin/openshell\n", 0); - if (command === "/usr/local/bin/openshell") { - const stderr = args[0] === "status" ? statusConnectionRefused : infoConnectionRefused; - return commandResult("", 1, stderr); - } - return commandResult(); + const stderr = args[0] === "status" ? statusConnectionRefused : infoConnectionRefused; + return resolvesOpenshell + ? commandResult("/usr/local/bin/openshell\n", 0) + : command === "/usr/local/bin/openshell" + ? commandResult("", 1, stderr) + : commandResult(); }); const gatewayPort = 0; From 744ad3d7b50e8131a620a50f90bb8a045cf3a8ec Mon Sep 17 00:00:00 2001 From: Senthil Ravichandran Date: Mon, 10 Aug 2026 14:29:23 -0700 Subject: [PATCH 6/9] fix(cli): harden gateway endpoint evidence Signed-off-by: Senthil Ravichandran --- test/e2e/live/double-onboard.test.ts | 12 +++- ...rd-gateway-port-conflict-fast-fail.test.ts | 61 +++++++------------ 2 files changed, 32 insertions(+), 41 deletions(-) diff --git a/test/e2e/live/double-onboard.test.ts b/test/e2e/live/double-onboard.test.ts index 33c4b0d1922..9f15e0e0544 100644 --- a/test/e2e/live/double-onboard.test.ts +++ b/test/e2e/live/double-onboard.test.ts @@ -562,9 +562,15 @@ test("double-onboard: reuses gateway, preserves sibling sandbox, and recovers st expect(gatewayStatus.exitCode, gatewayStatusText).toBe(0); const gatewayServerEndpoint = gatewayServerEndpointFromOutput(gatewayStatusText); expect(gatewayServerEndpoint, gatewayStatusText).toBeDefined(); - expect(new URL(gatewayServerEndpoint as string).port).toBe( - process.env.NEMOCLAW_GATEWAY_PORT ?? "8080", - ); + const parsedGatewayServerEndpoint = new URL(gatewayServerEndpoint as string); + const gatewayServerPort = + parsedGatewayServerEndpoint.port || + (parsedGatewayServerEndpoint.protocol === "https:" + ? "443" + : parsedGatewayServerEndpoint.protocol === "http:" + ? "80" + : ""); + expect(gatewayServerPort).toBe(process.env.NEMOCLAW_GATEWAY_PORT ?? "8080"); const sandboxAAfterFirst = await sandbox.openshell(["sandbox", "get", SANDBOX_A], { artifactName: "phase-2-openshell-sandbox-a-get", diff --git a/test/onboard-gateway-port-conflict-fast-fail.test.ts b/test/onboard-gateway-port-conflict-fast-fail.test.ts index e7644fd5099..25d79cc34f9 100644 --- a/test/onboard-gateway-port-conflict-fast-fail.test.ts +++ b/test/onboard-gateway-port-conflict-fast-fail.test.ts @@ -5,9 +5,10 @@ import type { AddressInfo } from "node:net"; import net from "node:net"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { getGatewayClusterContainerName } from "../src/lib/adapters/openshell/gateway-drift"; import { resolveGatewayName } from "../src/lib/onboard/gateway-binding"; +import { createProductionGatewayReadinessDependencies } from "../src/lib/readiness/gateway-production"; import { createOnboardProcessWorkspace, type OnboardProcessWorkspace, @@ -118,9 +119,9 @@ describe("onboard gateway port conflict readiness (#6752)", () => { ); it( - "accepts Server endpoint evidence in repeated onboarding invocations", - testTimeoutOptions(50_000), - () => { + "accepts Server endpoint evidence on repeated production readiness probes", + testTimeoutOptions(30_000), + async () => { const gatewayName = resolveGatewayName(gatewayPort); const gatewayEndpoint = `https://127.0.0.1:${String(gatewayPort)}/`; const gatewayStatus = [ @@ -184,44 +185,28 @@ describe("onboard gateway port conflict readiness (#6752)", () => { "exit 0", ].join("\n"), ); - workspace.writeExecutable( - "lsof", - [ - "#!/usr/bin/env bash", - `if [[ " $* " = *" :${String(gatewayPort)} "* ]]; then`, - ` if [ "$1" = -ti ]; then printf "%s\\n" ${JSON.stringify(String(process.pid))}; exit 0; fi`, - "fi", - "exit 1", - ].join("\n"), + workspace.writeExecutable("lsof", "#!/usr/bin/env bash\nexit 1\n"); + + vi.stubEnv("HOME", workspace.homeDir); + vi.stubEnv("PATH", `${workspace.binDir}:${process.env.PATH || ""}`); + vi.stubEnv("NEMOCLAW_GATEWAY_PORT", String(gatewayPort)); + vi.stubEnv( + "NEMOCLAW_OPENSHELL_GATEWAY_BIN", + path.join(workspace.binDir, "openshell-gateway"), ); workspace.writeExecutable("sudo", "#!/usr/bin/env bash\nexit 1\n"); - const env = workspaceEnv(workspace, { - COMPATIBLE_API_KEY: "test-only-compatible-key", - NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1", - NEMOCLAW_ENDPOINT_URL: "http://127.0.0.1:1/v1", - NEMOCLAW_GATEWAY_PORT: String(gatewayPort), - NEMOCLAW_MODEL: "test-model", - NEMOCLAW_OPENSHELL_BIN: path.join(workspace.binDir, "openshell"), - NEMOCLAW_OPENSHELL_CHANNEL: "stable", - NEMOCLAW_OPENSHELL_GATEWAY_BIN: path.join(workspace.binDir, "openshell-gateway"), - NEMOCLAW_OPENSHELL_SANDBOX_BIN: path.join(workspace.binDir, "openshell-sandbox"), - NEMOCLAW_PROVIDER: "custom", - NEMOCLAW_SKIP_HOST_DNS_PREFLIGHT: "1", - NEMOCLAW_TEST_NO_SLEEP: "1", + const readiness = createProductionGatewayReadinessDependencies({ + gatewayName: () => gatewayName, + gatewayPort: () => gatewayPort, }); - const argv = [CLI, "onboard", "--name", "reuse-server", "--no-gpu", "--non-interactive"]; - const first = runOnboardProcess(argv, { timeoutMs: 20_000, env }); - const second = runOnboardProcess(argv, { timeoutMs: 20_000, env }); - - for (const result of [first, second]) { - expect(result.error).toBeUndefined(); - expect(result.signal).toBeNull(); - expect(result.output).toContain("Reusing healthy NemoClaw gateway."); - expect(result.output).not.toContain("gateway.port.uncontested"); - expect(result.output).not.toContain( - `Gateway port ${String(gatewayPort)} could not be observed completely.`, - ); + const owner = readiness.resolveOwner(); + for (const result of [ + await readiness.observeManagedGateway(owner), + await readiness.observeManagedGateway(owner), + ]) { + expect(result.reuseState).toBe("healthy"); + expect(result.portConflictState).toBe("none"); } }, ); From ea16a137802f32e42204d7b763b236aa919eb24c Mon Sep 17 00:00:00 2001 From: Senthil Ravichandran Date: Mon, 10 Aug 2026 14:42:15 -0700 Subject: [PATCH 7/9] test(cli): fail closed on readiness probes Signed-off-by: Senthil Ravichandran --- src/lib/readiness/gateway-production.test.ts | 39 ++++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/lib/readiness/gateway-production.test.ts b/src/lib/readiness/gateway-production.test.ts index db6c543ac8b..0ae865efbea 100644 --- a/src/lib/readiness/gateway-production.test.ts +++ b/src/lib/readiness/gateway-production.test.ts @@ -21,6 +21,7 @@ vi.mock("node:child_process", async (importOriginal) => ({ afterEach(() => { resetTraceForTests(); + subprocess.spawnSync.mockReset(); vi.unstubAllEnvs(); vi.restoreAllMocks(); }); @@ -390,13 +391,27 @@ describe("managed gateway port readiness (#7411)", () => { " ╰─▶ Connection refused (os error 111)", ].join("\n"); subprocess.spawnSync.mockImplementation((command: string, args: readonly string[] = []) => { - const resolvesOpenshell = command === "sh" && args.includes('command -v "$1"'); - const stderr = args[0] === "status" ? statusConnectionRefused : infoConnectionRefused; - return resolvesOpenshell - ? commandResult("/usr/local/bin/openshell\n", 0) - : command === "/usr/local/bin/openshell" - ? commandResult("", 1, stderr) - : commandResult(); + const resolvesOpenshell = + command === "sh" && + args.length === 4 && + args[0] === "-c" && + args[1] === 'command -v "$1"' && + args[2] === "--" && + args[3] === "openshell"; + if (resolvesOpenshell) return commandResult("/usr/local/bin/openshell\n", 0); + if (command === "/usr/local/bin/openshell") { + const commandLine = args.join("\0"); + if (commandLine === ["status", "-g", "nemoclaw-readiness-test"].join("\0")) { + return commandResult("", 1, statusConnectionRefused); + } + if ( + commandLine === ["gateway", "info", "-g", "nemoclaw-readiness-test"].join("\0") || + commandLine === ["gateway", "info"].join("\0") + ) { + return commandResult("", 1, infoConnectionRefused); + } + } + return commandResult(); }); const gatewayPort = 0; @@ -417,6 +432,16 @@ describe("managed gateway port readiness (#7411)", () => { env: expect.objectContaining({ OPENSHELL_GATEWAY: "nemoclaw-readiness-test" }), }), ); + expect(subprocess.spawnSync).toHaveBeenCalledWith( + "/usr/local/bin/openshell", + ["gateway", "info", "-g", "nemoclaw-readiness-test"], + expect.any(Object), + ); + expect(subprocess.spawnSync).toHaveBeenCalledWith( + "/usr/local/bin/openshell", + ["gateway", "info"], + expect.any(Object), + ); }); it("collects production port evidence without attempting sudo", async () => { From 5499f67e5c30496e3aa3b3f93aec0f9330f45dbe Mon Sep 17 00:00:00 2001 From: Senthil Ravichandran Date: Mon, 10 Aug 2026 14:47:29 -0700 Subject: [PATCH 8/9] test(cli): use linear readiness mock lookup Signed-off-by: Senthil Ravichandran --- src/lib/readiness/gateway-production.test.ts | 40 ++++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/src/lib/readiness/gateway-production.test.ts b/src/lib/readiness/gateway-production.test.ts index 0ae865efbea..d353c846207 100644 --- a/src/lib/readiness/gateway-production.test.ts +++ b/src/lib/readiness/gateway-production.test.ts @@ -390,28 +390,26 @@ describe("managed gateway port readiness (#7411)", () => { "Error: × transport error", " ╰─▶ Connection refused (os error 111)", ].join("\n"); + const resultByInvocation = new Map([ + [ + ["sh", "-c", 'command -v "$1"', "--", "openshell"].join("\0"), + commandResult("/usr/local/bin/openshell\n", 0), + ], + [ + ["/usr/local/bin/openshell", "status", "-g", "nemoclaw-readiness-test"].join("\0"), + commandResult("", 1, statusConnectionRefused), + ], + [ + ["/usr/local/bin/openshell", "gateway", "info", "-g", "nemoclaw-readiness-test"].join("\0"), + commandResult("", 1, infoConnectionRefused), + ], + [ + ["/usr/local/bin/openshell", "gateway", "info"].join("\0"), + commandResult("", 1, infoConnectionRefused), + ], + ]); subprocess.spawnSync.mockImplementation((command: string, args: readonly string[] = []) => { - const resolvesOpenshell = - command === "sh" && - args.length === 4 && - args[0] === "-c" && - args[1] === 'command -v "$1"' && - args[2] === "--" && - args[3] === "openshell"; - if (resolvesOpenshell) return commandResult("/usr/local/bin/openshell\n", 0); - if (command === "/usr/local/bin/openshell") { - const commandLine = args.join("\0"); - if (commandLine === ["status", "-g", "nemoclaw-readiness-test"].join("\0")) { - return commandResult("", 1, statusConnectionRefused); - } - if ( - commandLine === ["gateway", "info", "-g", "nemoclaw-readiness-test"].join("\0") || - commandLine === ["gateway", "info"].join("\0") - ) { - return commandResult("", 1, infoConnectionRefused); - } - } - return commandResult(); + return resultByInvocation.get([command, ...args].join("\0")) ?? commandResult(); }); const gatewayPort = 0; From a0634bbb1662162aecd5517f40c4c6c2baba7f62 Mon Sep 17 00:00:00 2001 From: Julie Yaunches Date: Mon, 10 Aug 2026 17:54:25 -0400 Subject: [PATCH 9/9] test(cli): restore readiness test environment Signed-off-by: Julie Yaunches --- test/onboard-gateway-port-conflict-fast-fail.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/onboard-gateway-port-conflict-fast-fail.test.ts b/test/onboard-gateway-port-conflict-fast-fail.test.ts index 25d79cc34f9..89f941f947c 100644 --- a/test/onboard-gateway-port-conflict-fast-fail.test.ts +++ b/test/onboard-gateway-port-conflict-fast-fail.test.ts @@ -80,6 +80,7 @@ describe("onboard gateway port conflict readiness (#6752)", () => { }); afterEach(async () => { + vi.unstubAllEnvs(); await new Promise((resolve) => gatewayServer.close(() => resolve())); workspace.remove(); });