diff --git a/src/lib/onboard/machine/handlers/sandbox-messaging.test.ts b/src/lib/onboard/machine/handlers/sandbox-messaging.test.ts index ce7e784b1ca..eceece82f44 100644 --- a/src/lib/onboard/machine/handlers/sandbox-messaging.test.ts +++ b/src/lib/onboard/machine/handlers/sandbox-messaging.test.ts @@ -390,13 +390,13 @@ describe("reconcileReusedSandboxMessaging", () => { it("omits a retired host-backed channel from a reused sandbox selection (#9283)", () => { const plan = discordPlan(hashCredential("previous-discord-token") ?? ""); - const clearPlanEnv = vi.fn(); + const deps = { clearPlanEnv: vi.fn(), note: vi.fn(), writePlanToEnv: vi.fn() }; vi.stubEnv("DISCORD_BOT_TOKEN", ""); const result = reconcileReusedSandboxMessaging( structuredClone(plan), { name: "openclaw" }, - { clearPlanEnv, note: vi.fn(), writePlanToEnv: vi.fn() }, + deps, plan, ); @@ -407,7 +407,7 @@ describe("reconcileReusedSandboxMessaging", () => { selectedChannels: [], changed: true, }); - expect(clearPlanEnv).not.toHaveBeenCalled(); + expect(deps.clearPlanEnv).not.toHaveBeenCalled(); }); it("keeps a still-configured channel in a reused sandbox selection (#9283)", () => { diff --git a/test/e2e/support/e2e-collaborator-permission-retry.test.ts b/test/e2e/support/e2e-collaborator-permission-retry.test.ts index 635d785e2ae..fd352addd78 100644 --- a/test/e2e/support/e2e-collaborator-permission-retry.test.ts +++ b/test/e2e/support/e2e-collaborator-permission-retry.test.ts @@ -36,9 +36,9 @@ const AUTHORIZATION_STEPS: AuthorizationStep[] = [ name: "Authorize release qualification waiver", }, { - deniedMessage: "Launchable image publication requires a repository maintainer or administrator", - mismatchMessage: "Launchable image publication permission response did not match the actor", - name: "Authorize Launchable image publication", + deniedMessage: "Launchable E2E requires a repository maintainer or administrator", + mismatchMessage: "Launchable E2E permission response did not match the actor", + name: "Authorize Launchable E2E maintainer dispatch", }, ]; diff --git a/test/inference-set-config-read-exit.test.ts b/test/inference-set-config-read-exit.test.ts new file mode 100644 index 00000000000..34394346874 --- /dev/null +++ b/test/inference-set-config-read-exit.test.ts @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { testTimeoutOptions } from "./helpers/timeouts"; + +const CLI = path.join(import.meta.dirname, "..", "bin", "nemoclaw.js"); +const SANDBOX = "issue-9104-alpha"; + +describe("inference set sandbox configuration read failures", () => { + let home: string; + let openshell: string; + let openshellLog: string; + let registryFile: string; + + beforeEach(() => { + home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-9104-")); + openshell = path.join(home, "openshell"); + openshellLog = path.join(home, "openshell.log"); + fs.writeFileSync( + openshell, + [ + "#!/usr/bin/env bash", + `printf '%s\\n' "$*" >> ${JSON.stringify(openshellLog)}`, + "printf '%s\\n' 'exec session setup failed: container not ready' >&2", + "exit 1", + ].join("\n"), + { mode: 0o755 }, + ); + + const registryDir = path.join(home, ".nemoclaw"); + fs.mkdirSync(registryDir, { recursive: true }); + registryFile = path.join(registryDir, "sandboxes.json"); + fs.writeFileSync( + registryFile, + JSON.stringify({ + sandboxes: { + [SANDBOX]: { + agent: "openclaw", + gpuEnabled: false, + model: "nvidia/llama-3.3-nemotron-super-49b-v1", + name: SANDBOX, + policies: [], + provider: "nvidia-prod", + }, + }, + defaultSandbox: SANDBOX, + }), + { mode: 0o600 }, + ); + }); + + afterEach(() => { + fs.rmSync(home, { force: true, recursive: true }); + }); + + it.each([ + [ + "global", + [ + "inference", + "set", + "--provider", + "nvidia-prod", + "--model", + "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", + "--sandbox", + SANDBOX, + "--no-verify", + ], + ], + [ + "sandbox-first", + [ + SANDBOX, + "inference", + "set", + "--provider", + "nvidia-prod", + "--model", + "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", + "--no-verify", + ], + ], + ])( + "%s inference set exits with status 1 when OpenShell cannot read the sandbox configuration (#9104)", + testTimeoutOptions(30_000), + (_grammar, argv) => { + const registryBefore = fs.readFileSync(registryFile, "utf8"); + const result = spawnSync(process.execPath, [CLI, ...argv], { + encoding: "utf8", + env: { + ...process.env, + HOME: home, + NEMOCLAW_OPENSHELL_BIN: openshell, + NEMOCLAW_STATUS_PROBE_TIMEOUT_MS: "2000", + NEMOCLAW_TEST_NO_SLEEP: "1", + }, + killSignal: "SIGKILL", + timeout: 30_000, + }); + const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`; + + expect(result.error).toBeUndefined(); + expect(result.signal).toBeNull(); + expect(output).toContain("Cannot read openclaw config (/sandbox/.openclaw/openclaw.json)"); + expect(output).not.toContain("Setting OpenShell inference route"); + expect(fs.readFileSync(registryFile, "utf8")).toBe(registryBefore); + const openshellCalls = fs.readFileSync(openshellLog, "utf8").trim().split("\n"); + expect(openshellCalls).toHaveLength(1); + expect(openshellCalls[0]).toContain("sandbox exec"); + expect(openshellCalls[0]).toContain("cat /sandbox/.openclaw/openclaw.json"); + expect(openshellCalls).not.toContainEqual(expect.stringMatching(/\binference set\b/u)); + expect(result.status).toBe(1); + }, + ); +});