diff --git a/src/lib/actions/sandbox/gateway-select.test.ts b/src/lib/actions/sandbox/gateway-select.test.ts index 488dd1d7456..9388d81198d 100644 --- a/src/lib/actions/sandbox/gateway-select.test.ts +++ b/src/lib/actions/sandbox/gateway-select.test.ts @@ -20,10 +20,26 @@ describe("selectSandboxOwningGateway", () => { expect(selected).toEqual({ outcome: "selected", gatewayName: "nemoclaw-8091" }); expect(run).toHaveBeenCalledWith( ["gateway", "select", "nemoclaw-8091"], - expect.objectContaining({ ignoreError: true }), + expect.objectContaining({ + ignoreError: true, + stdio: ["inherit", "pipe", "inherit"], + }), ); }); + it("replays selection output through the stdio adapter", () => { + vi.spyOn(registry, "getSandbox").mockReturnValue({ gatewayPort: 8080 } as never); + const output = "\u001b[32m✓ Active gateway set to 'nemoclaw'\u001b[0m\n"; + const run = vi.fn(() => ({ status: 0, stdout: output }) as never); + const write = vi.fn(); + + expect(selectSandboxOwningGateway("alpha", run, write)).toEqual({ + outcome: "selected", + gatewayName: "nemoclaw", + }); + expect(write).toHaveBeenCalledWith(output); + }); + it("keeps the bare default gateway name for a default-port sandbox", () => { vi.spyOn(registry, "getSandbox").mockReturnValue({ gatewayPort: 8080 } as never); const run = vi.fn(() => ({ status: 0 }) as never); diff --git a/src/lib/actions/sandbox/gateway-select.ts b/src/lib/actions/sandbox/gateway-select.ts index ef2e0aa576c..e82098bfb7e 100644 --- a/src/lib/actions/sandbox/gateway-select.ts +++ b/src/lib/actions/sandbox/gateway-select.ts @@ -3,9 +3,11 @@ import { runOpenshell } from "../../adapters/openshell/runtime"; import { OPENSHELL_OPERATION_TIMEOUT_MS } from "../../adapters/openshell/timeouts"; +import { writeStderr } from "../../adapters/stdio"; import { getKnownSandboxTargetGatewayName } from "./gateway-target"; export type GatewaySelectRunner = typeof runOpenshell; +export type GatewaySelectOutputWriter = typeof writeStderr; export type GatewaySelectResult = | { outcome: "selected"; gatewayName: string } @@ -15,13 +17,16 @@ export type GatewaySelectResult = export function selectSandboxOwningGateway( sandboxName: string, run: GatewaySelectRunner = runOpenshell, + writeOutput: GatewaySelectOutputWriter = writeStderr, ): GatewaySelectResult { const targetGatewayName = getKnownSandboxTargetGatewayName(sandboxName); if (!targetGatewayName) return { outcome: "unregistered", gatewayName: null }; const result = run(["gateway", "select", targetGatewayName], { ignoreError: true, + stdio: ["inherit", "pipe", "inherit"], timeout: OPENSHELL_OPERATION_TIMEOUT_MS, }); + if (result.stdout) writeOutput(result.stdout); if (result.error || result.status !== 0) { return { outcome: "failed", gatewayName: targetGatewayName }; } diff --git a/src/lib/adapters/stdio.ts b/src/lib/adapters/stdio.ts new file mode 100644 index 00000000000..b1d9f16815e --- /dev/null +++ b/src/lib/adapters/stdio.ts @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +export function writeStderr(output: string): void { + process.stderr.write(output); +} diff --git a/test/cli/sandbox-status-json.test.ts b/test/cli/sandbox-status-json.test.ts index 970420a9a37..246c67fa1e2 100644 --- a/test/cli/sandbox-status-json.test.ts +++ b/test/cli/sandbox-status-json.test.ts @@ -133,6 +133,10 @@ describe("CLI sandbox status JSON output", testTimeoutOptions(20_000), () => { path.join(localBin, "openshell"), [ "#!/usr/bin/env bash", + 'if [ "$1" = "gateway" ] && [ "$2" = "select" ]; then', + " printf \"\\033[32m✓ Active gateway set to 'nemoclaw'\\033[0m\\n\"", + " exit 0", + "fi", 'if [ "$1" = "inference" ] && [ "$2" = "get" ]; then', " echo 'Gateway inference:'", " echo", @@ -168,6 +172,7 @@ describe("CLI sandbox status JSON output", testTimeoutOptions(20_000), () => { expect(r.out.trim().endsWith("}")).toBe(true); expect(r.out).not.toContain("Sandbox: "); expect(r.out).not.toContain("Nonexistent flag: --json"); + expect(r.out).not.toContain("Active gateway set"); const parsed = JSON.parse(r.out); expect(parsed).toMatchObject({ diff --git a/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh b/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh index 5c3425673d1..450b9a83f32 100755 --- a/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh +++ b/test/e2e/e2e-cloud-experimental/checks/04-deepagents-code-fresh-reonboard.sh @@ -257,15 +257,9 @@ if (JSON.parse(process.env.CONFIG_MODEL_JSON) !== "openai:" + process.env.MODEL_ ' || fail "keyed config get did not return model B after re-onboard" pass "keyed config get reports model B after re-onboard" -status_output="$("$CLI" "$SANDBOX_NAME" status --json 2>&1)" || fail "nemoclaw status failed after re-onboard: $status_output" -# gateway-select.ts invokes OpenShell with inherited stdio. Remove this exception when owning-gateway -# selection is quiet for machine-readable status output. -STATUS_OUTPUT="$status_output" SANDBOX_NAME="$SANDBOX_NAME" MODEL_B="$model_b" node -e ' -const lines = process.env.STATUS_OUTPUT.split(/\r?\n/); -const banner = "✓ Active gateway set to '\''nemoclaw'\''"; -const plainFirstLine = lines[0].replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, ""); -if (plainFirstLine === banner) lines.shift(); -const status = JSON.parse(lines.join("\n")); +status_json="$("$CLI" "$SANDBOX_NAME" status --json)" || fail "nemoclaw status failed after re-onboard: ${status_json:-}" +STATUS_JSON="$status_json" SANDBOX_NAME="$SANDBOX_NAME" MODEL_B="$model_b" node -e ' +const status = JSON.parse(process.env.STATUS_JSON); if (status.name !== process.env.SANDBOX_NAME || status.model !== process.env.MODEL_B || status.provider !== "compatible-endpoint") process.exit(1);