From cd74a8a2d42786f3e437514030e025a6a6301917 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 27 Aug 2026 01:40:02 -0700 Subject: [PATCH 01/13] test(onboard): centralize sandbox identity fixtures Signed-off-by: Apurv Kumaria --- ci/test-file-size-budget.json | 2 +- test/helpers/managed-image-buildless-e2e.ts | 37 ++- .../onboard-created-sandbox-fixture.test.ts | 134 ++++++++++ test/helpers/onboard-script-mocks.cjs | 191 +++++++++++---- test/helpers/vitest-watch-triggers.ts | 6 + .../onboard-custom-dockerfile.test.ts | 18 +- ...oard-extra-provider-reconciliation.test.ts | 52 ++-- .../onboard-fresh-create-identity.test.ts | 44 ++-- .../onboard-installer-restore-intent.test.ts | 37 ++- test/onboarding/onboard-messaging.test.ts | 94 ++++--- .../onboard-prepared-build-context.test.ts | 17 +- .../onboard-reservation-recreate.test.ts | 35 +-- test/onboarding/onboard-sandbox-build.test.ts | 92 +++---- .../onboard-sandbox-recreation.test.ts | 230 +++++++----------- .../onboard-terminal-dashboard.test.ts | 22 +- test/repository/vitest-watch-triggers.test.ts | 8 +- test/security/shellquote-sandbox.test.ts | 27 +- 17 files changed, 604 insertions(+), 442 deletions(-) create mode 100644 test/helpers/onboard-created-sandbox-fixture.test.ts diff --git a/ci/test-file-size-budget.json b/ci/test-file-size-budget.json index 635e2984d6e..f6ad300359c 100644 --- a/ci/test-file-size-budget.json +++ b/ci/test-file-size-budget.json @@ -8,7 +8,7 @@ "test/generation/generate-openclaw-config.test.ts": 1906, "test/installer-integration/install-preflight.test.ts": 3025, "test/agents/openclaw/runtime/nemoclaw-start.test.ts": 4626, - "test/onboarding/onboard-messaging.test.ts": 1979, + "test/onboarding/onboard-messaging.test.ts": 1977, "test/onboarding/onboard-selection.test.ts": 4176 } } diff --git a/test/helpers/managed-image-buildless-e2e.ts b/test/helpers/managed-image-buildless-e2e.ts index 25978e9350b..4cad383c6c5 100644 --- a/test/helpers/managed-image-buildless-e2e.ts +++ b/test/helpers/managed-image-buildless-e2e.ts @@ -149,7 +149,6 @@ const managedBootstrapCalls = []; const registerCalls = []; const runnerCommands = []; const spawnCalls = []; -let sandboxCreated = recreate; let existingEntryAvailable = recreate; let registeredSandbox = null; let managedHermesVolume = recreate ? { @@ -193,6 +192,11 @@ const replace = (target, name, value) => { const childProcess = require("node:child_process"); const fixtureMocks = require(${source("test/helpers/onboard-script-mocks.cjs")}); fixtureMocks.mockStandaloneGatewayTeardownAuthority(); +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ + sandboxName, + sandboxId: "fixture-managed-sandbox", + lifecycleState: recreate ? "created" : "absent", +}); const coreVersion = require(${source("src/lib/core/version.ts")}); replace(coreVersion, "getVersion", () => catalogRelease); @@ -420,16 +424,13 @@ runner.run = (command, options = {}) => { const argv = Array.isArray(command) ? command.map(String) : []; const normalized = normalize(command); runnerCommands.push(normalized); - sandboxCreated = normalized.includes("sandbox delete") ? false : sandboxCreated; - existingEntryAvailable = normalized.includes("sandbox delete") ? false : existingEntryAvailable; + if (normalized.includes("sandbox delete")) { + createdSandbox.delete(); + existingEntryAvailable = false; + } if (/(?:^|\s)docker(?:\s+buildx)?\s+build(?:\s|$)/u.test(normalized)) { return poison("docker build"); } - if (normalized.includes("sandbox get") && normalized.includes(sandboxName)) { - return sandboxCreated - ? { status: 0, stdout: "Name: " + sandboxName + "\nId: fixture-managed-sandbox\n", stderr: "" } - : { status: 1, stdout: "", stderr: "sandbox not found" }; - } if (argv[0] === "docker" && argv[1] === "volume") { const volumeName = argv.at(-1); if (argv[2] === "inspect") { @@ -449,16 +450,13 @@ runner.run = (command, options = {}) => { return { status: 0, stdout: volumeName + "\n", stderr: "" }; } } - return { status: 0, stdout: "", stderr: "" }; + return createdSandbox.run(command) ?? { status: 0, stdout: "", stderr: "" }; }; runner.runFile = (file, args = []) => runner.run([file, ...args]); runner.runCapture = (command) => { const normalized = normalize(command); runnerCommands.push(normalized); - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command, { - sandboxName, - sandboxId: "fixture-managed-sandbox", - }); + const createdIdentity = createdSandbox.capture(command); if (createdIdentity !== null) return createdIdentity; if (normalized.includes("policy get") && normalized.includes("--output json")) { return JSON.stringify({ @@ -474,12 +472,6 @@ runner.runCapture = (command) => { if (normalized.includes("gateway info")) { return "Gateway endpoint: http://127.0.0.1:8080"; } - if (normalized.includes("sandbox get") && normalized.includes(sandboxName)) { - return sandboxCreated - ? "Name: " + sandboxName + "\nId: fixture-managed-sandbox\nState: Ready" - : ""; - } - if (normalized.includes("sandbox list")) return sandboxName + " Ready"; if (normalized.includes("forward list")) { return sandboxName + " 127.0.0.1 18789 23189 running"; } @@ -540,7 +532,7 @@ const sourceEntry = recreate ? fixtureMocks.managedSandboxPolicyReceiptFixture({ credentialProxyReplayRequired: true, shared: true, }, -}, { sandboxName, sandboxId: "fixture-managed-sandbox" }) : null; +}, { sandboxName, sandboxId: createdSandbox.state.sandboxId }) : null; registry.getSandbox = () => registeredSandbox ?? (existingEntryAvailable ? sourceEntry : null); registry.getDefault = () => null; registry.listExtraProviders = () => []; @@ -575,7 +567,10 @@ childProcess.spawn = (command, args = [], options = {}) => { if (/(?:^|\s)docker(?:\s+buildx)?\s+build(?:\s|$)/u.test(normalized)) { return poison("docker build"); } - if (normalized.includes("sandbox create")) sandboxCreated = true; + if (normalized.includes("sandbox create")) { + if (createdSandbox.state.lifecycleState === "deleted") createdSandbox.recreate(); + else createdSandbox.create(); + } spawnCalls.push({ command: String(command), args: argv }); const child = new EventEmitter(); child.stdout = new EventEmitter(); diff --git a/test/helpers/onboard-created-sandbox-fixture.test.ts b/test/helpers/onboard-created-sandbox-fixture.test.ts new file mode 100644 index 00000000000..a6e6210f1f1 --- /dev/null +++ b/test/helpers/onboard-created-sandbox-fixture.test.ts @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; + +import { + parseOpenShellSandboxId, + parseStrictOpenShellSandboxListJson, +} from "../../src/lib/adapters/openshell/sandbox-identity"; + +type CreatedSandboxFixture = { + readonly capture: (command: string[]) => string | null; + readonly create: () => void; + readonly delete: () => void; + readonly recreate: () => void; + readonly setPhase: (phase: string) => void; + readonly state: Readonly<{ + sandboxName: string; + sandboxId: string; + gatewayName: string; + phase: string; + lifecycleState: string; + generation: number; + }>; +}; + +const { createCreatedSandboxFixture } = require("./onboard-script-mocks.cjs") as { + createCreatedSandboxFixture: (options?: Record) => CreatedSandboxFixture; +}; + +const CREATE_ATTEMPT_NONCE = "a".repeat(62); + +function selectorListCommand(gatewayName: string): string[] { + return [ + "openshell", + "sandbox", + "list", + "-g", + gatewayName, + "--selector", + `ai.nvidia.nemoclaw.create-attempt=${CREATE_ATTEMPT_NONCE}`, + "--output", + "json", + "--limit", + "2", + ]; +} + +describe("created sandbox fixture", () => { + it("uses one ID for create, list, and get observations (#10463)", () => { + const fixture = createCreatedSandboxFixture({ + sandboxName: "alpha", + sandboxId: "sandbox-alpha", + gatewayName: "gateway-alpha", + }); + + expect(fixture.capture(selectorListCommand("gateway-alpha"))).toBe("[]"); + fixture.create(); + const createdSandboxId = fixture.state.sandboxId; + + const selectorOutput = fixture.capture(selectorListCommand("gateway-alpha")); + const rows = parseStrictOpenShellSandboxListJson(selectorOutput ?? ""); + expect(rows).toHaveLength(1); + expect(rows?.[0]?.id).toBe(createdSandboxId); + + const listOutput = fixture.capture(["openshell", "sandbox", "list", "-g", "gateway-alpha"]); + expect(listOutput).toBe("alpha Ready\n"); + expect(fixture.state.sandboxId).toBe(createdSandboxId); + + const getOutput = fixture.capture([ + "openshell", + "sandbox", + "get", + "-g", + "gateway-alpha", + "alpha", + ]); + expect(parseOpenShellSandboxId(getOutput ?? "")).toBe(createdSandboxId); + }); + + it("invalidates the prior ID before recreation publishes a new ID (#10463)", () => { + const fixture = createCreatedSandboxFixture({ + sandboxName: "alpha", + sandboxId: "sandbox-alpha", + gatewayName: "gateway-alpha", + }); + fixture.create(); + const priorSandboxId = fixture.state.sandboxId; + + fixture.delete(); + expect(fixture.capture(selectorListCommand("gateway-alpha"))).toBe("[]"); + expect(fixture.capture(["openshell", "sandbox", "get", "-g", "gateway-alpha", "alpha"])).toBe( + "", + ); + + fixture.recreate(); + const replacementSandboxId = fixture.state.sandboxId; + expect(replacementSandboxId).not.toBe(priorSandboxId); + const replacementRows = parseStrictOpenShellSandboxListJson( + fixture.capture(selectorListCommand("gateway-alpha")) ?? "", + ); + expect(replacementRows?.[0]?.id).toBe(replacementSandboxId); + expect(replacementRows?.[0]?.id).not.toBe(priorSandboxId); + expect( + parseOpenShellSandboxId( + fixture.capture(["openshell", "sandbox", "get", "-g", "gateway-alpha", "alpha"]) ?? "", + ), + ).toBe(replacementSandboxId); + }); + + it.each([ + ["a missing", undefined], + ["an empty", ""], + ["a malformed", "invalid/id"], + ])("rejects %s durable sandbox ID (#10463)", (_case, sandboxId) => { + expect(() => createCreatedSandboxFixture({ sandboxId })).toThrow( + "Created sandbox fixture requires one durable sandbox ID.", + ); + }); + + it("does not answer an identity observation for another gateway (#10463)", () => { + const fixture = createCreatedSandboxFixture({ + sandboxName: "alpha", + sandboxId: "sandbox-alpha", + gatewayName: "gateway-alpha", + }); + fixture.create(); + + expect(fixture.capture(selectorListCommand("gateway-bravo"))).toBeNull(); + expect( + fixture.capture(["openshell", "sandbox", "get", "-g", "gateway-bravo", "alpha"]), + ).toBeNull(); + }); +}); diff --git a/test/helpers/onboard-script-mocks.cjs b/test/helpers/onboard-script-mocks.cjs index 1d30af8ca2f..e2eafda1288 100644 --- a/test/helpers/onboard-script-mocks.cjs +++ b/test/helpers/onboard-script-mocks.cjs @@ -96,11 +96,7 @@ function mockManagedEndpointlessProviderProfileRun(command) { ); } -function createStatefulMessagingProviderRunner({ - commands, - initialProviders = [], - readySandboxName = null, -}) { +function createStatefulMessagingProviderRunner({ commands, initialProviders = [] }) { const providers = new Map( initialProviders.map(([name, type, credential]) => [name, { type, credential }]), ); @@ -204,18 +200,6 @@ function createStatefulMessagingProviderRunner({ stderr: Buffer.alloc(0), }; } - if ( - readySandboxName && - args.includes("sandbox") && - args.includes("get") && - args.includes(readySandboxName) - ) { - return { - status: 0, - stdout: Buffer.from(`Name: ${readySandboxName}\nId: sbx-4f2a91c0d7\n`), - stderr: Buffer.alloc(0), - }; - } return { status: 0 }; }; } @@ -333,33 +317,152 @@ function mockOnboardRunCapture(command, options = {}) { return mockSandboxExecCurl(command, options); } -function mockCreatedSandboxIdentityList(command, options = {}) { - const args = Array.isArray(command) ? command.map(String) : []; - const sandboxIndex = args.indexOf("sandbox"); - if ( - sandboxIndex < 0 || - args[sandboxIndex + 1] !== "list" || - !args.includes("--output") || - args[args.indexOf("--output") + 1] !== "json" - ) { - return null; - } - const selectorIndex = args.indexOf("--selector"); - const selector = selectorIndex >= 0 ? args[selectorIndex + 1] || "" : ""; - const prefix = "ai.nvidia.nemoclaw.create-attempt="; - if (!selector.startsWith(prefix)) return null; - const nonce = selector.slice(prefix.length); - return JSON.stringify([ - { - id: options.sandboxId || "fixture-created-sandbox", - name: options.sandboxName || "my-assistant", - labels: { "ai.nvidia.nemoclaw.create-attempt": nonce }, - resource_version: 1, - created_at: "2026-08-25T00:00:00Z", - phase: "Ready", - current_policy_version: 1, +function createCreatedSandboxFixture(options = {}) { + const sandboxIdentity = require( + path.resolve(__dirname, "../../src/lib/adapters/openshell/sandbox-identity.ts"), + ); + const initialSandboxId = hasOwn(options, "sandboxId") + ? options.sandboxId + : "fixture-created-sandbox"; + const initialLifecycleState = hasOwn(options, "lifecycleState") + ? options.lifecycleState + : "absent"; + const state = { + sandboxName: hasOwn(options, "sandboxName") ? options.sandboxName : "my-assistant", + sandboxId: initialSandboxId, + gatewayName: hasOwn(options, "gatewayName") ? options.gatewayName : "nemoclaw", + phase: hasOwn(options, "phase") ? options.phase : "Ready", + lifecycleState: initialLifecycleState, + generation: initialLifecycleState === "created" ? 1 : 0, + }; + const lifecycleStates = new Set(["absent", "created", "deleted"]); + + const assertState = () => { + if ( + typeof state.sandboxName !== "string" || + state.sandboxName.length === 0 || + state.sandboxName.trim() !== state.sandboxName + ) { + throw new Error("Created sandbox fixture requires one sandbox name."); + } + if (!sandboxIdentity.isOpenShellSandboxId(state.sandboxId)) { + throw new Error("Created sandbox fixture requires one durable sandbox ID."); + } + if ( + typeof state.gatewayName !== "string" || + state.gatewayName.length === 0 || + state.gatewayName.trim() !== state.gatewayName + ) { + throw new Error("Created sandbox fixture requires one gateway name."); + } + if (typeof state.phase !== "string" || state.phase.length === 0) { + throw new Error("Created sandbox fixture requires one sandbox phase."); + } + if (!lifecycleStates.has(state.lifecycleState)) { + throw new Error("Created sandbox fixture requires one known lifecycle state."); + } + }; + + const commandDetails = (command) => { + const args = Array.isArray(command) ? command.map(String) : []; + const sandboxIndex = args.indexOf("sandbox"); + if (sandboxIndex < 0) return null; + const gatewayIndex = args.findIndex((arg) => arg === "-g" || arg === "--gateway"); + const gatewayName = gatewayIndex >= 0 ? args[gatewayIndex + 1] || null : null; + if (gatewayName !== null && gatewayName !== state.gatewayName) return null; + return { args, action: args[sandboxIndex + 1] || null }; + }; + + const isCreated = () => state.lifecycleState === "created"; + const capture = (command) => { + const details = commandDetails(command); + if (!details) return null; + const { args, action } = details; + if (action === "get") { + const sandboxName = args.at(-1); + if (sandboxName !== state.sandboxName) return null; + return isCreated() + ? `Name: ${state.sandboxName}\nId: ${state.sandboxId}\nPhase: ${state.phase}\n` + : ""; + } + if (action !== "list") return null; + + const selectorIndex = args.indexOf("--selector"); + if (selectorIndex >= 0) { + const selector = args[selectorIndex + 1] || ""; + const prefix = `${sandboxIdentity.NEMOCLAW_CREATE_ATTEMPT_LABEL}=`; + if (!selector.startsWith(prefix)) return null; + if (!args.includes("--output") || args[args.indexOf("--output") + 1] !== "json") { + return null; + } + if (!isCreated()) return "[]"; + const nonce = selector.slice(prefix.length); + return JSON.stringify([ + { + id: state.sandboxId, + name: state.sandboxName, + labels: { [sandboxIdentity.NEMOCLAW_CREATE_ATTEMPT_LABEL]: nonce }, + resource_version: state.generation, + created_at: "2026-08-25T00:00:00Z", + phase: state.phase, + current_policy_version: 1, + }, + ]); + } + + return isCreated() ? `${state.sandboxName} ${state.phase}\n` : "No sandboxes found.\n"; + }; + + const run = (command) => { + const output = capture(command); + return output === null + ? null + : { status: 0, stdout: Buffer.from(output), stderr: Buffer.alloc(0) }; + }; + + const create = () => { + if (state.lifecycleState === "created") return; + if (state.lifecycleState !== "absent") { + throw new Error("Created sandbox fixture cannot create a deleted sandbox."); + } + state.lifecycleState = "created"; + state.generation += 1; + }; + + const deleteSandbox = () => { + if (state.lifecycleState !== "created") { + throw new Error("Created sandbox fixture can delete only a created sandbox."); + } + state.lifecycleState = "deleted"; + }; + + const recreate = () => { + if (state.lifecycleState !== "deleted") { + throw new Error("Created sandbox fixture can recreate only a deleted sandbox."); + } + state.generation += 1; + state.sandboxId = `${initialSandboxId}-recreated-${state.generation - 1}`; + assertState(); + state.lifecycleState = "created"; + }; + + const setPhase = (phase) => { + state.phase = phase; + assertState(); + }; + + assertState(); + return Object.freeze({ + capture, + create, + delete: deleteSandbox, + recreate, + run, + setPhase, + get state() { + return Object.freeze({ ...state }); }, - ]); + }); } function installVerifiedSandboxCreateFixture(registry, options) { @@ -930,7 +1033,7 @@ module.exports = { isOpenClawSecurityInventoryProbe, mockDockerSandboxLifecycleReleaseFromRunner, mockFreshOpenClawPluginDiscovery, - mockCreatedSandboxIdentityList, + createCreatedSandboxFixture, installVerifiedSandboxCreateFixture, managedSandboxPolicyReceiptFixture, mockOnboardRunCapture, diff --git a/test/helpers/vitest-watch-triggers.ts b/test/helpers/vitest-watch-triggers.ts index 5790178bfb8..f671757173f 100644 --- a/test/helpers/vitest-watch-triggers.ts +++ b/test/helpers/vitest-watch-triggers.ts @@ -53,13 +53,19 @@ export const vitestWatchTriggerPatterns: VitestWatchTriggerPattern[] = [ { pattern: /(?:^|\/)test\/helpers\/onboard-script-mocks\.cjs$/, testsToRun: runTests( + "test/helpers/onboard-created-sandbox-fixture.test.ts", + "test/onboarding/onboard-custom-dockerfile.test.ts", "test/onboarding/onboard-extra-provider-reconciliation.test.ts", + "test/onboarding/onboard-fresh-create-identity.test.ts", "test/onboarding/onboard-installer-restore-intent.test.ts", + "test/onboarding/onboard-managed-image-buildless-e2e.test.ts", "test/onboarding/onboard-messaging.test.ts", + "test/onboarding/onboard-prepared-build-context.test.ts", "test/onboarding/onboard-reservation-recreate.test.ts", "test/onboarding/onboard-sandbox-build.test.ts", "test/onboarding/onboard-sandbox-recreation.test.ts", "test/onboarding/onboard-terminal-dashboard.test.ts", + "test/security/shellquote-sandbox.test.ts", ), }, { diff --git a/test/onboarding/onboard-custom-dockerfile.test.ts b/test/onboarding/onboard-custom-dockerfile.test.ts index 5171a1dc40a..e3d20ca705f 100644 --- a/test/onboarding/onboard-custom-dockerfile.test.ts +++ b/test/onboarding/onboard-custom-dockerfile.test.ts @@ -192,6 +192,9 @@ describe("onboard custom Dockerfile", () => { const script = String.raw` const runner = require(${runnerPath}); const fixtureMocks = require(${onboardScriptMocksPath}); +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ + sandboxName: "my-assistant", +}); const _n = (c) => (Array.isArray(c) ? c.join(" ") : String(c)).replace(/'/g, ""); const registry = require(${registryPath}); const preflight = require(${preflightPath}); @@ -233,21 +236,15 @@ runner.run = (command, opts = {}) => { commands.push({ command: normalized, env: opts.env || null }); const profileResult = fixtureMocks.mockManagedEndpointlessProviderProfileRun(command); if (profileResult !== null) return profileResult; - if (normalized.includes("sandbox list")) return { status: 0, stdout: "No sandboxes found." }; - return normalized.includes("sandbox get") && normalized.includes("my-assistant") - ? { status: 0, stdout: Buffer.from("Name: my-assistant\nId: sbx-4f2a91c0d7\n"), stderr: Buffer.alloc(0) } - : { status: 0 }; + const sandboxResult = createdSandbox.run(command); + return sandboxResult ?? { status: 0 }; }; runner.runCapture = (command) => { const normalized = _n(command); if (normalized.includes("gateway info")) return "Gateway endpoint: http://127.0.0.1:8080"; if (normalized.includes("policy get") && normalized.includes("--output json")) return JSON.stringify({ scope: "sandbox", sandbox: "my-assistant", status: "effective", policy_source: "sandbox", hash: "fixture-policy", active_version: 1, policy: {} }); - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command, { - sandboxName: "my-assistant", - }); - if (createdIdentity !== null) return createdIdentity; - if (normalized.includes("sandbox get") && normalized.includes("my-assistant")) return ""; - if (normalized.includes("sandbox list")) return "my-assistant Ready"; + const sandboxCapture = createdSandbox.capture(command); + if (sandboxCapture !== null) return sandboxCapture; { const mockedCapture = fixtureMocks.mockOnboardRunCapture(command); if (mockedCapture !== null) return mockedCapture; @@ -265,6 +262,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); diff --git a/test/onboarding/onboard-extra-provider-reconciliation.test.ts b/test/onboarding/onboard-extra-provider-reconciliation.test.ts index 4d9a0cc1174..bb8ed6bb074 100644 --- a/test/onboarding/onboard-extra-provider-reconciliation.test.ts +++ b/test/onboarding/onboard-extra-provider-reconciliation.test.ts @@ -66,6 +66,7 @@ const { EventEmitter } = require("node:events"); const _n = (command) => (Array.isArray(command) ? command.join(" ") : String(command)).replace(/'/g, ""); const commands = []; +let createdSandbox = null; runner.run = (command, opts = {}) => { const normalized = _n(command); @@ -85,16 +86,13 @@ runner.run = (command, opts = {}) => { if (normalized.includes("provider get -g nemoclaw ")) { return { status: 0, stdout: "" }; } - return normalized.includes("sandbox get") && normalized.includes("my-assistant") - ? { status: 0, stdout: Buffer.from("my-assistant\nId: sbx-4f2a91c0d7\n"), stderr: Buffer.alloc(0) } - : { status: 0 }; + const sandboxResult = createdSandbox?.run(command) ?? null; + return sandboxResult ?? { status: 0 }; }; runner.runCapture = (command) => { const normalized = _n(command); - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command); - if (createdIdentity !== null) return createdIdentity; - if (normalized.includes("sandbox get") && normalized.includes("my-assistant")) return ""; - if (normalized.includes("sandbox list")) return "my-assistant Ready"; + const sandboxCapture = createdSandbox?.capture(command) ?? null; + if (sandboxCapture !== null) return sandboxCapture; const mockedCapture = require(${onboardScriptMocksPath}).mockOnboardRunCapture(command); if (mockedCapture !== null) return mockedCapture; if (normalized.includes("forward list")) { @@ -113,6 +111,7 @@ sandboxBaseImage.resolveSandboxBaseImage = () => ({ }); childProcess.spawn = (...args) => { + createdSandbox.create(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -131,23 +130,28 @@ childProcess.spawn = (...args) => { const { createSandbox } = require(${onboardPath}); -const createReservedSandbox = () => createSandbox( - null, - "gpt-5.4", - "nvidia-prod", - null, - null, - null, - null, - null, - null, - null, - null, - null, - [], - null, - { sessionId: createFixture.sessionId }, -); +const createReservedSandbox = () => { + createdSandbox = fixtureMocks.createCreatedSandboxFixture({ + sandboxName: "my-assistant", + }); + return createSandbox( + null, + "gpt-5.4", + "nvidia-prod", + null, + null, + null, + null, + null, + null, + null, + null, + null, + [], + null, + { sessionId: createFixture.sessionId }, + ); +}; (async () => { process.env.OPENSHELL_GATEWAY = "nemoclaw"; diff --git a/test/onboarding/onboard-fresh-create-identity.test.ts b/test/onboarding/onboard-fresh-create-identity.test.ts index adb631e980f..58f321fd518 100644 --- a/test/onboarding/onboard-fresh-create-identity.test.ts +++ b/test/onboarding/onboard-fresh-create-identity.test.ts @@ -54,11 +54,10 @@ describe("fresh create identity", () => { writeOkOpenshell(fakeBin); const script = String.raw` - const runner = require(${runnerPath}); +const runner = require(${runnerPath}); const fixtureMocks = require(${onboardScriptMocksPath}); fixtureMocks.mockStandaloneGatewayTeardownAuthority(); const _n = (c) => (Array.isArray(c) ? c.join(" ") : String(c)).replace(/'/g, ""); -let _deleted = false; const registry = require(${registryPath}); const preflight = require(${preflightPath}); const credentials = require(${credentialsPath}); @@ -74,9 +73,12 @@ const fs = require("node:fs"); const commands = []; const lifecycleObservationCommands = []; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ + sandboxName: "my-assistant", + sandboxId: "sbx-fresh-create", +}); let sandboxListCalls = 0; let dockerPsCalls = 0; -let sandboxCreated = false; let registeredSandbox = null; let effectivePolicy = {}; const keepAlive = setInterval(() => {}, 1000); @@ -85,14 +87,12 @@ runner.run = (command, opts = {}) => { const cmd = _n(command); const profileResult = require(${onboardScriptMocksPath}).mockEndpointlessProviderProfileRun(command, "nemoclaw-mcp-v1", false); if (profileResult !== null) return profileResult; - _deleted = _deleted || cmd.includes("sandbox delete"); - commands.push({ command: cmd, env: opts.env || null }); - if (cmd.includes("sandbox list")) { - return { status: 0, stdout: Buffer.from("No sandboxes found.\n"), stderr: Buffer.alloc(0) }; + if (cmd.includes("sandbox delete") && createdSandbox.state.lifecycleState === "created") { + createdSandbox.delete(); } - return cmd.includes("sandbox get") && cmd.includes("my-assistant") && sandboxCreated - ? { status: 0, stdout: Buffer.from("my-assistant\nId: sbx-fresh-create\n"), stderr: Buffer.alloc(0) } - : { status: 0 }; + commands.push({ command: cmd, env: opts.env || null }); + const sandboxResult = createdSandbox.run(command); + return sandboxResult ?? { status: 0 }; }; runner.runCapture = (command) => { const cmd = _n(command); @@ -101,22 +101,16 @@ runner.run = (command, opts = {}) => { if (cmd.includes("sandbox get") || cmd.includes("sandbox list")) { lifecycleObservationCommands.push(cmd); } - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command, { - sandboxName: "my-assistant", - sandboxId: "sbx-fresh-create", - }); - if (createdIdentity !== null) return createdIdentity; + if (cmd.includes("sandbox list") && !cmd.includes("--selector")) { + sandboxListCalls += 1; + createdSandbox.setPhase(sandboxListCalls >= 2 ? "Ready" : "Pending"); + } + const sandboxCapture = createdSandbox.capture(command); + if (sandboxCapture !== null) return sandboxCapture; if (cmd.startsWith("docker ps -a --no-trunc ")) { dockerPsCalls += 1; if (dockerPsCalls === 1) return "a".repeat(64); } - if (cmd.includes("sandbox get") && cmd.includes("my-assistant")) { - return sandboxCreated ? ["my-assistant", "Id: sbx-fresh-create"].join(String.fromCharCode(10)) : ""; - } - if (cmd.includes("sandbox list")) { - sandboxListCalls += 1; - return sandboxListCalls >= 2 ? "my-assistant Ready" : "my-assistant Pending"; - } { const mockedCapture = require(${onboardScriptMocksPath}).mockOnboardRunCapture(command); if (mockedCapture !== null) return mockedCapture; @@ -152,8 +146,7 @@ process.kill = (pid, signal) => { }; childProcess.spawn = (...args) => { - sandboxCreated = true; - _deleted = false; + createdSandbox.create(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -213,6 +206,7 @@ const { createSandbox } = require(${onboardPath}); stdoutDestroyCalls: createCommand.child.stdout.destroyCalls, stderrDestroyCalls: createCommand.child.stderr.destroyCalls, lifecycleObservationCommands, + sandboxId: createdSandbox.state.sandboxId, registeredSandbox, createCommand: createCommand.command, commandNames: commands.map((entry) => entry.command), @@ -251,7 +245,7 @@ const { createSandbox } = require(${onboardPath}); assert.match(payload.registeredSandbox.lifecycleGeneration, /^[0-9a-f-]{36}$/u); assert.equal( payload.registeredSandbox.lifecycleLiveIdentityFingerprint, - createHash("sha256").update("sbx-fresh-create").digest("hex"), + createHash("sha256").update(payload.sandboxId).digest("hex"), ); const assertPolicyMode = apfInterceptorRequested ? () => { diff --git a/test/onboarding/onboard-installer-restore-intent.test.ts b/test/onboarding/onboard-installer-restore-intent.test.ts index bf0bf2f39de..af6af55bce1 100644 --- a/test/onboarding/onboard-installer-restore-intent.test.ts +++ b/test/onboarding/onboard-installer-restore-intent.test.ts @@ -65,34 +65,29 @@ const { EventEmitter } = require("node:events"); const PRE_UPGRADE_BACKUP = "/tmp/fake-pre-upgrade-backup"; const events = []; -let sandboxDeleted = false; -let sandboxRecreated = false; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ + sandboxName: "my-assistant", + lifecycleState: "created", + phase: "NotReady", +}); runner.run = (command) => { const cmd = _n(command); events.push({ kind: "run", cmd }); const profileResult = fixtureMocks.mockManagedEndpointlessProviderProfileRun(command); if (profileResult !== null) return profileResult; - if (cmd.includes("sandbox list")) return { status: 0, stdout: "No sandboxes found." }; - if (cmd.includes("sandbox delete")) sandboxDeleted = true; - if (cmd.includes("sandbox list")) { - return { status: 0, stdout: Buffer.from("No sandboxes found.\n"), stderr: Buffer.alloc(0) }; + if (cmd.includes("sandbox delete")) { + createdSandbox.delete(); + return { status: 0 }; } - return cmd.includes("sandbox get") && cmd.includes("my-assistant") - ? { status: 0, stdout: Buffer.from("my-assistant\nId: fixture-created-sandbox\n"), stderr: Buffer.alloc(0) } - : { status: 0 }; + const sandboxResult = createdSandbox.run(command); + return sandboxResult ?? { status: 0 }; }; runner.runCapture = (command) => { const cmd = _n(command); if (cmd.includes("gateway info")) return "Gateway endpoint: http://127.0.0.1:8080"; if (cmd.includes("policy get") && cmd.includes("--output json")) return JSON.stringify({ scope: "sandbox", sandbox: "my-assistant", status: "effective", policy_source: "sandbox", hash: "fixture-policy", active_version: 1, policy: {} }); - if (sandboxRecreated) { - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command, { sandboxName: "my-assistant" }); - if (createdIdentity !== null) return createdIdentity; - } - if (cmd.includes("sandbox get") && cmd.includes("my-assistant")) return sandboxDeleted && !sandboxRecreated ? "" : ["my-assistant", "Id: fixture-created-sandbox"].join(String.fromCharCode(10)); - if (cmd.includes("sandbox list")) { - return sandboxRecreated ? "my-assistant Ready" : sandboxDeleted ? "" : "my-assistant NotReady"; - } + const sandboxCapture = createdSandbox.capture(command); + if (sandboxCapture !== null) return sandboxCapture; if (cmd.includes("forward list")) return "my-assistant 127.0.0.1 18789 12345 running"; { const mockedCapture = fixtureMocks.mockOnboardRunCapture(command, { @@ -145,13 +140,17 @@ const preflight = require(${JSON.stringify(path.join(repoRoot, "src", "lib", "on preflight.checkPortAvailable = async () => ({ ok: true }); childProcess.spawn = (...args) => { - sandboxRecreated = true; + const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); + if (command.includes("sandbox create")) { + createdSandbox.recreate(); + createdSandbox.setPhase("Ready"); + } const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); child.unref = () => {}; child.pid = 4245; - events.push({ kind: "spawn", cmd: _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]) }); + events.push({ kind: "spawn", cmd: command }); process.nextTick(() => { child.stdout.emit("data", Buffer.from("Created sandbox: my-assistant\n")); child.emit("close", 0); diff --git a/test/onboarding/onboard-messaging.test.ts b/test/onboarding/onboard-messaging.test.ts index a1bcd67f2cb..12f0677b49e 100644 --- a/test/onboarding/onboard-messaging.test.ts +++ b/test/onboarding/onboard-messaging.test.ts @@ -81,15 +81,13 @@ const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); const fs = require("node:fs"); const commands = []; -runner.run = require(${onboardScriptMocksPath}).createStatefulMessagingProviderRunner({ - commands, - readySandboxName: "my-assistant", -}); +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName: "my-assistant" }); +const messagingProviderRunner = fixtureMocks.createStatefulMessagingProviderRunner({ commands }); +runner.run = (command, options = {}) => + createdSandbox.run(command) ?? messagingProviderRunner(command, options); runner.runCapture = (command) => { - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command); - if (createdIdentity !== null) return createdIdentity; - if (_n(command).includes("sandbox get") && _n(command).includes("my-assistant")) return ""; - if (_n(command).includes("sandbox list")) return "my-assistant Ready"; + const sandboxCapture = createdSandbox.capture(command); + if (sandboxCapture !== null) return sandboxCapture; if (_n(command).includes("provider get")) return "Provider: discord-bridge"; if (_n(command).includes("forward list")) return "my-assistant 127.0.0.1 18789 12345 running\nmy-assistant 127.0.0.1 8642 12346 running"; { @@ -112,6 +110,7 @@ const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -365,15 +364,13 @@ const nonSlackMessagingEnvKeys = [ const commands = []; let registeredSandbox = null; -runner.run = require(${onboardScriptMocksPath}).createStatefulMessagingProviderRunner({ - commands, - readySandboxName: "my-assistant", -}); +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName: "my-assistant" }); +const messagingProviderRunner = fixtureMocks.createStatefulMessagingProviderRunner({ commands }); +runner.run = (command, options = {}) => + createdSandbox.run(command) ?? messagingProviderRunner(command, options); runner.runCapture = (command) => { - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command); - if (createdIdentity !== null) return createdIdentity; - if (_n(command).includes("sandbox get") && _n(command).includes("my-assistant")) return ""; - if (_n(command).includes("sandbox list")) return "my-assistant Ready"; + const sandboxCapture = createdSandbox.capture(command); + if (sandboxCapture !== null) return sandboxCapture; if (_n(command).includes("forward list")) return "my-assistant 127.0.0.1 18789 12345 running\nmy-assistant 127.0.0.1 8642 12346 running"; { const mockedCapture = require(${onboardScriptMocksPath}).mockOnboardRunCapture(command, { @@ -400,6 +397,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -528,6 +526,7 @@ const fixtureMocks = require(${onboardScriptMocksPath}); const _n = (c) => (Array.isArray(c) ? c.join(" ") : String(c)).replace(/'/g, ""); const childProcess = require("node:child_process"), { EventEmitter } = require("node:events"); const commands = [], credentialKeys = ${JSON.stringify(providerCredentialKeys)}; let registered = null; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName: "my-assistant" }); const providers = Object.keys(credentialKeys), revisions = new Map(providers.map((name) => [name, 1])); const rawGatewayCredential = ${JSON.stringify(rawGatewayCredential)}, gatewaySecrets = new Map(providers.map((name) => [name, rawGatewayCredential])); registry.registerSandbox({ name: "my-assistant", messaging: { schemaVersion: 1, plan: ${messagingPlanLiteral(["slack", "telegram", "whatsapp"])} } }); @@ -540,13 +539,11 @@ runner.run = (command, opts = {}) => { const refresh = normalized.match(/provider update -g nemoclaw ([^ ]+)$/)?.[1]; if (refresh && gatewaySecrets.has(refresh)) { if (refresh === process.env.NEMOCLAW_TEST_FAIL_PROVIDER) return { status: 1 }; revisions.set(refresh, revisions.get(refresh) + 1); return { status: 0 }; } if (normalized.includes("provider get")) return { status: 1 }; - return normalized.includes("sandbox get") && normalized.includes("my-assistant") ? { status: 0, stdout: Buffer.from("Name: my-assistant\nId: sbx-4f2a91c0d7\n"), stderr: Buffer.alloc(0) } : { status: 0 }; + return createdSandbox.run(command) ?? { status: 0 }; }; runner.runCapture = (command) => { - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command); - if (createdIdentity !== null) return createdIdentity; - if (_n(command).includes("sandbox get") && _n(command).includes("my-assistant")) return ""; - if (_n(command).includes("sandbox list")) return "my-assistant Ready"; + const sandboxCapture = createdSandbox.capture(command); + if (sandboxCapture !== null) return sandboxCapture; const mockedCapture = require(${onboardScriptMocksPath}).mockOnboardRunCapture(command); if (mockedCapture !== null) return mockedCapture; if (_n(command).includes("forward list")) return "my-assistant 127.0.0.1 18789 12345 running\nmy-assistant 127.0.0.1 8642 12346 running"; @@ -562,6 +559,7 @@ const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, }); preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); child.unref = () => {}; child.pid = 4242; const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); const attachedProviders = [...command.matchAll(/--provider ([^ ]+)/g)].map((match) => match[1]); commands.push({ command, providerRevisions: command.includes("sandbox create") ? Object.fromEntries(attachedProviders.map((name) => [name, revisions.get(name)])) : null, rawCredentialInEnv: Object.values(args[2]?.env || {}).includes(rawGatewayCredential) }); @@ -708,6 +706,9 @@ const fs = require("node:fs"); const commands = []; let dockerfileContent; const registerCalls = []; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ + sandboxName: "my-assistant", +}); registry.registerSandbox({ name: "my-assistant", messaging: { schemaVersion: 1, plan: ${messagingPlanLiteral(["telegram"], ["telegram"])} }, @@ -717,13 +718,11 @@ runner.run = (command, opts = {}) => { commands.push({ command: normalized, env: opts.env || null }); if (normalized.includes("provider get -g nemoclaw my-assistant-telegram-bridge")) return { status: 0, stdout: "Name: my-assistant-telegram-bridge\nType: nemoclaw-mcp-v1\nCredential keys: TELEGRAM_BOT_TOKEN\nConfig keys: \n" }; if (normalized.includes("provider get")) return { status: 1 }; - return normalized.includes("sandbox get") && normalized.includes("my-assistant") ? { status: 0, stdout: Buffer.from("Name: my-assistant\nId: sbx-4f2a91c0d7\n"), stderr: Buffer.alloc(0) } : { status: 0 }; + return createdSandbox.run(command) ?? { status: 0 }; }; runner.runCapture = (command) => { - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command); - if (createdIdentity !== null) return createdIdentity; - if (_n(command).includes("sandbox get") && _n(command).includes("my-assistant")) return ""; - if (_n(command).includes("sandbox list")) return "my-assistant Ready"; + const sandboxCapture = createdSandbox.capture(command); + if (sandboxCapture !== null) return sandboxCapture; { const mockedCapture = require(${onboardScriptMocksPath}).mockOnboardRunCapture(command); if (mockedCapture !== null) return mockedCapture; @@ -749,6 +748,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -875,17 +875,16 @@ const fs = require("node:fs"); const commands = []; let dockerfileContent; const registerCalls = []; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture(); runner.run = (command, opts = {}) => { const normalized = _n(command); - commands.push({ command: normalized, env: opts.env || null }); if (normalized.includes("sandbox list")) return { status: 0, stdout: "No sandboxes found." }; + commands.push({ command: normalized, env: opts.env || null }); if (normalized.includes("provider get")) return { status: 1 }; - return normalized.includes("sandbox get") && normalized.includes("my-assistant") ? { status: 0, stdout: Buffer.from("Name: my-assistant\nId: sbx-4f2a91c0d7\n"), stderr: Buffer.alloc(0) } : { status: 0 }; + return createdSandbox.run(command) ?? { status: 0 }; }; runner.runCapture = (command) => { - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command); + const createdIdentity = createdSandbox.capture(command); if (createdIdentity !== null) return createdIdentity; - if (_n(command).includes("sandbox get") && _n(command).includes("my-assistant")) return ""; - if (_n(command).includes("sandbox list")) return "my-assistant Ready"; { const mockedCapture = require(${onboardScriptMocksPath}).mockOnboardRunCapture(command); if (mockedCapture !== null) return mockedCapture; @@ -910,6 +909,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -1045,17 +1045,16 @@ registry.registerSandbox({ const commands = []; let dockerfileContent; const registerCalls = []; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture(); runner.run = (command, opts = {}) => { const normalized = _n(command); - commands.push({ command: normalized, env: opts.env || null }); if (normalized.includes("sandbox list")) return { status: 0, stdout: "No sandboxes found." }; + commands.push({ command: normalized, env: opts.env || null }); if (normalized.includes("provider get")) return { status: 1 }; - return normalized.includes("sandbox get") && normalized.includes("my-assistant") ? { status: 0, stdout: Buffer.from("Name: my-assistant\nId: sbx-4f2a91c0d7\n"), stderr: Buffer.alloc(0) } : { status: 0 }; + return createdSandbox.run(command) ?? { status: 0 }; }; runner.runCapture = (command) => { - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command); + const createdIdentity = createdSandbox.capture(command); if (createdIdentity !== null) return createdIdentity; - if (_n(command).includes("sandbox get") && _n(command).includes("my-assistant")) return ""; - if (_n(command).includes("sandbox list")) return "my-assistant Ready"; { const mockedCapture = require(${onboardScriptMocksPath}).mockOnboardRunCapture(command); if (mockedCapture !== null) return mockedCapture; @@ -1081,6 +1080,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -1383,15 +1383,12 @@ const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); const commands = []; -runner.run = require(${onboardScriptMocksPath}).createStatefulMessagingProviderRunner({ - commands, - readySandboxName: "my-assistant", -}); +const createdSandbox = fixtureMocks.createCreatedSandboxFixture(); +const providerRunner = require(${onboardScriptMocksPath}).createStatefulMessagingProviderRunner({ commands }); +runner.run = (command, opts = {}) => createdSandbox.run(command) ?? providerRunner(command, opts); runner.runCapture = (command) => { - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command); + const createdIdentity = createdSandbox.capture(command); if (createdIdentity !== null) return createdIdentity; - if (_n(command).includes("sandbox get") && _n(command).includes("my-assistant")) return ""; - if (_n(command).includes("sandbox list")) return "my-assistant Ready"; { const mockedCapture = require(${onboardScriptMocksPath}).mockOnboardRunCapture(command); if (mockedCapture !== null) return mockedCapture; @@ -1412,6 +1409,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -1524,15 +1522,14 @@ const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); const commands = []; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture(); runner.run = (command, opts = {}) => { - commands.push({ command: _n(command), env: opts.env || null }); if (_n(command).includes("sandbox list")) return { status: 0, stdout: "No sandboxes found." }; - return _n(command).includes("sandbox get") && _n(command).includes("my-assistant") ? { status: 0, stdout: Buffer.from("Name: my-assistant\nId: sbx-4f2a91c0d7\n"), stderr: Buffer.alloc(0) } : { status: 0 }; + commands.push({ command: _n(command), env: opts.env || null }); + return createdSandbox.run(command) ?? { status: 0 }; }; runner.runCapture = (command) => { - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command); + const createdIdentity = createdSandbox.capture(command); if (createdIdentity !== null) return createdIdentity; - if (_n(command).includes("sandbox get") && _n(command).includes("my-assistant")) return ""; - if (_n(command).includes("sandbox list")) return "my-assistant Ready"; { const mockedCapture = require(${onboardScriptMocksPath}).mockOnboardRunCapture(command); if (mockedCapture !== null) return mockedCapture; @@ -1553,6 +1550,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); diff --git a/test/onboarding/onboard-prepared-build-context.test.ts b/test/onboarding/onboard-prepared-build-context.test.ts index 4b63a1d18b8..8477c31bfd3 100644 --- a/test/onboarding/onboard-prepared-build-context.test.ts +++ b/test/onboarding/onboard-prepared-build-context.test.ts @@ -82,6 +82,7 @@ const scenario = ${JSON.stringify(scenario)}; const buildCtx = ${JSON.stringify(preparedBuildCtx)}; const buildId = ${JSON.stringify(buildId)}; const sandboxName = "prepared-dcode"; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName }); const commands = []; const registerCalls = []; const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, { @@ -106,7 +107,6 @@ let cleanupCalls = 0; let patchCalls = 0; let patchSleepUsesSeconds = null; let stageCalls = 0; -let sandboxCreated = false; dockerGpuSandboxCreate.createDockerGpuSandboxCreatePatch = (options) => { patchSleepUsesSeconds = options.deps.sleep === wait.sleepSeconds; @@ -152,9 +152,8 @@ runner.run = (command) => { commands.push(normalized); const profileResult = require(${onboardScriptMocksPath}).mockManagedEndpointlessProviderProfileRun(command); if (profileResult !== null) return profileResult; - return normalized.includes("sandbox get") && normalized.includes(sandboxName) - ? { status: 0, stdout: Buffer.from(sandboxName + "\nId: sbx-4f2a91c0d7\n"), stderr: Buffer.alloc(0) } - : { status: 0 }; + const sandboxResult = createdSandbox.run(command); + return sandboxResult ?? { status: 0 }; }; runner.runFile = (file, args = []) => { commands.push(normalize([file, ...args])); @@ -162,8 +161,8 @@ runner.runFile = (file, args = []) => { }; runner.runCapture = (command) => { const normalized = normalize(command); - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command, { sandboxName }); - if (createdIdentity !== null) return createdIdentity; + const sandboxCapture = createdSandbox.capture(command); + if (sandboxCapture !== null) return sandboxCapture; if ( normalized.includes( "sandbox exec --name " + @@ -178,10 +177,6 @@ runner.runCapture = (command) => { "Endpoint: https://inference.local/v1", ].join("\n"); } - if (normalized.includes("sandbox get")) { - return sandboxCreated ? sandboxName + "\nId: fixture-created-sandbox\n" : ""; - } - if (normalized.includes("sandbox list")) return sandboxName + " Ready"; return ""; }; registry.getDefault = () => null; @@ -190,6 +185,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -197,7 +193,6 @@ childProcess.spawn = (...args) => { child.pid = 6195; commands.push(normalize([args[0], ...(Array.isArray(args[1]) ? args[1] : [])])); process.nextTick(() => { - sandboxCreated = true; child.stdout.emit("data", Buffer.from("Created sandbox: " + sandboxName + "\n")); child.emit("close", 0); }); diff --git a/test/onboarding/onboard-reservation-recreate.test.ts b/test/onboarding/onboard-reservation-recreate.test.ts index 9653ac812f9..c7d36a6032a 100644 --- a/test/onboarding/onboard-reservation-recreate.test.ts +++ b/test/onboarding/onboard-reservation-recreate.test.ts @@ -64,30 +64,27 @@ const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); const events = []; -let sandboxDeleted = false; -let sandboxRecreated = false; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ + sandboxName: "my-assistant", + lifecycleState: "created", + phase: "NotReady", +}); runner.run = (command) => { const cmd = _n(command); events.push({ kind: "run", cmd }); const profileResult = require(${onboardScriptMocksPath}).mockManagedEndpointlessProviderProfileRun(command); if (profileResult !== null) return profileResult; - if (cmd.includes("sandbox list")) return { status: 0, stdout: "No sandboxes found." }; - if (cmd.includes("sandbox delete")) sandboxDeleted = true; - if (cmd.includes("sandbox list")) { - return { status: 0, stdout: "No sandboxes found.\n" }; + if (cmd.includes("sandbox delete")) { + createdSandbox.delete(); + return { status: 0 }; } - return cmd.includes("sandbox get") && cmd.includes("my-assistant") - ? { status: 0, stdout: Buffer.from("Name: my-assistant\nId: sbx-4f2a91c0d7\n"), stderr: Buffer.alloc(0) } - : { status: 0 }; + const sandboxResult = createdSandbox.run(command); + return sandboxResult ?? { status: 0 }; }; runner.runCapture = (command) => { const cmd = _n(command); - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command); - if (createdIdentity !== null) return createdIdentity; - if (cmd.includes("sandbox get") && cmd.includes("my-assistant")) return sandboxRecreated ? ["my-assistant", "Id: fixture-created-sandbox"].join(String.fromCharCode(10)) : sandboxDeleted ? "" : ["my-assistant", "Id: fixture-created-sandbox"].join(String.fromCharCode(10)); - if (cmd.includes("sandbox list")) { - return sandboxRecreated ? "my-assistant Ready" : sandboxDeleted ? "" : "my-assistant NotReady"; - } + const sandboxCapture = createdSandbox.capture(command); + if (sandboxCapture !== null) return sandboxCapture; if (cmd.includes("forward list")) return "my-assistant 127.0.0.1 18789 12345 running"; { const mockedCapture = require(${onboardScriptMocksPath}).mockOnboardRunCapture(command, { @@ -136,13 +133,17 @@ policyAuthorityPreflight.qualifySandboxPolicyAuthority = () => ({ }); childProcess.spawn = (...args) => { - sandboxRecreated = true; + const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); + if (command.includes("sandbox create")) { + createdSandbox.recreate(); + createdSandbox.setPhase("Ready"); + } const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); child.unref = () => {}; child.pid = 4246; - events.push({ kind: "spawn", cmd: _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]) }); + events.push({ kind: "spawn", cmd: command }); process.nextTick(() => { child.stdout.emit("data", Buffer.from("Created sandbox: my-assistant\n")); child.emit("close", 0); diff --git a/test/onboarding/onboard-sandbox-build.test.ts b/test/onboarding/onboard-sandbox-build.test.ts index 8d2e3405ae4..6bb9282c588 100644 --- a/test/onboarding/onboard-sandbox-build.test.ts +++ b/test/onboarding/onboard-sandbox-build.test.ts @@ -50,6 +50,9 @@ describe("onboard helpers", () => { const script = String.raw` const runner = require(${runnerPath}); const fixtureMocks = require(${onboardScriptMocksPath}); +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ + sandboxName: "my-assistant", +}); const _n = (c) => (Array.isArray(c) ? c.join(" ") : String(c)).replace(/'/g, ""); const registry = require(${registryPath}); const preflight = require(${preflightPath}); @@ -66,19 +69,13 @@ runner.run = (command, opts = {}) => { commands.push({ command: normalized, env: opts.env || null }); const profileResult = fixtureMocks.mockManagedEndpointlessProviderProfileRun(command); if (profileResult !== null) return profileResult; - if (normalized.includes("sandbox list")) { - return { status: 0, stdout: Buffer.from("No sandboxes found.\n"), stderr: Buffer.alloc(0) }; - } - return normalized.includes("sandbox get") && normalized.includes("my-assistant") - ? { status: 0, stdout: Buffer.from("Name: my-assistant\nId: sbx-4f2a91c0d7\n"), stderr: Buffer.alloc(0) } - : { status: 0 }; + const sandboxResult = createdSandbox.run(command); + return sandboxResult ?? { status: 0 }; }; runner.runCapture = (command) => { const normalized = _n(command); - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command, { sandboxName: "my-assistant" }); - if (createdIdentity !== null) return createdIdentity; - if (normalized.includes("sandbox get") && normalized.includes("my-assistant")) return ""; - if (normalized.includes("sandbox list")) return "my-assistant Ready"; + const sandboxCapture = createdSandbox.capture(command); + if (sandboxCapture !== null) return sandboxCapture; { const mockedCapture = fixtureMocks.mockOnboardRunCapture(command); if (mockedCapture !== null) return mockedCapture; @@ -98,6 +95,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -226,6 +224,9 @@ const os = require("node:os"); const path = require("node:path"); const runner = require(${runnerPath}); const fixtureMocks = require(${onboardScriptMocksPath}); +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ + sandboxName: "hermes-sandbox", +}); const _n = (c) => (Array.isArray(c) ? c.join(" ") : String(c)).replace(/'/g, ""); const registry = require(${registryPath}); const preflight = require(${preflightPath}); @@ -293,10 +294,8 @@ runner.run = (command, opts = {}) => { commands.push({ command: normalized, env: opts.env || null }); const profileResult = fixtureMocks.mockManagedEndpointlessProviderProfileRun(command); if (profileResult !== null) return profileResult; - if (normalized.includes("sandbox list")) { - return { status: 0, stdout: Buffer.from("No sandboxes found.\n"), stderr: Buffer.alloc(0) }; - } - return normalized.includes("sandbox get hermes-sandbox") ? { status: 0, stdout: Buffer.from("Name: hermes-sandbox\nId: sbx-4f2a91c0d7\n"), stderr: Buffer.alloc(0) } : { status: 0 }; + const sandboxResult = createdSandbox.run(command); + return sandboxResult ?? { status: 0 }; }; runner.runFile = (file, args = [], opts = {}) => { commands.push({ command: _n([file, ...args]), env: opts.env || null }); @@ -304,10 +303,8 @@ runner.runFile = (file, args = [], opts = {}) => { }; runner.runCapture = (command) => { const normalized = _n(command); - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command, { sandboxName: "hermes-sandbox" }); - if (createdIdentity !== null) return createdIdentity; - if (normalized.includes("sandbox get hermes-sandbox")) return ""; - if (normalized.includes("sandbox list")) return "hermes-sandbox Ready"; + const sandboxCapture = createdSandbox.capture(command); + if (sandboxCapture !== null) return sandboxCapture; { const mockedCapture = fixtureMocks.mockOnboardRunCapture(command); if (mockedCapture !== null) return mockedCapture; @@ -324,6 +321,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -463,11 +461,13 @@ const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); platform.isWsl = () => false; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ + sandboxName: "my-assistant", +}); const commands = []; const logs = []; const baseResolutionCalls = []; -let sandboxCreated = false; const originalLog = console.log; console.log = (...args) => { logs.push(args.join(" ")); @@ -512,12 +512,8 @@ runner.run = (command, opts = {}) => { commands.push({ command: normalized, env: opts.env || null }); const profileResult = fixtureMocks.mockManagedEndpointlessProviderProfileRun(command); if (profileResult !== null) return profileResult; - if (normalized.includes("sandbox list")) { - return { status: 0, stdout: Buffer.from("No sandboxes found.\n"), stderr: Buffer.alloc(0) }; - } - return normalized.includes("sandbox get") && normalized.includes("my-assistant") - ? { status: 0, stdout: Buffer.from("Name: my-assistant\nId: sbx-4f2a91c0d7\n"), stderr: Buffer.alloc(0) } - : { status: 0 }; + const sandboxResult = createdSandbox.run(command); + return sandboxResult ?? { status: 0 }; }; runner.runFile = (file, args = [], opts = {}) => { commands.push({ command: _n([file, ...args]), env: opts.env || null }); @@ -525,12 +521,8 @@ runner.runFile = (file, args = [], opts = {}) => { }; runner.runCapture = (command) => { const normalized = _n(command); - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command, { sandboxName: "my-assistant" }); - if (createdIdentity !== null) return createdIdentity; - if (normalized.includes("sandbox get") && normalized.includes("my-assistant")) { - return sandboxCreated ? "Name: my-assistant\\nId: fixture-created-sandbox\\nPhase: Ready\\n" : ""; - } - if (normalized.includes("sandbox list")) return "my-assistant Ready"; + const sandboxCapture = createdSandbox.capture(command); + if (sandboxCapture !== null) return sandboxCapture; { const mockedCapture = fixtureMocks.mockOnboardRunCapture(command); if (mockedCapture !== null) return mockedCapture; @@ -547,7 +539,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { - sandboxCreated = true; + createdSandbox.create(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -630,24 +622,21 @@ const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); const commands = []; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ + sandboxName: "my-assistant", +}); runner.run = (command, opts = {}) => { const normalized = _n(command); commands.push({ command: normalized, env: opts.env || null }); const profileResult = fixtureMocks.mockManagedEndpointlessProviderProfileRun(command); if (profileResult !== null) return profileResult; - if (normalized.includes("sandbox list")) { - return { status: 0, stdout: Buffer.from("No sandboxes found.\n"), stderr: Buffer.alloc(0) }; - } - return normalized.includes("sandbox get") && normalized.includes("my-assistant") - ? { status: 0, stdout: Buffer.from("Name: my-assistant\nId: sbx-4f2a91c0d7\n"), stderr: Buffer.alloc(0) } - : { status: 0 }; + const sandboxResult = createdSandbox.run(command); + return sandboxResult ?? { status: 0 }; }; runner.runCapture = (command) => { const normalized = _n(command); - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command, { sandboxName: "my-assistant" }); - if (createdIdentity !== null) return createdIdentity; - if (normalized.includes("sandbox get") && normalized.includes("my-assistant")) return ""; - if (normalized.includes("sandbox list")) return "my-assistant Ready"; + const sandboxCapture = createdSandbox.capture(command); + if (sandboxCapture !== null) return sandboxCapture; { const mockedCapture = fixtureMocks.mockOnboardRunCapture(command); if (mockedCapture !== null) return mockedCapture; @@ -664,6 +653,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -744,17 +734,16 @@ const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); const commands = []; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ + sandboxName: "my-assistant", +}); runner.run = (command, opts = {}) => { const normalized = _n(command); commands.push({ command: normalized, env: opts.env || null }); const profileResult = fixtureMocks.mockManagedEndpointlessProviderProfileRun(command); if (profileResult !== null) return profileResult; - if (normalized.includes("sandbox list")) { - return { status: 0, stdout: Buffer.from("No sandboxes found.\n"), stderr: Buffer.alloc(0) }; - } - return normalized.includes("sandbox get") && normalized.includes("my-assistant") - ? { status: 0, stdout: Buffer.from("Name: my-assistant\nId: sbx-4f2a91c0d7\n"), stderr: Buffer.alloc(0) } - : { status: 0 }; + const sandboxResult = createdSandbox.run(command); + return sandboxResult ?? { status: 0 }; }; runner.runFile = (file, args = [], opts = {}) => { commands.push({ command: _n([file, ...args]), env: opts.env || null }); @@ -762,10 +751,8 @@ runner.runFile = (file, args = [], opts = {}) => { }; runner.runCapture = (command) => { const normalized = _n(command); - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command, { sandboxName: "my-assistant" }); - if (createdIdentity !== null) return createdIdentity; - if (normalized.includes("sandbox get") && normalized.includes("my-assistant")) return ""; - if (normalized.includes("sandbox list")) return "my-assistant Ready"; + const sandboxCapture = createdSandbox.capture(command); + if (sandboxCapture !== null) return sandboxCapture; // Custom port: dashboard readiness curl uses 19000 (DASHBOARD_PORT from env) { const mockedCapture = fixtureMocks.mockOnboardRunCapture(command); @@ -783,6 +770,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); diff --git a/test/onboarding/onboard-sandbox-recreation.test.ts b/test/onboarding/onboard-sandbox-recreation.test.ts index 738b37435b5..38fc3803de0 100644 --- a/test/onboarding/onboard-sandbox-recreation.test.ts +++ b/test/onboarding/onboard-sandbox-recreation.test.ts @@ -40,36 +40,36 @@ describe("onboard helpers", () => { writeOkOpenshell(fakeBin); const script = String.raw` - const runner = require(${runnerPath}); +const runner = require(${runnerPath}); const fixtureMocks = require(${onboardScriptMocksPath}); fixtureMocks.mockStandaloneGatewayTeardownAuthority(); const _n = (c) => (Array.isArray(c) ? c.join(" ") : String(c)).replace(/'/g, ""); -let _deleted = false; const registry = require(${registryPath}); const childProcess = require("node:child_process"); +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ + lifecycleState: "created", + phase: "NotReady", +}); runner.run = (command) => { - _deleted = _deleted || _n(command).includes("sandbox delete"); if (_n(command).includes("sandbox delete")) { throw new Error("unexpected sandbox delete"); } - if (_n(command).includes("sandbox list")) return { status: 0, stdout: "No sandboxes found." }; - return { status: 0 }; + return createdSandbox.run(command) ?? { status: 0 }; }; runner.runCapture = (command) => { const cmd = _n(command); if (cmd.includes("gateway info")) return "Gateway endpoint: http://127.0.0.1:8080"; if (cmd.includes("policy get") && cmd.includes("--output json")) return JSON.stringify({ scope: "sandbox", sandbox: "my-assistant", status: "effective", policy_source: "sandbox", hash: "fixture-policy", active_version: 1, policy: {} }); - // Existing sandbox that is NOT ready - if (cmd.includes("sandbox get") && cmd.includes("my-assistant")) return _deleted ? "" : ["my-assistant", "Id: sbx-4f2a91c0d7"].join(String.fromCharCode(10)); - if (cmd.includes("sandbox list")) return _deleted ? "" : "my-assistant NotReady"; + const createdIdentity = createdSandbox.capture(command); + if (createdIdentity !== null) return createdIdentity; if (cmd.includes("forward list")) return "my-assistant 127.0.0.1 18789 12345 running"; return ""; }; registry.getSandbox = () => fixtureMocks.managedSandboxPolicyReceiptFixture({ name: "my-assistant", toolDisclosure: "progressive", - }, { sandboxId: "sbx-4f2a91c0d7" }); + }, { sandboxId: createdSandbox.state.sandboxId }); childProcess.spawn = () => { throw new Error("unexpected sandbox create"); }; @@ -133,16 +133,17 @@ const { createSandbox } = require(${onboardPath}); writeOkOpenshell(fakeBin); const script = String.raw` - const runner = require(${runnerPath}); +const runner = require(${runnerPath}); const fixtureMocks = require(${onboardScriptMocksPath}); fixtureMocks.mockStandaloneGatewayTeardownAuthority(); const _n = (c) => (Array.isArray(c) ? c.join(" ") : String(c)).replace(/'/g, ""); -let _deleted = false; let _sandboxId = "sbx-4f2a91c0d7"; const registry = require(${registryPath}); const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); const commands = []; let registeredSandbox = null; + const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ lifecycleState: "created" }); + const sourceSandboxId = createdSandbox.state.sandboxId; const sourceSandbox = fixtureMocks.managedSandboxPolicyReceiptFixture({ name: "my-assistant", gpuEnabled: false, @@ -154,28 +155,21 @@ const commands = []; let registeredSandbox = null; reference: "openshell/sandbox-from:source", shared: false, }, - }, { sandboxId: "sbx-4f2a91c0d7" }); + }, { sandboxId: sourceSandboxId }); runner.run = (command, opts = {}) => { const cmd = _n(command); const profileResult = require(${onboardScriptMocksPath}).mockEndpointlessProviderProfileRun(command, "nemoclaw-mcp-v1", false); if (profileResult !== null) return profileResult; - _deleted = _deleted || cmd.includes("sandbox delete"); + if (cmd.includes("sandbox delete")) createdSandbox.delete(); commands.push({ command: cmd, env: opts.env || null }); - if (cmd.includes("sandbox list")) { - return { status: 0, stdout: Buffer.from("No sandboxes found.\n"), stderr: Buffer.alloc(0) }; - } - return cmd.includes("sandbox get") && cmd.includes("my-assistant") - ? { status: 0, stdout: Buffer.from("my-assistant\nId: " + _sandboxId + "\n"), stderr: Buffer.alloc(0) } - : { status: 0 }; + return createdSandbox.run(command) ?? { status: 0 }; }; runner.runCapture = (command) => { const cmd = _n(command); if (cmd.includes("gateway info")) return "Gateway endpoint: http://127.0.0.1:8080"; if (cmd.includes("policy get") && cmd.includes("--output json")) return JSON.stringify({ scope: "sandbox", sandbox: "my-assistant", status: "effective", policy_source: "sandbox", hash: "fixture-policy", active_version: 1, policy: {} }); - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command, { sandboxName: "my-assistant", sandboxId: _sandboxId }); + const createdIdentity = createdSandbox.capture(command); if (createdIdentity !== null) return createdIdentity; - if (cmd.includes("sandbox get") && cmd.includes("my-assistant")) return _deleted ? "" : ["my-assistant", "Id: " + _sandboxId].join(String.fromCharCode(10)); - if (cmd.includes("sandbox list")) return _deleted ? "" : "my-assistant Ready"; if (cmd.includes("forward list")) return "my-assistant 127.0.0.1 18789 12345 running"; { const mockedCapture = require(${onboardScriptMocksPath}).mockOnboardRunCapture(command, { @@ -199,14 +193,14 @@ const preflight = require(${JSON.stringify(path.join(repoRoot, "src", "lib", "on preflight.checkPortAvailable = async () => ({ ok: true }); childProcess.spawn = (...args) => { - _deleted = false; - _sandboxId = "sbx-8e6b10fd33"; + const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); + if (command.includes("sandbox create")) createdSandbox.recreate(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); child.unref = () => {}; child.pid = 4242; - commands.push({ command: _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]), env: args[2]?.env || null }); + commands.push({ command, env: args[2]?.env || null }); process.nextTick(() => { child.stdout.emit("data", Buffer.from("Created sandbox: my-assistant\n")); child.emit("close", 0); @@ -224,7 +218,7 @@ const { createSandbox } = require(${onboardPath}); [null, "gpt-5.4", "nvidia-prod", null, "my-assistant", null, null, null, null, null, null, null, []], createFixture, )); - console.log(JSON.stringify({ sandboxName, commands, registeredSandbox })); + console.log(JSON.stringify({ sandboxName, commands, registeredSandbox, sourceSandboxId, replacementSandboxId: createdSandbox.state.sandboxId })); })().catch((error) => { console.error(error); process.exit(1); @@ -268,8 +262,10 @@ const { createSandbox } = require(${onboardPath}); ), "must defer source image retirement until replacement registration is proven", ); - const sourceFingerprint = createHash("sha256").update("sbx-4f2a91c0d7").digest("hex"); - const replacementFingerprint = createHash("sha256").update("sbx-8e6b10fd33").digest("hex"); + const sourceFingerprint = createHash("sha256").update(payload.sourceSandboxId).digest("hex"); + const replacementFingerprint = createHash("sha256") + .update(payload.replacementSandboxId) + .digest("hex"); assert.match(payload.registeredSandbox?.lifecycleGeneration ?? "", /^[0-9a-f-]{36}$/); assert.equal( payload.registeredSandbox?.lifecycleLiveIdentityFingerprint, @@ -309,34 +305,27 @@ const { createSandbox } = require(${onboardPath}); const fixtureMocks = require(${onboardScriptMocksPath}); fixtureMocks.mockStandaloneGatewayTeardownAuthority(); const _n = (c) => (Array.isArray(c) ? c.join(" ") : String(c)).replace(/'/g, ""); - let _deleted = false; let _sandboxId = "sbx-4f2a91c0d7"; const registry = require(${registryPath}); const sandboxState = require(${sandboxStatePath}); const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); const events = []; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ lifecycleState: "created" }); runner.run = (command) => { const cmd = _n(command); const profileResult = require(${onboardScriptMocksPath}).mockEndpointlessProviderProfileRun(command, "nemoclaw-mcp-v1", false); if (profileResult !== null) return profileResult; - _deleted = _deleted || cmd.includes("sandbox delete"); + if (cmd.includes("sandbox delete")) createdSandbox.delete(); events.push({ kind: "run", cmd }); - if (cmd.includes("sandbox list")) { - return { status: 0, stdout: Buffer.from("No sandboxes found.\n"), stderr: Buffer.alloc(0) }; - } - return cmd.includes("sandbox get") && cmd.includes("my-assistant") - ? { status: 0, stdout: Buffer.from("my-assistant\nId: " + _sandboxId + "\n"), stderr: Buffer.alloc(0) } - : { status: 0 }; + return createdSandbox.run(command) ?? { status: 0 }; }; runner.runCapture = (command) => { const cmd = _n(command); if (cmd.includes("gateway info")) return "Gateway endpoint: http://127.0.0.1:8080"; if (cmd.includes("policy get") && cmd.includes("--output json")) return JSON.stringify({ scope: "sandbox", sandbox: "my-assistant", status: "effective", policy_source: "sandbox", hash: "fixture-policy", active_version: 1, policy: {} }); - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command, { sandboxName: "my-assistant", sandboxId: _sandboxId }); + const createdIdentity = createdSandbox.capture(command); if (createdIdentity !== null) return createdIdentity; - if (cmd.includes("sandbox get") && cmd.includes("my-assistant")) return _deleted ? "" : ["my-assistant", "Id: " + _sandboxId].join(String.fromCharCode(10)); - if (cmd.includes("sandbox list")) return _deleted ? "" : "my-assistant Ready"; if (cmd.includes("forward list")) return "my-assistant 127.0.0.1 18789 12345 running"; { const mockedCapture = require(${onboardScriptMocksPath}).mockOnboardRunCapture(command, { @@ -349,7 +338,7 @@ runner.run = (command) => { const sourceSandbox = fixtureMocks.managedSandboxPolicyReceiptFixture({ name: "my-assistant", gpuEnabled: false, - }, { sandboxId: "sbx-4f2a91c0d7" }); + }, { sandboxId: createdSandbox.state.sandboxId }); registry.getSandbox = () => sourceSandbox; const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, { sandboxName: "my-assistant", @@ -384,14 +373,14 @@ const preflight = require(${JSON.stringify(path.join(repoRoot, "src", "lib", "on preflight.checkPortAvailable = async () => ({ ok: true }); childProcess.spawn = (...args) => { - _deleted = false; - _sandboxId = "sbx-8e6b10fd34"; + const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); + if (command.includes("sandbox create")) createdSandbox.recreate(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); child.unref = () => {}; child.pid = 4243; - events.push({ kind: "spawn", cmd: _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]) }); + events.push({ kind: "spawn", cmd: command }); process.nextTick(() => { child.stdout.emit("data", Buffer.from("Created sandbox: my-assistant\n")); child.emit("close", 0); @@ -495,34 +484,27 @@ const { createSandbox } = require(${onboardPath}); const fixtureMocks = require(${onboardScriptMocksPath}); fixtureMocks.mockStandaloneGatewayTeardownAuthority(); const _n = (c) => (Array.isArray(c) ? c.join(" ") : String(c)).replace(/'/g, ""); - let _deleted = false; let _sandboxId = "sbx-4f2a91c0d7"; const registry = require(${registryPath}); const sandboxState = require(${sandboxStatePath}); const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); const events = []; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ lifecycleState: "created" }); runner.run = (command) => { const cmd = _n(command); const profileResult = require(${onboardScriptMocksPath}).mockEndpointlessProviderProfileRun(command, "nemoclaw-mcp-v1", false); if (profileResult !== null) return profileResult; - _deleted = _deleted || cmd.includes("sandbox delete"); + if (cmd.includes("sandbox delete")) createdSandbox.delete(); events.push({ kind: "run", cmd }); - if (cmd.includes("sandbox list")) { - return { status: 0, stdout: Buffer.from("No sandboxes found.\n"), stderr: Buffer.alloc(0) }; - } - return cmd.includes("sandbox get") && cmd.includes("my-assistant") - ? { status: 0, stdout: Buffer.from("my-assistant\nId: " + _sandboxId + "\n"), stderr: Buffer.alloc(0) } - : { status: 0 }; + return createdSandbox.run(command) ?? { status: 0 }; }; runner.runCapture = (command) => { const cmd = _n(command); if (cmd.includes("gateway info")) return "Gateway endpoint: http://127.0.0.1:8080"; if (cmd.includes("policy get") && cmd.includes("--output json")) return JSON.stringify({ scope: "sandbox", sandbox: "my-assistant", status: "effective", policy_source: "sandbox", hash: "fixture-policy", active_version: 1, policy: {} }); - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command, { sandboxName: "my-assistant", sandboxId: _sandboxId }); + const createdIdentity = createdSandbox.capture(command); if (createdIdentity !== null) return createdIdentity; - if (cmd.includes("sandbox get") && cmd.includes("my-assistant")) return _deleted ? "" : ["my-assistant", "Id: " + _sandboxId].join(String.fromCharCode(10)); - if (cmd.includes("sandbox list")) return _deleted ? "" : "my-assistant Ready"; if (cmd.includes("forward list")) return "my-assistant 127.0.0.1 18789 12345 running"; { const mockedCapture = require(${onboardScriptMocksPath}).mockOnboardRunCapture(command, { @@ -535,7 +517,7 @@ runner.run = (command) => { const sourceSandbox = fixtureMocks.managedSandboxPolicyReceiptFixture({ name: "my-assistant", gpuEnabled: false, - }, { sandboxId: "sbx-4f2a91c0d7" }); + }, { sandboxId: createdSandbox.state.sandboxId }); registry.getSandbox = () => sourceSandbox; const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, { sandboxName: "my-assistant", @@ -557,14 +539,14 @@ const preflight = require(${JSON.stringify(path.join(repoRoot, "src", "lib", "on preflight.checkPortAvailable = async () => ({ ok: true }); childProcess.spawn = (...args) => { - _deleted = false; - _sandboxId = "sbx-8e6b10fd35"; + const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); + if (command.includes("sandbox create")) createdSandbox.recreate(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); child.unref = () => {}; child.pid = 4244; - events.push({ kind: "spawn", cmd: _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]) }); + events.push({ kind: "spawn", cmd: command }); process.nextTick(() => { child.stdout.emit("data", Buffer.from("Created sandbox: my-assistant\n")); child.emit("close", 0); @@ -649,39 +631,30 @@ const { createSandbox } = require(${onboardPath}); const fixtureMocks = require(${onboardScriptMocksPath}); fixtureMocks.mockStandaloneGatewayTeardownAuthority(); const _n = (c) => (Array.isArray(c) ? c.join(" ") : String(c)).replace(/'/g, ""); - let _deleted = false; let _sandboxId = "sbx-4f2a91c0d7"; const registry = require(${registryPath}); const sandboxState = require(${sandboxStatePath}); const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); const events = []; -let sandboxDeleted = false; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ + lifecycleState: "created", + phase: "NotReady", +}); runner.run = (command) => { const cmd = _n(command); const profileResult = require(${onboardScriptMocksPath}).mockEndpointlessProviderProfileRun(command, "nemoclaw-mcp-v1", false); if (profileResult !== null) return profileResult; - _deleted = _deleted || cmd.includes("sandbox delete"); + if (cmd.includes("sandbox delete")) createdSandbox.delete(); events.push({ kind: "run", cmd }); - if (cmd.includes("sandbox list")) return { status: 0, stdout: "No sandboxes found." }; - if (cmd.includes("sandbox delete")) sandboxDeleted = true; - if (cmd.includes("sandbox list")) { - return { status: 0, stdout: Buffer.from("No sandboxes found.\n"), stderr: Buffer.alloc(0) }; - } - return cmd.includes("sandbox get") && cmd.includes("my-assistant") - ? { status: 0, stdout: Buffer.from("my-assistant\nId: " + _sandboxId + "\n"), stderr: Buffer.alloc(0) } - : { status: 0 }; + return createdSandbox.run(command) ?? { status: 0 }; }; runner.runCapture = (command) => { const cmd = _n(command); if (cmd.includes("gateway info")) return "Gateway endpoint: http://127.0.0.1:8080"; if (cmd.includes("policy get") && cmd.includes("--output json")) return JSON.stringify({ scope: "sandbox", sandbox: "my-assistant", status: "effective", policy_source: "sandbox", hash: "fixture-policy", active_version: 1, policy: {} }); - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command, { sandboxName: "my-assistant", sandboxId: _sandboxId }); + const createdIdentity = createdSandbox.capture(command); if (createdIdentity !== null) return createdIdentity; - if (cmd.includes("sandbox get") && cmd.includes("my-assistant")) return _deleted ? "" : ["my-assistant", "Id: " + _sandboxId].join(String.fromCharCode(10)); - if (cmd.includes("sandbox list")) { - return _deleted ? "" : sandboxDeleted ? "my-assistant Ready" : "my-assistant NotReady"; - } if (cmd.includes("forward list")) return "my-assistant 127.0.0.1 18789 12345 running"; { const mockedCapture = require(${onboardScriptMocksPath}).mockOnboardRunCapture(command, { @@ -694,7 +667,7 @@ runner.run = (command) => { const sourceSandbox = fixtureMocks.managedSandboxPolicyReceiptFixture({ name: "my-assistant", gpuEnabled: false, - }, { sandboxId: "sbx-4f2a91c0d7" }); + }, { sandboxId: createdSandbox.state.sandboxId }); registry.getSandbox = () => sourceSandbox; const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, { sandboxName: "my-assistant", @@ -729,14 +702,17 @@ const preflight = require(${JSON.stringify(path.join(repoRoot, "src", "lib", "on preflight.checkPortAvailable = async () => ({ ok: true }); childProcess.spawn = (...args) => { - _deleted = false; - _sandboxId = "sbx-8e6b10fd36"; + const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); + if (command.includes("sandbox create")) { + createdSandbox.recreate(); + createdSandbox.setPhase("Ready"); + } const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); child.unref = () => {}; child.pid = 4245; - events.push({ kind: "spawn", cmd: _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]) }); + events.push({ kind: "spawn", cmd: command }); process.nextTick(() => { child.stdout.emit("data", Buffer.from("Created sandbox: my-assistant\n")); child.emit("close", 0); @@ -827,34 +803,27 @@ const { createSandbox } = require(${onboardPath}); const fixtureMocks = require(${onboardScriptMocksPath}); fixtureMocks.mockStandaloneGatewayTeardownAuthority(); const _n = (c) => (Array.isArray(c) ? c.join(" ") : String(c)).replace(/'/g, ""); - let _deleted = false; let _sandboxId = "sbx-4f2a91c0d7"; const registry = require(${registryPath}); const onboardSession = require(${sessionModulePath}); const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); const commands = []; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ lifecycleState: "created" }); runner.run = (command, opts = {}) => { const cmd = _n(command); const profileResult = require(${onboardScriptMocksPath}).mockEndpointlessProviderProfileRun(command, "nemoclaw-mcp-v1", false); if (profileResult !== null) return profileResult; - _deleted = _deleted || cmd.includes("sandbox delete"); + if (cmd.includes("sandbox delete")) createdSandbox.delete(); commands.push({ command: cmd, env: opts.env || null }); - if (cmd.includes("sandbox list")) { - return { status: 0, stdout: Buffer.from("No sandboxes found.\n"), stderr: Buffer.alloc(0) }; - } - return cmd.includes("sandbox get") && cmd.includes("my-assistant") - ? { status: 0, stdout: Buffer.from("my-assistant\nId: " + _sandboxId + "\n"), stderr: Buffer.alloc(0) } - : { status: 0 }; + return createdSandbox.run(command) ?? { status: 0 }; }; runner.runCapture = (command) => { const cmd = _n(command); if (cmd.includes("gateway info")) return "Gateway endpoint: http://127.0.0.1:8080"; if (cmd.includes("policy get") && cmd.includes("--output json")) return JSON.stringify({ scope: "sandbox", sandbox: "my-assistant", status: "effective", policy_source: "sandbox", hash: "fixture-policy", active_version: 1, policy: {} }); - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command, { sandboxName: "my-assistant", sandboxId: _sandboxId }); + const createdIdentity = createdSandbox.capture(command); if (createdIdentity !== null) return createdIdentity; - if (cmd.includes("sandbox get") && cmd.includes("my-assistant")) return _deleted ? "" : ["my-assistant", "Id: " + _sandboxId].join(String.fromCharCode(10)); - if (cmd.includes("sandbox list")) return _deleted ? "" : "my-assistant Ready"; if (cmd.includes("forward list")) return "my-assistant 127.0.0.1 18789 12345 running"; { const mockedCapture = require(${onboardScriptMocksPath}).mockOnboardRunCapture(command, { @@ -873,7 +842,7 @@ runner.run = (command, opts = {}) => { gpuEnabled: false, policies: ["npm"], policyTier: "balanced", - }, { sandboxId: "sbx-4f2a91c0d7" }); + }, { sandboxId: createdSandbox.state.sandboxId }); registry.getSandbox = () => sourceSandbox; const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, { sandboxName: "my-assistant", @@ -886,14 +855,14 @@ const preflight = require(${JSON.stringify(path.join(repoRoot, "src", "lib", "on preflight.checkPortAvailable = async () => ({ ok: true }); childProcess.spawn = (...args) => { - _deleted = false; - _sandboxId = "sbx-8e6b10fd37"; + const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); + if (command.includes("sandbox create")) createdSandbox.recreate(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); child.unref = () => {}; child.pid = 4242; - commands.push({ command: _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]), env: args[2]?.env || null }); + commands.push({ command, env: args[2]?.env || null }); process.nextTick(() => { child.stdout.emit("data", Buffer.from("Created sandbox: my-assistant\n")); child.emit("close", 0); @@ -976,7 +945,6 @@ const { createSandbox } = require(${onboardPath}); const fixtureMocks = require(${onboardScriptMocksPath}); fixtureMocks.mockStandaloneGatewayTeardownAuthority(); const _n = (c) => (Array.isArray(c) ? c.join(" ") : String(c)).replace(/'/g, ""); -let _deleted = false; const registry = require(${registryPath}); const credentials = require(${credentialsPath}); const childProcess = require("node:child_process"); @@ -985,11 +953,12 @@ const fs = require("node:fs"); const path = require("node:path"); const commands = []; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ lifecycleState: "created" }); runner.run = (command, opts = {}) => { const cmd = _n(command); const profileResult = require(${onboardScriptMocksPath}).mockEndpointlessProviderProfileRun(command, "nemoclaw-mcp-v1", false); if (profileResult !== null) return profileResult; - _deleted = _deleted || cmd.includes("sandbox delete"); + if (cmd.includes("sandbox delete")) createdSandbox.delete(); const commandString = Array.isArray(command) ? command.join(" ") : String(command); if (cmd.includes("sandbox download")) { const parts = commandString.match(/'([^']*)'/g) || []; @@ -1007,12 +976,7 @@ runner.run = (command, opts = {}) => { } } commands.push({ command: cmd, env: opts.env || null }); - if (cmd.includes("sandbox list")) { - return { status: 0, stdout: Buffer.from("No sandboxes found.\n"), stderr: Buffer.alloc(0) }; - } - return cmd.includes("sandbox get") && cmd.includes("my-assistant") - ? { status: 0, stdout: Buffer.from("my-assistant\nId: sbx-4f2a91c0d7\n"), stderr: Buffer.alloc(0) } - : { status: 0 }; + return createdSandbox.run(command) ?? { status: 0 }; }; runner.runFile = (file, args = [], opts = {}) => { commands.push({ type: "runFile", command: _n([file, ...args]), file, args, env: opts.env || null }); @@ -1022,21 +986,20 @@ runner.runFile = (file, args = [], opts = {}) => { const cmd = _n(command); if (cmd.includes("gateway info")) return "Gateway endpoint: http://127.0.0.1:8080"; if (cmd.includes("policy get") && cmd.includes("--output json")) return JSON.stringify({ scope: "sandbox", sandbox: "my-assistant", status: "effective", policy_source: "sandbox", hash: "fixture-policy", active_version: 1, policy: {} }); - if (cmd.includes("sandbox get") && cmd.includes("my-assistant")) return _deleted ? "" : ["my-assistant", "Id: sbx-4f2a91c0d7"].join(String.fromCharCode(10)); - if (cmd.includes("sandbox list")) return _deleted ? "" : "my-assistant Ready"; + const createdIdentity = createdSandbox.capture(command); + if (createdIdentity !== null) return createdIdentity; if (cmd.includes("forward list")) return "my-assistant 127.0.0.1 18789 12345 running"; return ""; }; registry.getSandbox = () => fixtureMocks.managedSandboxPolicyReceiptFixture({ name: "my-assistant", toolDisclosure: "progressive", - }, { sandboxId: "sbx-4f2a91c0d7" }); + }, { sandboxId: createdSandbox.state.sandboxId }); // Mock prompt to return "y" (reuse) credentials.prompt = async () => "y"; childProcess.spawn = (...args) => { - _deleted = false; const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -1132,7 +1095,6 @@ const { createSandbox } = require(${onboardPath}); const fixtureMocks = require(${onboardScriptMocksPath}); fixtureMocks.mockStandaloneGatewayTeardownAuthority(); const _n = (c) => (Array.isArray(c) ? c.join(" ") : String(c)).replace(/'/g, ""); - let _deleted = false; let _sandboxId = "sbx-4f2a91c0d7"; const registry = require(${registryPath}); const credentials = require(${credentialsPath}); const childProcess = require("node:child_process"); @@ -1141,11 +1103,12 @@ const fs = require("node:fs"); const path = require("node:path"); const commands = []; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ lifecycleState: "created" }); runner.run = (command, opts = {}) => { const cmd = _n(command); const profileResult = require(${onboardScriptMocksPath}).mockEndpointlessProviderProfileRun(command, "nemoclaw-mcp-v1", false); if (profileResult !== null) return profileResult; - _deleted = _deleted || cmd.includes("sandbox delete"); + if (cmd.includes("sandbox delete")) createdSandbox.delete(); const commandString = Array.isArray(command) ? command.join(" ") : String(command); if (cmd.includes("sandbox download")) { const parts = commandString.match(/'([^']*)'/g) || []; @@ -1163,12 +1126,7 @@ runner.run = (command, opts = {}) => { } } commands.push({ command: cmd, env: opts.env || null }); - if (cmd.includes("sandbox list")) { - return { status: 0, stdout: Buffer.from("No sandboxes found.\n"), stderr: Buffer.alloc(0) }; - } - return cmd.includes("sandbox get") && cmd.includes("my-assistant") - ? { status: 0, stdout: Buffer.from("my-assistant\nId: " + _sandboxId + "\n"), stderr: Buffer.alloc(0) } - : { status: 0 }; + return createdSandbox.run(command) ?? { status: 0 }; }; runner.runFile = (file, args = [], opts = {}) => { commands.push({ type: "runFile", command: _n([file, ...args]), file, args, env: opts.env || null }); @@ -1178,10 +1136,8 @@ runner.runFile = (file, args = [], opts = {}) => { const cmd = _n(command); if (cmd.includes("gateway info")) return "Gateway endpoint: http://127.0.0.1:8080"; if (cmd.includes("policy get") && cmd.includes("--output json")) return JSON.stringify({ scope: "sandbox", sandbox: "my-assistant", status: "effective", policy_source: "sandbox", hash: "fixture-policy", active_version: 1, policy: {} }); - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command, { sandboxName: "my-assistant", sandboxId: _sandboxId }); + const createdIdentity = createdSandbox.capture(command); if (createdIdentity !== null) return createdIdentity; - if (cmd.includes("sandbox get") && cmd.includes("my-assistant")) return _deleted ? "" : ["my-assistant", "Id: " + _sandboxId].join(String.fromCharCode(10)); - if (cmd.includes("sandbox list")) return _deleted ? "" : "my-assistant Ready"; if (cmd.includes("forward list")) return "my-assistant 127.0.0.1 18789 12345 running"; { const mockedCapture = require(${onboardScriptMocksPath}).mockOnboardRunCapture(command, { @@ -1194,7 +1150,7 @@ runner.runFile = (file, args = [], opts = {}) => { const sourceSandbox = fixtureMocks.managedSandboxPolicyReceiptFixture({ name: "my-assistant", toolDisclosure: "progressive", - }, { sandboxId: "sbx-4f2a91c0d7" }); + }, { sandboxId: createdSandbox.state.sandboxId }); registry.getSandbox = () => sourceSandbox; const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, { sandboxName: "my-assistant", @@ -1210,14 +1166,14 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => "y"; childProcess.spawn = (...args) => { - _deleted = false; - _sandboxId = "sbx-8e6b10fd38"; + const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); + if (command.includes("sandbox create")) createdSandbox.recreate(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); child.unref = () => {}; child.pid = 4242; - commands.push({ command: _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]), env: args[2]?.env || null }); + commands.push({ command, env: args[2]?.env || null }); process.nextTick(() => { child.stdout.emit("data", Buffer.from("Created sandbox: my-assistant\n")); child.emit("close", 0); @@ -1314,40 +1270,31 @@ const { createSandbox } = require(${onboardPath}); const fixtureMocks = require(${onboardScriptMocksPath}); fixtureMocks.mockStandaloneGatewayTeardownAuthority(); const _n = (c) => (Array.isArray(c) ? c.join(" ") : String(c)).replace(/'/g, ""); - let _deleted = false; let _sandboxId = "sbx-4f2a91c0d7"; const registry = require(${registryPath}); const credentials = require(${credentialsPath}); const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); const commands = []; -let sandboxDeleted = false; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ + lifecycleState: "created", + phase: "NotReady", +}); runner.run = (command, opts = {}) => { const cmd = _n(command); const profileResult = require(${onboardScriptMocksPath}).mockEndpointlessProviderProfileRun(command, "nemoclaw-mcp-v1", false); if (profileResult !== null) return profileResult; - _deleted = _deleted || cmd.includes("sandbox delete"); + if (cmd.includes("sandbox delete")) createdSandbox.delete(); commands.push({ command: cmd, env: opts.env || null }); - if (cmd.includes("sandbox list")) return { status: 0, stdout: "No sandboxes found." }; - if (cmd.includes("sandbox delete")) sandboxDeleted = true; - if (cmd.includes("sandbox list")) { - return { status: 0, stdout: Buffer.from("No sandboxes found.\n"), stderr: Buffer.alloc(0) }; - } - return cmd.includes("sandbox get") && cmd.includes("my-assistant") - ? { status: 0, stdout: Buffer.from("my-assistant\nId: " + _sandboxId + "\n"), stderr: Buffer.alloc(0) } - : { status: 0 }; + return createdSandbox.run(command) ?? { status: 0 }; }; runner.runCapture = (command) => { // Existing sandbox that is NOT ready initially, becomes Ready after recreation const cmd = _n(command); if (cmd.includes("gateway info")) return "Gateway endpoint: http://127.0.0.1:8080"; if (cmd.includes("policy get") && cmd.includes("--output json")) return JSON.stringify({ scope: "sandbox", sandbox: "my-assistant", status: "effective", policy_source: "sandbox", hash: "fixture-policy", active_version: 1, policy: {} }); - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command, { sandboxName: "my-assistant", sandboxId: _sandboxId }); + const createdIdentity = createdSandbox.capture(command); if (createdIdentity !== null) return createdIdentity; - if (cmd.includes("sandbox get") && cmd.includes("my-assistant")) return _deleted ? "" : ["my-assistant", "Id: " + _sandboxId].join(String.fromCharCode(10)); - if (cmd.includes("sandbox list")) { - return _deleted ? "" : sandboxDeleted ? "my-assistant Ready" : "my-assistant NotReady"; - } if (cmd.includes("forward list")) return "my-assistant 127.0.0.1 18789 12345 running"; { const mockedCapture = require(${onboardScriptMocksPath}).mockOnboardRunCapture(command, { @@ -1360,7 +1307,7 @@ runner.run = (command, opts = {}) => { const sourceSandbox = fixtureMocks.managedSandboxPolicyReceiptFixture({ name: "my-assistant", toolDisclosure: "progressive", - }, { sandboxId: "sbx-4f2a91c0d7" }); + }, { sandboxId: createdSandbox.state.sandboxId }); registry.getSandbox = () => sourceSandbox; const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, { sandboxName: "my-assistant", @@ -1376,12 +1323,17 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => "y"; const fakeSpawn = (...args) => { + const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); + if (command.includes("sandbox create") && createdSandbox.state.lifecycleState === "deleted") { + createdSandbox.recreate(); + createdSandbox.setPhase("Ready"); + } const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); child.unref = () => {}; child.pid = 4242; - commands.push({ command: _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]), env: args[2]?.env || null }); + commands.push({ command, env: args[2]?.env || null }); process.nextTick(() => { child.stdout.emit("data", Buffer.from("Created sandbox: my-assistant\n")); child.emit("close", 0); @@ -1389,8 +1341,6 @@ const fakeSpawn = (...args) => { return child; }; childProcess.spawn = (...args) => { - _deleted = false; - _sandboxId = "sbx-8e6b10fd39"; return fakeSpawn(...args); }; diff --git a/test/onboarding/onboard-terminal-dashboard.test.ts b/test/onboarding/onboard-terminal-dashboard.test.ts index 4b145aaa767..4a53c93c201 100644 --- a/test/onboarding/onboard-terminal-dashboard.test.ts +++ b/test/onboarding/onboard-terminal-dashboard.test.ts @@ -63,6 +63,10 @@ const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); const scenario = ${JSON.stringify(scenario)}; const sandboxName = "deepagents-box"; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ + sandboxName, + lifecycleState: scenario === "reuse" ? "created" : "absent", +}); const commands = []; const registerCalls = []; const updateCalls = []; @@ -98,9 +102,8 @@ runner.run = (command, opts = {}) => { commands.push({ command: normalized, env: opts.env || null }); const profileResult = require(${onboardScriptMocksPath}).mockManagedEndpointlessProviderProfileRun(command); if (profileResult !== null) return profileResult; - return normalized.includes("sandbox get") && normalized.includes(sandboxName) - ? { status: 0, stdout: Buffer.from("Name: " + sandboxName + "\nId: sbx-4f2a91c0d7\n"), stderr: Buffer.alloc(0) } - : { status: 0 }; + const sandboxResult = createdSandbox.run(command); + return sandboxResult ?? { status: 0 }; }; runner.runFile = (file, args = [], opts = {}) => { commands.push({ command: _n([file, ...args]), env: opts.env || null }); @@ -108,10 +111,8 @@ runner.runFile = (file, args = [], opts = {}) => { }; runner.runCapture = (command) => { const normalized = _n(command); - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command, { - sandboxName, - }); - if (createdIdentity !== null) return createdIdentity; + const sandboxCapture = createdSandbox.capture(command); + if (sandboxCapture !== null) return sandboxCapture; commands.push({ command: normalized, env: null }); if ( normalized.includes( @@ -127,12 +128,6 @@ runner.runCapture = (command) => { "Endpoint: https://inference.local/v1", ].join("\n"); } - if (normalized.includes("sandbox get") && normalized.includes(sandboxName)) { - return scenario === "reuse" - ? [sandboxName, "Id: fixture-created-sandbox"].join(String.fromCharCode(10)) - : ""; - } - if (normalized.includes("sandbox list")) return sandboxName + " Ready"; if (normalized.includes("forward list")) return sandboxName + " 127.0.0.1 18789 12345 running"; return ""; }; @@ -171,6 +166,7 @@ const createFixture = childProcess.spawn = (...args) => { if (scenario === "reuse") throw new Error("unexpected sandbox create"); + createdSandbox.create(); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); diff --git a/test/repository/vitest-watch-triggers.test.ts b/test/repository/vitest-watch-triggers.test.ts index 7c17a6dc5ac..41905b573cb 100644 --- a/test/repository/vitest-watch-triggers.test.ts +++ b/test/repository/vitest-watch-triggers.test.ts @@ -116,15 +116,21 @@ function triggeredBy(relativePath: string): string[] { } describe("Vitest opaque-input watch triggers", () => { - it("maps the onboard child-process preload to its managed-image fixtures", () => { + it("maps the onboard preload to every spawned identity consumer (#10463)", () => { expect(triggeredBy("test/helpers/onboard-script-mocks.cjs")).toEqual([ + "test/helpers/onboard-created-sandbox-fixture.test.ts", + "test/onboarding/onboard-custom-dockerfile.test.ts", "test/onboarding/onboard-extra-provider-reconciliation.test.ts", + "test/onboarding/onboard-fresh-create-identity.test.ts", "test/onboarding/onboard-installer-restore-intent.test.ts", + "test/onboarding/onboard-managed-image-buildless-e2e.test.ts", "test/onboarding/onboard-messaging.test.ts", + "test/onboarding/onboard-prepared-build-context.test.ts", "test/onboarding/onboard-reservation-recreate.test.ts", "test/onboarding/onboard-sandbox-build.test.ts", "test/onboarding/onboard-sandbox-recreation.test.ts", "test/onboarding/onboard-terminal-dashboard.test.ts", + "test/security/shellquote-sandbox.test.ts", ]); }); diff --git a/test/security/shellquote-sandbox.test.ts b/test/security/shellquote-sandbox.test.ts index 4bde66e0631..618af746f26 100644 --- a/test/security/shellquote-sandbox.test.ts +++ b/test/security/shellquote-sandbox.test.ts @@ -89,6 +89,7 @@ for (const key of Object.keys(process.env)) { process.env.NEMOCLAW_OPENSHELL_BIN = ${JSON.stringify(path.join(fakeBin, "openshell"))}; const commands = []; const asText = (command) => Array.isArray(command) ? command.join(" ") : String(command); +const createdSandbox = fixtureMocks.createCreatedSandboxFixture(); runner.run = (command, opts = {}) => { const text = asText(command); commands.push({ type: "run", command: text, env: opts.env || null }); @@ -105,14 +106,7 @@ runner.run = (command, opts = {}) => { stderr: Buffer.alloc(0), }; } - if (text.includes("sandbox get") && text.includes("my-assistant")) { - return { - status: 0, - stdout: Buffer.from("Name: my-assistant\nId: sbx-4f2a91c0d7\n"), - stderr: Buffer.alloc(0), - }; - } - return { status: 0 }; + return createdSandbox.run(command) ?? { status: 0 }; }; runner.runFile = (file, args = [], opts = {}) => { commands.push({ type: "runFile", file, args, command: asText([file, ...args]), env: opts.env || null }); @@ -120,10 +114,8 @@ runner.runFile = (file, args = [], opts = {}) => { }; runner.runCapture = (command) => { const text = asText(command); - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command); + const createdIdentity = createdSandbox.capture(command); if (createdIdentity !== null) return createdIdentity; - if (text.includes("sandbox get") && text.includes("my-assistant")) return ""; - if (text.includes("sandbox list")) return "my-assistant Ready"; if (text.includes("forward list")) return "my-assistant 127.0.0.1 18789 12345 running"; if (text.includes("sandbox exec") && text.includes("http://localhost:") && text.includes("/health")) return "200"; if (text === "uname -r") return "6.8.0"; @@ -143,11 +135,14 @@ const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, }); preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; -sandboxCreateStream.streamSandboxCreate = async () => ({ - status: 0, - output: "Built image openshell/sandbox-from:123\nCreated sandbox: my-assistant", - sawProgress: true, -}); +sandboxCreateStream.streamSandboxCreate = async () => { + createdSandbox.create(); + return { + status: 0, + output: "Built image openshell/sandbox-from:123\nCreated sandbox: my-assistant", + sawProgress: true, + }; +}; const { createSandbox } = require(${onboardPath}); (async () => { try { From 5187132aeedd2944150a4c9c102f15a24f6b8b33 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 27 Aug 2026 02:31:28 -0700 Subject: [PATCH 02/13] test(onboard): align managed create identity Signed-off-by: Apurv Kumaria --- .../onboard/__test-helpers__/sandbox-gpu-create-flow.ts | 4 ++-- src/lib/onboard/sandbox-gpu-create-flow.test.ts | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/lib/onboard/__test-helpers__/sandbox-gpu-create-flow.ts b/src/lib/onboard/__test-helpers__/sandbox-gpu-create-flow.ts index 8fb7b4e9643..8942245ed7a 100644 --- a/src/lib/onboard/__test-helpers__/sandbox-gpu-create-flow.ts +++ b/src/lib/onboard/__test-helpers__/sandbox-gpu-create-flow.ts @@ -50,13 +50,13 @@ export function createGpuFlowInput(): SandboxGpuCreateFlowInput { }; } -export function createGpuFlowDeps(): SandboxGpuCreateFlowDeps { +export function createGpuFlowDeps(sandboxId = "alpha-sandbox-id"): SandboxGpuCreateFlowDeps { return { runOpenshell: vi.fn((args: string[]) => args[0] === "sandbox" && args[1] === "get" ? { status: 0, - stdout: "Name: alpha\nId: alpha-sandbox-id\nState: Ready\n", + stdout: `Name: alpha\nId: ${sandboxId}\nState: Ready\n`, stderr: "", } : { status: 0, stdout: "", stderr: "" }, diff --git a/src/lib/onboard/sandbox-gpu-create-flow.test.ts b/src/lib/onboard/sandbox-gpu-create-flow.test.ts index 08ef87b805a..f6ee3a4e11f 100644 --- a/src/lib/onboard/sandbox-gpu-create-flow.test.ts +++ b/src/lib/onboard/sandbox-gpu-create-flow.test.ts @@ -350,11 +350,12 @@ describe("runSandboxGpuCreateFlow provider-owned managed create", () => { intendedWorkloadArgv: launch.intendedSandboxStartupCommand, expectedSupervisorArgv: ["/mxc/supervisor"], }; - const deps = createDeps(); + const sandboxId = "mxc-alpha"; + const deps = createDeps(sandboxId); const adapterOverride = {} as never; deps.createManagedBootstrapAdapter = vi.fn(() => adapterOverride); vi.mocked(deps.runCaptureOpenshell).mockImplementation((args) => - args[1] === "get" ? "ID: mxc-alpha\n" : "alpha Ready", + args[1] === "get" ? `ID: ${sandboxId}\n` : "alpha Ready", ); recoverUnfinished.mockRejectedValueOnce(new Error("unfinished recovery failed")); @@ -425,7 +426,7 @@ describe("runSandboxGpuCreateFlow provider-owned managed create", () => { expect(mocks.streamSandboxCreate).not.toHaveBeenCalled(); expect(errorOutput()).toContain("recovery stopped before sandbox 'alpha' was created"); expect(errorOutput()).toContain("Transaction"); - expect(errorOutput()).toContain("durable sandbox ID mxc-alpha"); + expect(errorOutput()).toContain(`durable sandbox ID ${sandboxId}`); expect(errorOutput()).toContain("OpenShell's sandbox get command"); expect(errorOutput()).toContain("never delete a runtime by mutable sandbox name"); expect(errorOutput()).toContain("Authorization: Bearer "); From 94d6e78697b32245d2330cd30323589c73ed59a2 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 27 Aug 2026 02:53:20 -0700 Subject: [PATCH 03/13] test(onboard): bind fixture create attempts Signed-off-by: Apurv Kumaria --- test/helpers/managed-image-buildless-e2e.ts | 7 +- .../onboard-created-sandbox-fixture.test.ts | 59 ++++++++++++--- test/helpers/onboard-script-mocks.cjs | 75 +++++++++++++++++-- .../onboard-custom-dockerfile.test.ts | 2 +- ...oard-extra-provider-reconciliation.test.ts | 2 +- .../onboard-fresh-create-identity.test.ts | 2 +- .../onboard-installer-restore-intent.test.ts | 2 +- test/onboarding/onboard-messaging.test.ts | 16 ++-- .../onboard-prepared-build-context.test.ts | 2 +- .../onboard-reservation-recreate.test.ts | 2 +- test/onboarding/onboard-sandbox-build.test.ts | 10 +-- .../onboard-sandbox-recreation.test.ts | 14 ++-- .../onboard-terminal-dashboard.test.ts | 2 +- test/security/shellquote-sandbox.test.ts | 4 +- 14 files changed, 149 insertions(+), 50 deletions(-) diff --git a/test/helpers/managed-image-buildless-e2e.ts b/test/helpers/managed-image-buildless-e2e.ts index 4cad383c6c5..ab458c687b0 100644 --- a/test/helpers/managed-image-buildless-e2e.ts +++ b/test/helpers/managed-image-buildless-e2e.ts @@ -568,8 +568,11 @@ childProcess.spawn = (command, args = [], options = {}) => { return poison("docker build"); } if (normalized.includes("sandbox create")) { - if (createdSandbox.state.lifecycleState === "deleted") createdSandbox.recreate(); - else createdSandbox.create(); + if (createdSandbox.state.lifecycleState === "deleted") { + createdSandbox.recreate([command, ...argv]); + } else { + createdSandbox.create([command, ...argv]); + } } spawnCalls.push({ command: String(command), args: argv }); const child = new EventEmitter(); diff --git a/test/helpers/onboard-created-sandbox-fixture.test.ts b/test/helpers/onboard-created-sandbox-fixture.test.ts index a6e6210f1f1..72f3cfa1c80 100644 --- a/test/helpers/onboard-created-sandbox-fixture.test.ts +++ b/test/helpers/onboard-created-sandbox-fixture.test.ts @@ -10,10 +10,11 @@ import { type CreatedSandboxFixture = { readonly capture: (command: string[]) => string | null; - readonly create: () => void; + readonly create: (command: string[]) => void; readonly delete: () => void; - readonly recreate: () => void; + readonly recreate: (command: string[]) => void; readonly setPhase: (phase: string) => void; + readonly run: (command: string[]) => { status: number; stdout: Buffer; stderr: Buffer } | null; readonly state: Readonly<{ sandboxName: string; sandboxId: string; @@ -21,6 +22,8 @@ type CreatedSandboxFixture = { phase: string; lifecycleState: string; generation: number; + createAttemptNonce: string | null; + ownerScopedIdentityObserved: boolean; }>; }; @@ -30,15 +33,14 @@ const { createCreatedSandboxFixture } = require("./onboard-script-mocks.cjs") as const CREATE_ATTEMPT_NONCE = "a".repeat(62); -function selectorListCommand(gatewayName: string): string[] { +function selectorListCommand(gatewayName: string | null, nonce = CREATE_ATTEMPT_NONCE): string[] { return [ "openshell", "sandbox", "list", - "-g", - gatewayName, + ...(gatewayName === null ? [] : ["-g", gatewayName]), "--selector", - `ai.nvidia.nemoclaw.create-attempt=${CREATE_ATTEMPT_NONCE}`, + `ai.nvidia.nemoclaw.create-attempt=${nonce}`, "--output", "json", "--limit", @@ -46,6 +48,16 @@ function selectorListCommand(gatewayName: string): string[] { ]; } +function createCommand(nonce = CREATE_ATTEMPT_NONCE): string[] { + return [ + "openshell", + "sandbox", + "create", + "--label", + `ai.nvidia.nemoclaw.create-attempt=${nonce}`, + ]; +} + describe("created sandbox fixture", () => { it("uses one ID for create, list, and get observations (#10463)", () => { const fixture = createCreatedSandboxFixture({ @@ -55,7 +67,7 @@ describe("created sandbox fixture", () => { }); expect(fixture.capture(selectorListCommand("gateway-alpha"))).toBe("[]"); - fixture.create(); + fixture.create(createCommand()); const createdSandboxId = fixture.state.sandboxId; const selectorOutput = fixture.capture(selectorListCommand("gateway-alpha")); @@ -66,6 +78,7 @@ describe("created sandbox fixture", () => { const listOutput = fixture.capture(["openshell", "sandbox", "list", "-g", "gateway-alpha"]); expect(listOutput).toBe("alpha Ready\n"); expect(fixture.state.sandboxId).toBe(createdSandboxId); + expect(fixture.run(["openshell", "sandbox", "get", "alpha"])).toBeNull(); const getOutput = fixture.capture([ "openshell", @@ -76,6 +89,11 @@ describe("created sandbox fixture", () => { "alpha", ]); expect(parseOpenShellSandboxId(getOutput ?? "")).toBe(createdSandboxId); + expect( + parseOpenShellSandboxId( + fixture.run(["openshell", "sandbox", "get", "alpha"])?.stdout.toString() ?? "", + ), + ).toBe(createdSandboxId); }); it("invalidates the prior ID before recreation publishes a new ID (#10463)", () => { @@ -84,7 +102,7 @@ describe("created sandbox fixture", () => { sandboxId: "sandbox-alpha", gatewayName: "gateway-alpha", }); - fixture.create(); + fixture.create(createCommand()); const priorSandboxId = fixture.state.sandboxId; fixture.delete(); @@ -93,12 +111,14 @@ describe("created sandbox fixture", () => { "", ); - fixture.recreate(); + const replacementNonce = "b".repeat(62); + fixture.recreate(createCommand(replacementNonce)); const replacementSandboxId = fixture.state.sandboxId; expect(replacementSandboxId).not.toBe(priorSandboxId); const replacementRows = parseStrictOpenShellSandboxListJson( - fixture.capture(selectorListCommand("gateway-alpha")) ?? "", + fixture.capture(selectorListCommand("gateway-alpha", replacementNonce)) ?? "", ); + expect(fixture.capture(selectorListCommand("gateway-alpha"))).toBe("[]"); expect(replacementRows?.[0]?.id).toBe(replacementSandboxId); expect(replacementRows?.[0]?.id).not.toBe(priorSandboxId); expect( @@ -124,11 +144,28 @@ describe("created sandbox fixture", () => { sandboxId: "sandbox-alpha", gatewayName: "gateway-alpha", }); - fixture.create(); + fixture.create(createCommand()); expect(fixture.capture(selectorListCommand("gateway-bravo"))).toBeNull(); expect( fixture.capture(["openshell", "sandbox", "get", "-g", "gateway-bravo", "alpha"]), ).toBeNull(); + expect(fixture.capture(selectorListCommand(null))).toBeNull(); + expect(fixture.capture(["openshell", "sandbox", "get", "alpha"])).toBeNull(); + }); + + it("does not answer a selector for another create attempt (#10463)", () => { + const fixture = createCreatedSandboxFixture({ gatewayName: "gateway-alpha" }); + fixture.create(createCommand()); + + expect(fixture.capture(selectorListCommand("gateway-alpha", "b".repeat(62)))).toBe("[]"); + }); + + it("rejects a malformed create-attempt nonce (#10463)", () => { + const fixture = createCreatedSandboxFixture(); + + expect(() => fixture.create(createCommand("invalid"))).toThrow( + "Created sandbox fixture requires one valid create-attempt label.", + ); }); }); diff --git a/test/helpers/onboard-script-mocks.cjs b/test/helpers/onboard-script-mocks.cjs index e2eafda1288..feaf4670b5c 100644 --- a/test/helpers/onboard-script-mocks.cjs +++ b/test/helpers/onboard-script-mocks.cjs @@ -334,8 +334,14 @@ function createCreatedSandboxFixture(options = {}) { phase: hasOwn(options, "phase") ? options.phase : "Ready", lifecycleState: initialLifecycleState, generation: initialLifecycleState === "created" ? 1 : 0, + createAttemptNonce: null, + ownerScopedIdentityObserved: initialLifecycleState === "created", }; const lifecycleStates = new Set(["absent", "created", "deleted"]); + const createAttemptNoncePattern = new RegExp( + `^[0-9a-f]{${sandboxIdentity.NEMOCLAW_CREATE_ATTEMPT_NONCE_HEX_LENGTH}}$`, + "u", + ); const assertState = () => { if ( @@ -361,6 +367,12 @@ function createCreatedSandboxFixture(options = {}) { if (!lifecycleStates.has(state.lifecycleState)) { throw new Error("Created sandbox fixture requires one known lifecycle state."); } + if ( + state.createAttemptNonce !== null && + !createAttemptNoncePattern.test(state.createAttemptNonce) + ) { + throw new Error("Created sandbox fixture requires one valid create-attempt nonce."); + } }; const commandDetails = (command) => { @@ -369,18 +381,45 @@ function createCreatedSandboxFixture(options = {}) { if (sandboxIndex < 0) return null; const gatewayIndex = args.findIndex((arg) => arg === "-g" || arg === "--gateway"); const gatewayName = gatewayIndex >= 0 ? args[gatewayIndex + 1] || null : null; - if (gatewayName !== null && gatewayName !== state.gatewayName) return null; - return { args, action: args[sandboxIndex + 1] || null }; + return { args, action: args[sandboxIndex + 1] || null, gatewayName }; + }; + + const nonceFromCreateCommand = (command) => { + const details = commandDetails(command); + if (!details || details.action !== "create") { + throw new Error("Created sandbox fixture requires one sandbox create command."); + } + const prefix = `${sandboxIdentity.NEMOCLAW_CREATE_ATTEMPT_LABEL}=`; + const labels = details.args.flatMap((arg, index) => { + if (arg === "--label") return [details.args[index + 1] || ""]; + return arg.startsWith("--label=") ? [arg.slice("--label=".length)] : []; + }); + const nonces = labels + .filter((label) => label.startsWith(prefix)) + .map((label) => label.slice(prefix.length)); + if (nonces.length !== 1 || !createAttemptNoncePattern.test(nonces[0])) { + throw new Error("Created sandbox fixture requires one valid create-attempt label."); + } + return nonces[0]; }; const isCreated = () => state.lifecycleState === "created"; - const capture = (command) => { + const observe = (command, allowPublishedUnscopedGet) => { const details = commandDetails(command); if (!details) return null; - const { args, action } = details; + const { args, action, gatewayName } = details; if (action === "get") { + const wrongGateway = gatewayName !== null && gatewayName !== state.gatewayName; + const unscopedBeforePublication = + gatewayName === null && (!allowPublishedUnscopedGet || !state.ownerScopedIdentityObserved); + if (wrongGateway || unscopedBeforePublication) { + return null; + } const sandboxName = args.at(-1); if (sandboxName !== state.sandboxName) return null; + if (gatewayName === state.gatewayName && isCreated()) { + state.ownerScopedIdentityObserved = true; + } return isCreated() ? `Name: ${state.sandboxName}\nId: ${state.sandboxId}\nPhase: ${state.phase}\n` : ""; @@ -389,6 +428,7 @@ function createCreatedSandboxFixture(options = {}) { const selectorIndex = args.indexOf("--selector"); if (selectorIndex >= 0) { + if (gatewayName !== state.gatewayName) return null; const selector = args[selectorIndex + 1] || ""; const prefix = `${sandboxIdentity.NEMOCLAW_CREATE_ATTEMPT_LABEL}=`; if (!selector.startsWith(prefix)) return null; @@ -397,6 +437,7 @@ function createCreatedSandboxFixture(options = {}) { } if (!isCreated()) return "[]"; const nonce = selector.slice(prefix.length); + if (nonce !== state.createAttemptNonce) return "[]"; return JSON.stringify([ { id: state.sandboxId, @@ -410,21 +451,35 @@ function createCreatedSandboxFixture(options = {}) { ]); } + if (gatewayName !== null && gatewayName !== state.gatewayName) return null; return isCreated() ? `${state.sandboxName} ${state.phase}\n` : "No sandboxes found.\n"; }; + const capture = (command) => observe(command, false); + const run = (command) => { - const output = capture(command); + const output = observe(command, true); return output === null ? null : { status: 0, stdout: Buffer.from(output), stderr: Buffer.alloc(0) }; }; - const create = () => { - if (state.lifecycleState === "created") return; + const create = (command) => { + const details = commandDetails(command); + if (!details || details.action !== "create") return; + const createAttemptNonce = nonceFromCreateCommand(command); + if (state.lifecycleState === "created") { + if (createAttemptNonce !== state.createAttemptNonce) { + throw new Error("Created sandbox fixture cannot change a live create attempt."); + } + return; + } if (state.lifecycleState !== "absent") { throw new Error("Created sandbox fixture cannot create a deleted sandbox."); } + state.createAttemptNonce = createAttemptNonce; + state.ownerScopedIdentityObserved = false; + assertState(); state.lifecycleState = "created"; state.generation += 1; }; @@ -434,14 +489,18 @@ function createCreatedSandboxFixture(options = {}) { throw new Error("Created sandbox fixture can delete only a created sandbox."); } state.lifecycleState = "deleted"; + state.ownerScopedIdentityObserved = false; }; - const recreate = () => { + const recreate = (command) => { if (state.lifecycleState !== "deleted") { throw new Error("Created sandbox fixture can recreate only a deleted sandbox."); } + const createAttemptNonce = nonceFromCreateCommand(command); state.generation += 1; state.sandboxId = `${initialSandboxId}-recreated-${state.generation - 1}`; + state.createAttemptNonce = createAttemptNonce; + state.ownerScopedIdentityObserved = false; assertState(); state.lifecycleState = "created"; }; diff --git a/test/onboarding/onboard-custom-dockerfile.test.ts b/test/onboarding/onboard-custom-dockerfile.test.ts index e3d20ca705f..3ba7979248c 100644 --- a/test/onboarding/onboard-custom-dockerfile.test.ts +++ b/test/onboarding/onboard-custom-dockerfile.test.ts @@ -262,7 +262,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { - createdSandbox.create(); + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); diff --git a/test/onboarding/onboard-extra-provider-reconciliation.test.ts b/test/onboarding/onboard-extra-provider-reconciliation.test.ts index bb8ed6bb074..5140b70bf36 100644 --- a/test/onboarding/onboard-extra-provider-reconciliation.test.ts +++ b/test/onboarding/onboard-extra-provider-reconciliation.test.ts @@ -111,7 +111,7 @@ sandboxBaseImage.resolveSandboxBaseImage = () => ({ }); childProcess.spawn = (...args) => { - createdSandbox.create(); + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); diff --git a/test/onboarding/onboard-fresh-create-identity.test.ts b/test/onboarding/onboard-fresh-create-identity.test.ts index 58f321fd518..93be790d832 100644 --- a/test/onboarding/onboard-fresh-create-identity.test.ts +++ b/test/onboarding/onboard-fresh-create-identity.test.ts @@ -146,7 +146,7 @@ process.kill = (pid, signal) => { }; childProcess.spawn = (...args) => { - createdSandbox.create(); + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); diff --git a/test/onboarding/onboard-installer-restore-intent.test.ts b/test/onboarding/onboard-installer-restore-intent.test.ts index af6af55bce1..c41374afc78 100644 --- a/test/onboarding/onboard-installer-restore-intent.test.ts +++ b/test/onboarding/onboard-installer-restore-intent.test.ts @@ -142,7 +142,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); childProcess.spawn = (...args) => { const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); if (command.includes("sandbox create")) { - createdSandbox.recreate(); + createdSandbox.recreate(args.flat()); createdSandbox.setPhase("Ready"); } const child = new EventEmitter(); diff --git a/test/onboarding/onboard-messaging.test.ts b/test/onboarding/onboard-messaging.test.ts index 12f0677b49e..2fc7c305c14 100644 --- a/test/onboarding/onboard-messaging.test.ts +++ b/test/onboarding/onboard-messaging.test.ts @@ -110,7 +110,7 @@ const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { - createdSandbox.create(); + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -397,7 +397,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { - createdSandbox.create(); + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -559,7 +559,7 @@ const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, }); preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { - createdSandbox.create(); + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); child.unref = () => {}; child.pid = 4242; const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); const attachedProviders = [...command.matchAll(/--provider ([^ ]+)/g)].map((match) => match[1]); commands.push({ command, providerRevisions: command.includes("sandbox create") ? Object.fromEntries(attachedProviders.map((name) => [name, revisions.get(name)])) : null, rawCredentialInEnv: Object.values(args[2]?.env || {}).includes(rawGatewayCredential) }); @@ -748,7 +748,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { - createdSandbox.create(); + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -909,7 +909,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { - createdSandbox.create(); + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -1080,7 +1080,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { - createdSandbox.create(); + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -1409,7 +1409,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { - createdSandbox.create(); + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -1550,7 +1550,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { - createdSandbox.create(); + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); diff --git a/test/onboarding/onboard-prepared-build-context.test.ts b/test/onboarding/onboard-prepared-build-context.test.ts index 8477c31bfd3..1bab57024c4 100644 --- a/test/onboarding/onboard-prepared-build-context.test.ts +++ b/test/onboarding/onboard-prepared-build-context.test.ts @@ -185,7 +185,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { - createdSandbox.create(); + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); diff --git a/test/onboarding/onboard-reservation-recreate.test.ts b/test/onboarding/onboard-reservation-recreate.test.ts index c7d36a6032a..be2a3334545 100644 --- a/test/onboarding/onboard-reservation-recreate.test.ts +++ b/test/onboarding/onboard-reservation-recreate.test.ts @@ -135,7 +135,7 @@ policyAuthorityPreflight.qualifySandboxPolicyAuthority = () => ({ childProcess.spawn = (...args) => { const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); if (command.includes("sandbox create")) { - createdSandbox.recreate(); + createdSandbox.recreate(args.flat()); createdSandbox.setPhase("Ready"); } const child = new EventEmitter(); diff --git a/test/onboarding/onboard-sandbox-build.test.ts b/test/onboarding/onboard-sandbox-build.test.ts index 6bb9282c588..e3dfc5e3062 100644 --- a/test/onboarding/onboard-sandbox-build.test.ts +++ b/test/onboarding/onboard-sandbox-build.test.ts @@ -95,7 +95,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { - createdSandbox.create(); + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -321,7 +321,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { - createdSandbox.create(); + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -539,7 +539,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { - createdSandbox.create(); + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -653,7 +653,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { - createdSandbox.create(); + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -770,7 +770,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { - createdSandbox.create(); + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); diff --git a/test/onboarding/onboard-sandbox-recreation.test.ts b/test/onboarding/onboard-sandbox-recreation.test.ts index 38fc3803de0..749eddc3dd7 100644 --- a/test/onboarding/onboard-sandbox-recreation.test.ts +++ b/test/onboarding/onboard-sandbox-recreation.test.ts @@ -194,7 +194,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); childProcess.spawn = (...args) => { const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); - if (command.includes("sandbox create")) createdSandbox.recreate(); + if (command.includes("sandbox create")) createdSandbox.recreate(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -374,7 +374,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); childProcess.spawn = (...args) => { const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); - if (command.includes("sandbox create")) createdSandbox.recreate(); + if (command.includes("sandbox create")) createdSandbox.recreate(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -540,7 +540,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); childProcess.spawn = (...args) => { const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); - if (command.includes("sandbox create")) createdSandbox.recreate(); + if (command.includes("sandbox create")) createdSandbox.recreate(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -704,7 +704,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); childProcess.spawn = (...args) => { const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); if (command.includes("sandbox create")) { - createdSandbox.recreate(); + createdSandbox.recreate(args.flat()); createdSandbox.setPhase("Ready"); } const child = new EventEmitter(); @@ -856,7 +856,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); childProcess.spawn = (...args) => { const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); - if (command.includes("sandbox create")) createdSandbox.recreate(); + if (command.includes("sandbox create")) createdSandbox.recreate(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -1167,7 +1167,7 @@ credentials.prompt = async () => "y"; childProcess.spawn = (...args) => { const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); - if (command.includes("sandbox create")) createdSandbox.recreate(); + if (command.includes("sandbox create")) createdSandbox.recreate(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -1325,7 +1325,7 @@ credentials.prompt = async () => "y"; const fakeSpawn = (...args) => { const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); if (command.includes("sandbox create") && createdSandbox.state.lifecycleState === "deleted") { - createdSandbox.recreate(); + createdSandbox.recreate(args.flat()); createdSandbox.setPhase("Ready"); } const child = new EventEmitter(); diff --git a/test/onboarding/onboard-terminal-dashboard.test.ts b/test/onboarding/onboard-terminal-dashboard.test.ts index 4a53c93c201..6c54cec758b 100644 --- a/test/onboarding/onboard-terminal-dashboard.test.ts +++ b/test/onboarding/onboard-terminal-dashboard.test.ts @@ -166,7 +166,7 @@ const createFixture = childProcess.spawn = (...args) => { if (scenario === "reuse") throw new Error("unexpected sandbox create"); - createdSandbox.create(); + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); diff --git a/test/security/shellquote-sandbox.test.ts b/test/security/shellquote-sandbox.test.ts index 618af746f26..5e960b4e055 100644 --- a/test/security/shellquote-sandbox.test.ts +++ b/test/security/shellquote-sandbox.test.ts @@ -135,8 +135,8 @@ const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, }); preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; -sandboxCreateStream.streamSandboxCreate = async () => { - createdSandbox.create(); +sandboxCreateStream.streamSandboxCreate = async (...args) => { + createdSandbox.create(args.flat()); return { status: 0, output: "Built image openshell/sandbox-from:123\nCreated sandbox: my-assistant", From 4af7879d28e6d3a24857c315e47147780029ebaf Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 27 Aug 2026 10:49:43 -0700 Subject: [PATCH 04/13] test(onboard): keep managed sandbox identity consistent Signed-off-by: Apurv Kumaria --- src/lib/onboard/sandbox-gpu-create-flow.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/onboard/sandbox-gpu-create-flow.test.ts b/src/lib/onboard/sandbox-gpu-create-flow.test.ts index 5d962c6004f..b0ab1591b87 100644 --- a/src/lib/onboard/sandbox-gpu-create-flow.test.ts +++ b/src/lib/onboard/sandbox-gpu-create-flow.test.ts @@ -354,7 +354,7 @@ describe("runSandboxGpuCreateFlow provider-owned managed create", () => { const deps = createDeps(sandboxId); const adapterOverride = {} as never; deps.createManagedBootstrapAdapter = vi.fn(() => adapterOverride); - deps.runOpenshell = vi.fn(() => readySandboxGetResult("alpha-sandbox-id")); + deps.runOpenshell = vi.fn(() => readySandboxGetResult(sandboxId)); vi.mocked(deps.runCaptureOpenshell).mockImplementation((args) => args[1] === "get" ? `ID: ${sandboxId}\n` : "alpha Ready", ); From f8c36a09b5b8efc065dd826c42c63a267b9371e1 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 27 Aug 2026 11:03:04 -0700 Subject: [PATCH 05/13] test(onboard): keep fixture observations consistent Signed-off-by: Apurv Kumaria --- ci/test-file-size-budget.json | 2 +- test/helpers/managed-image-buildless-e2e.ts | 5 +++- .../onboard-created-sandbox-fixture.test.ts | 18 ++++++------ test/helpers/onboard-script-mocks.cjs | 8 +++++- test/onboarding/onboard-messaging.test.ts | 17 ++++++----- .../onboard-reservation-recreate.test.ts | 6 ++-- .../onboard-sandbox-recreation.test.ts | 28 +++++++++---------- 7 files changed, 46 insertions(+), 38 deletions(-) diff --git a/ci/test-file-size-budget.json b/ci/test-file-size-budget.json index 326d95eb5c6..8fb9442b5d0 100644 --- a/ci/test-file-size-budget.json +++ b/ci/test-file-size-budget.json @@ -8,7 +8,7 @@ "test/generation/generate-openclaw-config.test.ts": 1898, "test/installer-integration/install-preflight.test.ts": 3025, "test/agents/openclaw/runtime/nemoclaw-start.test.ts": 4626, - "test/onboarding/onboard-messaging.test.ts": 1977, + "test/onboarding/onboard-messaging.test.ts": 1976, "test/onboarding/onboard-selection.test.ts": 4176 } } diff --git a/test/helpers/managed-image-buildless-e2e.ts b/test/helpers/managed-image-buildless-e2e.ts index 7c69d85337d..de33e67b86d 100644 --- a/test/helpers/managed-image-buildless-e2e.ts +++ b/test/helpers/managed-image-buildless-e2e.ts @@ -424,7 +424,10 @@ runner.run = (command, options = {}) => { const argv = Array.isArray(command) ? command.map(String) : []; const normalized = normalize(command); runnerCommands.push(normalized); - if (normalized.includes("sandbox delete")) { + if ( + normalized.includes("sandbox delete") && + createdSandbox.state.lifecycleState === "created" + ) { createdSandbox.delete(); existingEntryAvailable = false; } diff --git a/test/helpers/onboard-created-sandbox-fixture.test.ts b/test/helpers/onboard-created-sandbox-fixture.test.ts index 72f3cfa1c80..f070b6f1e3e 100644 --- a/test/helpers/onboard-created-sandbox-fixture.test.ts +++ b/test/helpers/onboard-created-sandbox-fixture.test.ts @@ -2,8 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 import { describe, expect, it } from "vitest"; +import { createRequire } from "node:module"; import { + NEMOCLAW_CREATE_ATTEMPT_LABEL, + NEMOCLAW_CREATE_ATTEMPT_NONCE_HEX_LENGTH, parseOpenShellSandboxId, parseStrictOpenShellSandboxListJson, } from "../../src/lib/adapters/openshell/sandbox-identity"; @@ -27,11 +30,12 @@ type CreatedSandboxFixture = { }>; }; -const { createCreatedSandboxFixture } = require("./onboard-script-mocks.cjs") as { +const requireCjs = createRequire(import.meta.url); +const { createCreatedSandboxFixture } = requireCjs("./onboard-script-mocks.cjs") as { createCreatedSandboxFixture: (options?: Record) => CreatedSandboxFixture; }; -const CREATE_ATTEMPT_NONCE = "a".repeat(62); +const CREATE_ATTEMPT_NONCE = "a".repeat(NEMOCLAW_CREATE_ATTEMPT_NONCE_HEX_LENGTH); function selectorListCommand(gatewayName: string | null, nonce = CREATE_ATTEMPT_NONCE): string[] { return [ @@ -40,7 +44,7 @@ function selectorListCommand(gatewayName: string | null, nonce = CREATE_ATTEMPT_ "list", ...(gatewayName === null ? [] : ["-g", gatewayName]), "--selector", - `ai.nvidia.nemoclaw.create-attempt=${nonce}`, + `${NEMOCLAW_CREATE_ATTEMPT_LABEL}=${nonce}`, "--output", "json", "--limit", @@ -49,13 +53,7 @@ function selectorListCommand(gatewayName: string | null, nonce = CREATE_ATTEMPT_ } function createCommand(nonce = CREATE_ATTEMPT_NONCE): string[] { - return [ - "openshell", - "sandbox", - "create", - "--label", - `ai.nvidia.nemoclaw.create-attempt=${nonce}`, - ]; + return ["openshell", "sandbox", "create", "--label", `${NEMOCLAW_CREATE_ATTEMPT_LABEL}=${nonce}`]; } describe("created sandbox fixture", () => { diff --git a/test/helpers/onboard-script-mocks.cjs b/test/helpers/onboard-script-mocks.cjs index 01aeb8e20e4..68ed8c407e0 100644 --- a/test/helpers/onboard-script-mocks.cjs +++ b/test/helpers/onboard-script-mocks.cjs @@ -98,7 +98,11 @@ function mockManagedEndpointlessProviderProfileRun(command) { ); } -function createStatefulMessagingProviderRunner({ commands, initialProviders = [] }) { +function createStatefulMessagingProviderRunner({ + commands, + initialProviders = [], + createdSandbox = null, +}) { const providers = new Map( initialProviders.map(([name, type, credential]) => [name, { type, credential }]), ); @@ -116,6 +120,8 @@ function createStatefulMessagingProviderRunner({ commands, initialProviders = [] const args = normalized.split(/\s+/); const providerIndex = args.indexOf("provider"); commands.push({ command: normalized, env: options.env || null }); + const sandboxResult = createdSandbox?.run(command) ?? null; + if (sandboxResult !== null) return sandboxResult; const providerAction = providerIndex >= 0 ? args[providerIndex + 1] : null; if (providerAction === "profile") { diff --git a/test/onboarding/onboard-messaging.test.ts b/test/onboarding/onboard-messaging.test.ts index 49c2611a7cf..e3411424c47 100644 --- a/test/onboarding/onboard-messaging.test.ts +++ b/test/onboarding/onboard-messaging.test.ts @@ -82,9 +82,7 @@ const { EventEmitter } = require("node:events"); const fs = require("node:fs"); const commands = []; const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName: "my-assistant" }); -const messagingProviderRunner = fixtureMocks.createStatefulMessagingProviderRunner({ commands }); -runner.run = (command, options = {}) => - createdSandbox.run(command) ?? messagingProviderRunner(command, options); +runner.run = fixtureMocks.createStatefulMessagingProviderRunner({ commands, createdSandbox }); runner.runCapture = (command) => { const sandboxCapture = createdSandbox.capture(command); if (sandboxCapture !== null) return sandboxCapture; @@ -365,9 +363,7 @@ const nonSlackMessagingEnvKeys = [ const commands = []; let registeredSandbox = null; const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName: "my-assistant" }); -const messagingProviderRunner = fixtureMocks.createStatefulMessagingProviderRunner({ commands }); -runner.run = (command, options = {}) => - createdSandbox.run(command) ?? messagingProviderRunner(command, options); +runner.run = fixtureMocks.createStatefulMessagingProviderRunner({ commands, createdSandbox }); runner.runCapture = (command) => { const sandboxCapture = createdSandbox.capture(command); if (sandboxCapture !== null) return sandboxCapture; @@ -1275,13 +1271,14 @@ const commands = []; const existingSandbox = fixtureMocks.createCreatedSandboxFixture({ lifecycleState: "created" }); const messagingProviderRunner = require(${onboardScriptMocksPath}).createStatefulMessagingProviderRunner({ commands, + createdSandbox: existingSandbox, initialProviders: [ ["my-assistant-discord-bridge", "nemoclaw-mcp-v1", "DISCORD_BOT_TOKEN"], ["my-assistant-slack-bridge", "nemoclaw-mcp-v1", "SLACK_BOT_TOKEN"], ["my-assistant-slack-app", "nemoclaw-mcp-v1", "SLACK_APP_TOKEN"], ], }); -runner.run = (command, options = {}) => existingSandbox.run(command) ?? messagingProviderRunner(command, options); +runner.run = messagingProviderRunner; runner.runCapture = (command) => { const sandboxCapture = existingSandbox.capture(command); if (sandboxCapture !== null) return sandboxCapture; @@ -1384,8 +1381,10 @@ const { EventEmitter } = require("node:events"); const commands = []; const createdSandbox = fixtureMocks.createCreatedSandboxFixture(); -const providerRunner = require(${onboardScriptMocksPath}).createStatefulMessagingProviderRunner({ commands }); -runner.run = (command, opts = {}) => createdSandbox.run(command) ?? providerRunner(command, opts); +runner.run = require(${onboardScriptMocksPath}).createStatefulMessagingProviderRunner({ + commands, + createdSandbox, +}); runner.runCapture = (command) => { const createdIdentity = createdSandbox.capture(command); if (createdIdentity !== null) return createdIdentity; diff --git a/test/onboarding/onboard-reservation-recreate.test.ts b/test/onboarding/onboard-reservation-recreate.test.ts index 4aaa0c5b826..00e4d242f3c 100644 --- a/test/onboarding/onboard-reservation-recreate.test.ts +++ b/test/onboarding/onboard-reservation-recreate.test.ts @@ -491,8 +491,10 @@ process.kill = (pid, signal) => { }; childProcess.spawn = (...args) => { const command = normalize([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); - if (command.includes("sandbox create")) fs.appendFileSync(createCountPath, "create\n"); - createdSandbox.create(args.flat()); + if (command.includes("sandbox create")) { + fs.appendFileSync(createCountPath, "create\n"); + createdSandbox.create(args.flat()); + } const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); diff --git a/test/onboarding/onboard-sandbox-recreation.test.ts b/test/onboarding/onboard-sandbox-recreation.test.ts index 749eddc3dd7..a406ebd856c 100644 --- a/test/onboarding/onboard-sandbox-recreation.test.ts +++ b/test/onboarding/onboard-sandbox-recreation.test.ts @@ -160,7 +160,7 @@ runner.run = (command, opts = {}) => { const cmd = _n(command); const profileResult = require(${onboardScriptMocksPath}).mockEndpointlessProviderProfileRun(command, "nemoclaw-mcp-v1", false); if (profileResult !== null) return profileResult; - if (cmd.includes("sandbox delete")) createdSandbox.delete(); + if (cmd.includes("sandbox delete") && createdSandbox.state.lifecycleState === "created") createdSandbox.delete(); commands.push({ command: cmd, env: opts.env || null }); return createdSandbox.run(command) ?? { status: 0 }; }; @@ -194,7 +194,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); childProcess.spawn = (...args) => { const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); - if (command.includes("sandbox create")) createdSandbox.recreate(args.flat()); + if (command.includes("sandbox create") && createdSandbox.state.lifecycleState === "deleted") createdSandbox.recreate(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -316,7 +316,7 @@ runner.run = (command) => { const cmd = _n(command); const profileResult = require(${onboardScriptMocksPath}).mockEndpointlessProviderProfileRun(command, "nemoclaw-mcp-v1", false); if (profileResult !== null) return profileResult; - if (cmd.includes("sandbox delete")) createdSandbox.delete(); + if (cmd.includes("sandbox delete") && createdSandbox.state.lifecycleState === "created") createdSandbox.delete(); events.push({ kind: "run", cmd }); return createdSandbox.run(command) ?? { status: 0 }; }; @@ -374,7 +374,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); childProcess.spawn = (...args) => { const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); - if (command.includes("sandbox create")) createdSandbox.recreate(args.flat()); + if (command.includes("sandbox create") && createdSandbox.state.lifecycleState === "deleted") createdSandbox.recreate(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -495,7 +495,7 @@ runner.run = (command) => { const cmd = _n(command); const profileResult = require(${onboardScriptMocksPath}).mockEndpointlessProviderProfileRun(command, "nemoclaw-mcp-v1", false); if (profileResult !== null) return profileResult; - if (cmd.includes("sandbox delete")) createdSandbox.delete(); + if (cmd.includes("sandbox delete") && createdSandbox.state.lifecycleState === "created") createdSandbox.delete(); events.push({ kind: "run", cmd }); return createdSandbox.run(command) ?? { status: 0 }; }; @@ -540,7 +540,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); childProcess.spawn = (...args) => { const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); - if (command.includes("sandbox create")) createdSandbox.recreate(args.flat()); + if (command.includes("sandbox create") && createdSandbox.state.lifecycleState === "deleted") createdSandbox.recreate(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -645,7 +645,7 @@ runner.run = (command) => { const cmd = _n(command); const profileResult = require(${onboardScriptMocksPath}).mockEndpointlessProviderProfileRun(command, "nemoclaw-mcp-v1", false); if (profileResult !== null) return profileResult; - if (cmd.includes("sandbox delete")) createdSandbox.delete(); + if (cmd.includes("sandbox delete") && createdSandbox.state.lifecycleState === "created") createdSandbox.delete(); events.push({ kind: "run", cmd }); return createdSandbox.run(command) ?? { status: 0 }; }; @@ -703,7 +703,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); childProcess.spawn = (...args) => { const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); - if (command.includes("sandbox create")) { + if (command.includes("sandbox create") && createdSandbox.state.lifecycleState === "deleted") { createdSandbox.recreate(args.flat()); createdSandbox.setPhase("Ready"); } @@ -814,7 +814,7 @@ runner.run = (command, opts = {}) => { const cmd = _n(command); const profileResult = require(${onboardScriptMocksPath}).mockEndpointlessProviderProfileRun(command, "nemoclaw-mcp-v1", false); if (profileResult !== null) return profileResult; - if (cmd.includes("sandbox delete")) createdSandbox.delete(); + if (cmd.includes("sandbox delete") && createdSandbox.state.lifecycleState === "created") createdSandbox.delete(); commands.push({ command: cmd, env: opts.env || null }); return createdSandbox.run(command) ?? { status: 0 }; }; @@ -856,7 +856,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); childProcess.spawn = (...args) => { const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); - if (command.includes("sandbox create")) createdSandbox.recreate(args.flat()); + if (command.includes("sandbox create") && createdSandbox.state.lifecycleState === "deleted") createdSandbox.recreate(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -958,7 +958,7 @@ runner.run = (command, opts = {}) => { const cmd = _n(command); const profileResult = require(${onboardScriptMocksPath}).mockEndpointlessProviderProfileRun(command, "nemoclaw-mcp-v1", false); if (profileResult !== null) return profileResult; - if (cmd.includes("sandbox delete")) createdSandbox.delete(); + if (cmd.includes("sandbox delete") && createdSandbox.state.lifecycleState === "created") createdSandbox.delete(); const commandString = Array.isArray(command) ? command.join(" ") : String(command); if (cmd.includes("sandbox download")) { const parts = commandString.match(/'([^']*)'/g) || []; @@ -1108,7 +1108,7 @@ runner.run = (command, opts = {}) => { const cmd = _n(command); const profileResult = require(${onboardScriptMocksPath}).mockEndpointlessProviderProfileRun(command, "nemoclaw-mcp-v1", false); if (profileResult !== null) return profileResult; - if (cmd.includes("sandbox delete")) createdSandbox.delete(); + if (cmd.includes("sandbox delete") && createdSandbox.state.lifecycleState === "created") createdSandbox.delete(); const commandString = Array.isArray(command) ? command.join(" ") : String(command); if (cmd.includes("sandbox download")) { const parts = commandString.match(/'([^']*)'/g) || []; @@ -1167,7 +1167,7 @@ credentials.prompt = async () => "y"; childProcess.spawn = (...args) => { const command = _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]); - if (command.includes("sandbox create")) createdSandbox.recreate(args.flat()); + if (command.includes("sandbox create") && createdSandbox.state.lifecycleState === "deleted") createdSandbox.recreate(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -1284,7 +1284,7 @@ runner.run = (command, opts = {}) => { const cmd = _n(command); const profileResult = require(${onboardScriptMocksPath}).mockEndpointlessProviderProfileRun(command, "nemoclaw-mcp-v1", false); if (profileResult !== null) return profileResult; - if (cmd.includes("sandbox delete")) createdSandbox.delete(); + if (cmd.includes("sandbox delete") && createdSandbox.state.lifecycleState === "created") createdSandbox.delete(); commands.push({ command: cmd, env: opts.env || null }); return createdSandbox.run(command) ?? { status: 0 }; }; From 4b594e26754aaa88eb54f351809861a1a8a7dff8 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 27 Aug 2026 11:16:25 -0700 Subject: [PATCH 06/13] test(onboard): report missing fixture sandbox Signed-off-by: Apurv Kumaria --- test/helpers/onboard-created-sandbox-fixture.test.ts | 11 +++++++++-- test/helpers/onboard-script-mocks.cjs | 12 +++++++++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/test/helpers/onboard-created-sandbox-fixture.test.ts b/test/helpers/onboard-created-sandbox-fixture.test.ts index f070b6f1e3e..30497e6e63c 100644 --- a/test/helpers/onboard-created-sandbox-fixture.test.ts +++ b/test/helpers/onboard-created-sandbox-fixture.test.ts @@ -108,8 +108,11 @@ describe("created sandbox fixture", () => { expect(fixture.capture(["openshell", "sandbox", "get", "-g", "gateway-alpha", "alpha"])).toBe( "", ); + expect( + fixture.run(["openshell", "sandbox", "get", "-g", "gateway-alpha", "alpha"]), + ).toMatchObject({ status: 1 }); - const replacementNonce = "b".repeat(62); + const replacementNonce = "b".repeat(NEMOCLAW_CREATE_ATTEMPT_NONCE_HEX_LENGTH); fixture.recreate(createCommand(replacementNonce)); const replacementSandboxId = fixture.state.sandboxId; expect(replacementSandboxId).not.toBe(priorSandboxId); @@ -156,7 +159,11 @@ describe("created sandbox fixture", () => { const fixture = createCreatedSandboxFixture({ gatewayName: "gateway-alpha" }); fixture.create(createCommand()); - expect(fixture.capture(selectorListCommand("gateway-alpha", "b".repeat(62)))).toBe("[]"); + expect( + fixture.capture( + selectorListCommand("gateway-alpha", "b".repeat(NEMOCLAW_CREATE_ATTEMPT_NONCE_HEX_LENGTH)), + ), + ).toBe("[]"); }); it("rejects a malformed create-attempt nonce (#10463)", () => { diff --git a/test/helpers/onboard-script-mocks.cjs b/test/helpers/onboard-script-mocks.cjs index 68ed8c407e0..db89ca6a5d3 100644 --- a/test/helpers/onboard-script-mocks.cjs +++ b/test/helpers/onboard-script-mocks.cjs @@ -498,9 +498,15 @@ function createCreatedSandboxFixture(options = {}) { const run = (command) => { const output = observe(command, true); - return output === null - ? null - : { status: 0, stdout: Buffer.from(output), stderr: Buffer.alloc(0) }; + if (output === null) return null; + if (output === "") { + return { + status: 1, + stdout: Buffer.alloc(0), + stderr: Buffer.from(`Error: sandbox ${state.sandboxName} not found\n`), + }; + } + return { status: 0, stdout: Buffer.from(output), stderr: Buffer.alloc(0) }; }; const create = (command) => { From 00d386b8227de6750d964b5d6c5cbbbf557f34ef Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 27 Aug 2026 11:17:17 -0700 Subject: [PATCH 07/13] test(onboard): remove duplicate selector case Signed-off-by: Apurv Kumaria --- test/helpers/onboard-script-mocks-policy-authority.test.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/test/helpers/onboard-script-mocks-policy-authority.test.ts b/test/helpers/onboard-script-mocks-policy-authority.test.ts index f6b6fddea6f..303514487b8 100644 --- a/test/helpers/onboard-script-mocks-policy-authority.test.ts +++ b/test/helpers/onboard-script-mocks-policy-authority.test.ts @@ -39,13 +39,6 @@ afterEach(() => { }); describe("created sandbox fixture selector observations", () => { - it("publishes identity for the exact gateway-scoped create-attempt query (#9833)", () => { - const fixture = createCreatedSandboxFixture({ gatewayName: "nemoclaw-test" }); - fixture.create(exactCreateCommand); - - expect(fixture.capture(exactCreateQuery)).toContain('"name":"my-assistant"'); - }); - it("publishes identity through the Linux process-tree timeout wrapper (#10238, #9833)", () => { const fixture = createCreatedSandboxFixture({ gatewayName: "nemoclaw-test" }); fixture.create(exactCreateCommand); From 30ed04975cd66f2f2f3bf4cb2f305715a02b09c8 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 27 Aug 2026 11:32:18 -0700 Subject: [PATCH 08/13] test(onboard): bound replacement fixture identity Signed-off-by: Apurv Kumaria --- test/helpers/onboard-created-sandbox-fixture.test.ts | 12 ++++++++++++ test/helpers/onboard-script-mocks.cjs | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/test/helpers/onboard-created-sandbox-fixture.test.ts b/test/helpers/onboard-created-sandbox-fixture.test.ts index 30497e6e63c..b769c9119d6 100644 --- a/test/helpers/onboard-created-sandbox-fixture.test.ts +++ b/test/helpers/onboard-created-sandbox-fixture.test.ts @@ -7,6 +7,7 @@ import { createRequire } from "node:module"; import { NEMOCLAW_CREATE_ATTEMPT_LABEL, NEMOCLAW_CREATE_ATTEMPT_NONCE_HEX_LENGTH, + isOpenShellSandboxId, parseOpenShellSandboxId, parseStrictOpenShellSandboxListJson, } from "../../src/lib/adapters/openshell/sandbox-identity"; @@ -129,6 +130,17 @@ describe("created sandbox fixture", () => { ).toBe(replacementSandboxId); }); + it("keeps a replacement ID valid for a maximum-length input (#10463)", () => { + const maximumSandboxId = "a".repeat(512); + const fixture = createCreatedSandboxFixture({ sandboxId: maximumSandboxId }); + fixture.create(createCommand()); + fixture.delete(); + fixture.recreate(createCommand("b".repeat(NEMOCLAW_CREATE_ATTEMPT_NONCE_HEX_LENGTH))); + + expect(fixture.state.sandboxId).not.toBe(maximumSandboxId); + expect(isOpenShellSandboxId(fixture.state.sandboxId)).toBe(true); + }); + it.each([ ["a missing", undefined], ["an empty", ""], diff --git a/test/helpers/onboard-script-mocks.cjs b/test/helpers/onboard-script-mocks.cjs index db89ca6a5d3..01e43439850 100644 --- a/test/helpers/onboard-script-mocks.cjs +++ b/test/helpers/onboard-script-mocks.cjs @@ -543,7 +543,8 @@ function createCreatedSandboxFixture(options = {}) { } const createAttemptNonce = nonceFromCreateCommand(command); state.generation += 1; - state.sandboxId = `${initialSandboxId}-recreated-${state.generation - 1}`; + const replacementFingerprint = sandboxIdentity.fingerprintOpenShellSandboxId(initialSandboxId); + state.sandboxId = `sbx-recreated-${state.generation}-${replacementFingerprint}`; state.createAttemptNonce = createAttemptNonce; state.ownerScopedIdentityObserved = false; assertState(); From 7e736b2ea1abbd6fd7a4092a8d5301411d5e389b Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 27 Aug 2026 11:51:27 -0700 Subject: [PATCH 09/13] test(onboard): defer shared fixture identity import Signed-off-by: Apurv Kumaria --- test/helpers/onboard-script-mocks.cjs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/helpers/onboard-script-mocks.cjs b/test/helpers/onboard-script-mocks.cjs index 01e43439850..3dc048298cf 100644 --- a/test/helpers/onboard-script-mocks.cjs +++ b/test/helpers/onboard-script-mocks.cjs @@ -46,7 +46,9 @@ function registerSourceRequire() { // Vitest setup files and NODE_OPTIONS preloads both depend on this hook. registerSourceRequire(); -const { ONBOARD_READY_SANDBOX_ID } = require("./onboard-openshell-fixture.ts"); +function onboardReadySandboxId() { + return require("./onboard-openshell-fixture.ts").ONBOARD_READY_SANDBOX_ID; +} function normalizeCommand(command) { return (Array.isArray(command) ? command.join(" ") : String(command)).replace(/'/g, ""); @@ -350,7 +352,7 @@ function createCreatedSandboxFixture(options = {}) { ); const initialSandboxId = hasOwn(options, "sandboxId") ? options.sandboxId - : ONBOARD_READY_SANDBOX_ID; + : onboardReadySandboxId(); const initialLifecycleState = hasOwn(options, "lifecycleState") ? options.lifecycleState : "absent"; @@ -855,7 +857,7 @@ function managedSandboxPolicyReceiptFixture(entry, options = {}) { const gatewayName = options.gatewayName || "nemoclaw"; const gatewayPort = options.gatewayPort || 8080; const lifecycleGeneration = options.lifecycleGeneration || "123e4567-e89b-42d3-a456-426614174983"; - const sandboxId = options.sandboxId || ONBOARD_READY_SANDBOX_ID; + const sandboxId = options.sandboxId || onboardReadySandboxId(); const sandboxIdentityFingerprint = require("node:crypto") .createHash("sha256") .update(sandboxId) @@ -1149,7 +1151,7 @@ function mockManagedImageBootstrap() { path.resolve(__dirname, "../../src/lib/adapters/openshell/sandbox-identity.ts"), ); - sandboxIdentity.resolveOpenShellSandboxId = () => ONBOARD_READY_SANDBOX_ID; + sandboxIdentity.resolveOpenShellSandboxId = () => onboardReadySandboxId(); authorityStore.createDockerManagedBootstrapAuthorityStore = () => ({ async recordPreparedAuthority(authority) { return { From ddb456b0cf79dd0bb386ce02729560ac06ea56b6 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Thu, 27 Aug 2026 11:57:18 -0700 Subject: [PATCH 10/13] test(onboard): reject mismatched fixture gateways Signed-off-by: Apurv Kumaria --- .../onboard-created-sandbox-fixture.test.ts | 27 +++++++++++++++++-- test/helpers/onboard-script-mocks.cjs | 3 +++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/test/helpers/onboard-created-sandbox-fixture.test.ts b/test/helpers/onboard-created-sandbox-fixture.test.ts index b769c9119d6..b7bbf760423 100644 --- a/test/helpers/onboard-created-sandbox-fixture.test.ts +++ b/test/helpers/onboard-created-sandbox-fixture.test.ts @@ -53,8 +53,15 @@ function selectorListCommand(gatewayName: string | null, nonce = CREATE_ATTEMPT_ ]; } -function createCommand(nonce = CREATE_ATTEMPT_NONCE): string[] { - return ["openshell", "sandbox", "create", "--label", `${NEMOCLAW_CREATE_ATTEMPT_LABEL}=${nonce}`]; +function createCommand(nonce = CREATE_ATTEMPT_NONCE, gatewayName: string | null = null): string[] { + return [ + "openshell", + "sandbox", + "create", + ...(gatewayName === null ? [] : ["-g", gatewayName]), + "--label", + `${NEMOCLAW_CREATE_ATTEMPT_LABEL}=${nonce}`, + ]; } describe("created sandbox fixture", () => { @@ -167,6 +174,22 @@ describe("created sandbox fixture", () => { expect(fixture.capture(["openshell", "sandbox", "get", "alpha"])).toBeNull(); }); + it("rejects create and recreate commands for another gateway (#10463)", () => { + const fixture = createCreatedSandboxFixture({ gatewayName: "gateway-alpha" }); + + expect(() => fixture.create(createCommand(CREATE_ATTEMPT_NONCE, "gateway-bravo"))).toThrow( + "Created sandbox fixture requires its configured gateway.", + ); + expect(fixture.state.lifecycleState).toBe("absent"); + + fixture.create(createCommand()); + fixture.delete(); + expect(() => fixture.recreate(createCommand(CREATE_ATTEMPT_NONCE, "gateway-bravo"))).toThrow( + "Created sandbox fixture requires its configured gateway.", + ); + expect(fixture.state.lifecycleState).toBe("deleted"); + }); + it("does not answer a selector for another create attempt (#10463)", () => { const fixture = createCreatedSandboxFixture({ gatewayName: "gateway-alpha" }); fixture.create(createCommand()); diff --git a/test/helpers/onboard-script-mocks.cjs b/test/helpers/onboard-script-mocks.cjs index 3dc048298cf..36610663baf 100644 --- a/test/helpers/onboard-script-mocks.cjs +++ b/test/helpers/onboard-script-mocks.cjs @@ -418,6 +418,9 @@ function createCreatedSandboxFixture(options = {}) { if (!details || details.action !== "create") { throw new Error("Created sandbox fixture requires one sandbox create command."); } + if (details.gatewayName !== null && details.gatewayName !== state.gatewayName) { + throw new Error("Created sandbox fixture requires its configured gateway."); + } const prefix = `${sandboxIdentity.NEMOCLAW_CREATE_ATTEMPT_LABEL}=`; const labels = details.args.flatMap((arg, index) => { if (arg === "--label") return [details.args[index + 1] || ""]; From 4de259544f4b3e6baca7283a06cb7a112a2d5d96 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Fri, 28 Aug 2026 12:58:35 -0700 Subject: [PATCH 11/13] test(onboard): route runtime identity through fixture Signed-off-by: Apurv Kumaria --- test/helpers/managed-image-buildless-e2e.ts | 10 ++-- .../onboard-created-sandbox-fixture.test.ts | 31 +++++++++++ test/helpers/onboard-openshell-fixture.ts | 11 +--- test/helpers/onboard-script-mocks.cjs | 36 ++++++++++--- .../onboard-custom-dockerfile.test.ts | 3 +- ...oard-extra-provider-reconciliation.test.ts | 3 +- .../onboard-fresh-create-identity.test.ts | 52 ++++++++++++++++++- .../onboard-installer-restore-intent.test.ts | 6 ++- ...onboard-mcp-observability-redirect.test.ts | 16 ++++-- test/onboarding/onboard-messaging.test.ts | 36 ++++++------- .../onboard-prepared-build-context.test.ts | 3 +- test/onboarding/onboard-sandbox-build.test.ts | 15 ++++-- .../onboard-terminal-dashboard.test.ts | 3 +- test/onboarding/onboard.test.ts | 13 +++-- test/security/shellquote-sandbox.test.ts | 3 +- 15 files changed, 182 insertions(+), 59 deletions(-) diff --git a/test/helpers/managed-image-buildless-e2e.ts b/test/helpers/managed-image-buildless-e2e.ts index de33e67b86d..831d19792ba 100644 --- a/test/helpers/managed-image-buildless-e2e.ts +++ b/test/helpers/managed-image-buildless-e2e.ts @@ -75,6 +75,7 @@ interface ChildPayload { agent?: string | null; dashboardPort?: number | null; imageTag?: string | null; + lifecycleLiveIdentityFingerprint?: string | null; name?: string; workload?: { schemaVersion?: number; @@ -93,6 +94,7 @@ interface ChildPayload { }; }>; runnerCommands: string[]; + sandboxId: string; spawnCalls: SpawnCall[]; } @@ -197,6 +199,7 @@ const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxId: "fixture-managed-sandbox", lifecycleState: recreate ? "created" : "absent", }); +createdSandbox.installRuntimeObservation(); const coreVersion = require(${source("src/lib/core/version.ts")}); replace(coreVersion, "getVersion", () => catalogRelease); @@ -626,6 +629,7 @@ const { createSandbox } = require(${source("src/lib/onboard.ts")}); managedBootstrapCalls, registerCalls, runnerCommands, + sandboxId: createdSandbox.state.sandboxId, spawnCalls, })); })().catch((error) => { @@ -646,9 +650,6 @@ function writeRuntimeStubs(fakeBin: string, dockerLog: string): void { 'if [ "${1:-}" = "policy" ] && [ "${2:-}" = "list" ] && [[ " $* " = *" --global "* ]]; then', ' printf "%s\\n" "No global policy history found" >&2', "fi", - 'if [ "${1:-}" = "sandbox" ] && [ "${2:-}" = "get" ]; then', - ' printf "Sandbox:\\n\\n Id: fixture-managed-sandbox\\n Name: %s\\n Phase: Ready\\n" "${!#}"', - "fi", "exit 0", "", ].join("\n"), @@ -879,6 +880,9 @@ function assertManagedLaunch( )}`, ).toBeDefined(); expect(registration?.agent).toBe(agent); + expect(registration?.lifecycleLiveIdentityFingerprint).toBe( + createHash("sha256").update(result.payload.sandboxId).digest("hex"), + ); if (agent === "langchain-deepagents-code") { expect(registration?.dashboardPort).toBe(0); } diff --git a/test/helpers/onboard-created-sandbox-fixture.test.ts b/test/helpers/onboard-created-sandbox-fixture.test.ts index b7bbf760423..481b04bdbfb 100644 --- a/test/helpers/onboard-created-sandbox-fixture.test.ts +++ b/test/helpers/onboard-created-sandbox-fixture.test.ts @@ -16,6 +16,7 @@ type CreatedSandboxFixture = { readonly capture: (command: string[]) => string | null; readonly create: (command: string[]) => void; readonly delete: () => void; + readonly installRuntimeObservation: () => () => void; readonly recreate: (command: string[]) => void; readonly setPhase: (phase: string) => void; readonly run: (command: string[]) => { status: number; stdout: Buffer; stderr: Buffer } | null; @@ -137,6 +138,36 @@ describe("created sandbox fixture", () => { ).toBe(replacementSandboxId); }); + it("routes direct runtime observations through the fixture lifecycle (#10463)", () => { + const fixture = createCreatedSandboxFixture({ + sandboxName: "alpha", + sandboxId: "sandbox-alpha", + gatewayName: "gateway-alpha", + }); + const openshellRuntime = requireCjs("../../src/lib/adapters/openshell/runtime.ts") as { + captureResolvedOpenshell: (args: string[]) => { + status: number; + stdout: string; + }; + }; + const restore = fixture.installRuntimeObservation(); + const getSandbox = () => + openshellRuntime.captureResolvedOpenshell(["sandbox", "get", "-g", "gateway-alpha", "alpha"]); + + try { + fixture.create(createCommand()); + expect(parseOpenShellSandboxId(getSandbox().stdout)).toBe(fixture.state.sandboxId); + + fixture.delete(); + expect(getSandbox().status).toBe(1); + + fixture.recreate(createCommand("b".repeat(NEMOCLAW_CREATE_ATTEMPT_NONCE_HEX_LENGTH))); + expect(parseOpenShellSandboxId(getSandbox().stdout)).toBe(fixture.state.sandboxId); + } finally { + restore(); + } + }); + it("keeps a replacement ID valid for a maximum-length input (#10463)", () => { const maximumSandboxId = "a".repeat(512); const fixture = createCreatedSandboxFixture({ sandboxId: maximumSandboxId }); diff --git a/test/helpers/onboard-openshell-fixture.ts b/test/helpers/onboard-openshell-fixture.ts index a154eab3679..e74e0297dc8 100644 --- a/test/helpers/onboard-openshell-fixture.ts +++ b/test/helpers/onboard-openshell-fixture.ts @@ -8,21 +8,14 @@ function writeExecutable(target: string, contents: string): void { fs.writeFileSync(target, contents, { mode: 0o755 }); } -export const ONBOARD_READY_SANDBOX_ID = "sbx-4f2a91c0d7"; - export function writeOkOpenshell( fakeBin: string, - options: { gatewayPort?: number; readySandboxGet?: boolean } = {}, + options: { gatewayPort?: number } = {}, ): void { const gatewayPort = options.gatewayPort ?? 8080; - const sandboxGet = options.readySandboxGet - ? 'if [ "${1:-}" = sandbox ] && [ "${2:-}" = get ]; then printf "Sandbox:\\n\\n Id: ' + - ONBOARD_READY_SANDBOX_ID + - '\\n Name: %s\\n Phase: Ready\\n" "${!#}"; fi\n' - : ""; writeExecutable( path.join(fakeBin, "openshell"), - `#!/usr/bin/env bash\n${sandboxGet}if [ "\${1:-}" = policy ] && [ "\${2:-}" = list ] && [[ " $* " = *" --global "* ]]; then printf '%s\\n' 'No global policy history found' >&2; fi\nif [ "\${1:-}" = policy ] && [ "\${2:-}" = get ] && [[ " $* " = *" --output json "* ]]; then printf '{"scope":"sandbox","sandbox":"%s","status":"effective","policy_source":"sandbox","hash":"fixture-policy","active_version":1,"policy":{}}\\n' "\${!#}"; fi\nif [ "\${1:-}" = policy ] && [ "\${2:-}" = get ] && [[ " $* " = *" --base "* ]]; then printf 'version: 1\\n'; fi\nif [ "\${1:-}" = gateway ] && [ "\${2:-}" = info ]; then printf 'Gateway endpoint: http://127.0.0.1:${gatewayPort}\\n'; fi\nif [ "\${1:-}" = sandbox ] && [ "\${2:-}" = ssh-config ]; then printf "Host openshell-%s.default\\n HostName 127.0.0.1\\n User sandbox\\n" "\${3:-sandbox}"; fi\nexit 0\n`, + `#!/usr/bin/env bash\nif [ "\${1:-}" = policy ] && [ "\${2:-}" = list ] && [[ " $* " = *" --global "* ]]; then printf '%s\\n' 'No global policy history found' >&2; fi\nif [ "\${1:-}" = policy ] && [ "\${2:-}" = get ] && [[ " $* " = *" --output json "* ]]; then printf '{"scope":"sandbox","sandbox":"%s","status":"effective","policy_source":"sandbox","hash":"fixture-policy","active_version":1,"policy":{}}\\n' "\${!#}"; fi\nif [ "\${1:-}" = policy ] && [ "\${2:-}" = get ] && [[ " $* " = *" --base "* ]]; then printf 'version: 1\\n'; fi\nif [ "\${1:-}" = gateway ] && [ "\${2:-}" = info ]; then printf 'Gateway endpoint: http://127.0.0.1:${gatewayPort}\\n'; fi\nif [ "\${1:-}" = sandbox ] && [ "\${2:-}" = ssh-config ]; then printf "Host openshell-%s.default\\n HostName 127.0.0.1\\n User sandbox\\n" "\${3:-sandbox}"; fi\nexit 0\n`, ); writeExecutable( path.join(fakeBin, "ssh"), diff --git a/test/helpers/onboard-script-mocks.cjs b/test/helpers/onboard-script-mocks.cjs index f4cb7cdddc4..bdbbf60a013 100644 --- a/test/helpers/onboard-script-mocks.cjs +++ b/test/helpers/onboard-script-mocks.cjs @@ -88,9 +88,7 @@ Module._resolveFilename = function resolveLazySourceFilename(request, parent, is }; Module._extensions[".ts"] = lazySourceRequire; -function onboardReadySandboxId() { - return require("./onboard-openshell-fixture.ts").ONBOARD_READY_SANDBOX_ID; -} +const ONBOARD_READY_SANDBOX_ID = "sbx-4f2a91c0d7"; function normalizeCommand(command) { return (Array.isArray(command) ? command.join(" ") : String(command)).replace(/'/g, ""); @@ -394,7 +392,7 @@ function createCreatedSandboxFixture(options = {}) { ); const initialSandboxId = hasOwn(options, "sandboxId") ? options.sandboxId - : onboardReadySandboxId(); + : ONBOARD_READY_SANDBOX_ID; const initialLifecycleState = hasOwn(options, "lifecycleState") ? options.lifecycleState : "absent"; @@ -603,11 +601,37 @@ function createCreatedSandboxFixture(options = {}) { assertState(); }; + const installRuntimeObservation = () => { + const openshellRuntime = require( + path.resolve(__dirname, "../../src/lib/adapters/openshell/runtime.ts"), + ); + const previousCapture = openshellRuntime.captureResolvedOpenshell; + const fixtureCapture = (args, options = {}) => { + const result = run(["openshell", ...args]); + if (result === null) return previousCapture(args, options); + const stdout = result.stdout.toString(); + const stderr = result.stderr.toString(); + return { + status: result.status, + output: options.includeStderr ? `${stdout}${stderr}` : stdout, + stdout, + stderr, + }; + }; + openshellRuntime.captureResolvedOpenshell = fixtureCapture; + return () => { + if (openshellRuntime.captureResolvedOpenshell === fixtureCapture) { + openshellRuntime.captureResolvedOpenshell = previousCapture; + } + }; + }; + assertState(); return Object.freeze({ capture, create, delete: deleteSandbox, + installRuntimeObservation, recreate, run, setPhase, @@ -902,7 +926,7 @@ function managedSandboxPolicyReceiptFixture(entry, options = {}) { const gatewayName = options.gatewayName || "nemoclaw"; const gatewayPort = options.gatewayPort || 8080; const lifecycleGeneration = options.lifecycleGeneration || "123e4567-e89b-42d3-a456-426614174983"; - const sandboxId = options.sandboxId || onboardReadySandboxId(); + const sandboxId = options.sandboxId || ONBOARD_READY_SANDBOX_ID; const sandboxIdentityFingerprint = require("node:crypto") .createHash("sha256") .update(sandboxId) @@ -1196,7 +1220,7 @@ function mockManagedImageBootstrap() { path.resolve(__dirname, "../../src/lib/adapters/openshell/sandbox-identity.ts"), ); - sandboxIdentity.resolveOpenShellSandboxId = () => onboardReadySandboxId(); + sandboxIdentity.resolveOpenShellSandboxId = () => ONBOARD_READY_SANDBOX_ID; authorityStore.createDockerManagedBootstrapAuthorityStore = () => ({ async recordPreparedAuthority(authority) { return { diff --git a/test/onboarding/onboard-custom-dockerfile.test.ts b/test/onboarding/onboard-custom-dockerfile.test.ts index eb91b6b6665..5d359ab0c33 100644 --- a/test/onboarding/onboard-custom-dockerfile.test.ts +++ b/test/onboarding/onboard-custom-dockerfile.test.ts @@ -185,7 +185,7 @@ describe("onboard custom Dockerfile", () => { fs.writeFileSync(path.join(customBuildDir, "credentials.json"), "{}"); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const customDockerfilePath = JSON.stringify(path.join(customBuildDir, "Dockerfile")); @@ -195,6 +195,7 @@ const fixtureMocks = require(${onboardScriptMocksPath}); const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName: "my-assistant", }); +createdSandbox.installRuntimeObservation(); const _n = (c) => (Array.isArray(c) ? c.join(" ") : String(c)).replace(/'/g, ""); const registry = require(${registryPath}); const preflight = require(${preflightPath}); diff --git a/test/onboarding/onboard-extra-provider-reconciliation.test.ts b/test/onboarding/onboard-extra-provider-reconciliation.test.ts index f9a0f123334..d83d14724b1 100644 --- a/test/onboarding/onboard-extra-provider-reconciliation.test.ts +++ b/test/onboarding/onboard-extra-provider-reconciliation.test.ts @@ -43,7 +43,7 @@ describe("onboard extra-provider reconciliation", () => { ); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const registry = require(${registryPath}); @@ -143,6 +143,7 @@ const createReservedSandbox = () => { createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName: "my-assistant", }); + createdSandbox.installRuntimeObservation(); return createSandbox( ...fixtureMocks.sandboxCreateArgsWithVerifiedReservation( [null, "gpt-5.4", "nvidia-prod", null, null, null, null, null, null, null, null, null, []], diff --git a/test/onboarding/onboard-fresh-create-identity.test.ts b/test/onboarding/onboard-fresh-create-identity.test.ts index 526abaec096..978f0e1c5b4 100644 --- a/test/onboarding/onboard-fresh-create-identity.test.ts +++ b/test/onboarding/onboard-fresh-create-identity.test.ts @@ -62,6 +62,14 @@ describe("fresh create identity", () => { agent: null, expectedOutcome: "providerless-apf" as const, }, + { + title: "rejects mismatched selector and get identities before later effects (#10463)", + apfInterceptorRequested: true, + provider: null, + model: null, + agent: null, + expectedOutcome: "identity-mismatch-refusal" as const, + }, { title: "surfaces retained sandbox recovery through the public error message (#9833)", apfInterceptorRequested: true, @@ -212,11 +220,14 @@ const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxId: "sbx-fresh-create", gatewayName: "nemoclaw-18080", }); +const mismatchedSandboxId = createdSandbox.state.sandboxId + "-mismatch"; let sandboxListCalls = 0; let dockerPsCalls = 0; let registeredSandbox = null; let effectivePolicy = {}; let credentialReadCalls = 0; +let identityMismatchGetCalls = 0; +let policyVerificationCalls = 0; let routeReservationCalls = 0; const keepAlive = setInterval(() => {}, 1000); const apfInterceptorRequested = ${JSON.stringify(apfInterceptorRequested)}; @@ -231,6 +242,9 @@ const cancellationSelector = ${JSON.stringify( )}; const cancelAfterCreate = cancellationSelector !== null; const recoveryReentry = process.env.NEMOCLAW_RECOVERY_REENTRY || ""; +const identityMismatchRefusal = ${JSON.stringify( + expectedOutcome === "identity-mismatch-refusal", + )}; const stagedMessagingRefusal = ${JSON.stringify(expectedOutcome === "staged-messaging-refusal")}; const postCreateAuthorityRefusal = ${JSON.stringify( expectedOutcome === "post-create-authority-refusal", @@ -293,7 +307,17 @@ runner.run = (command, opts = {}) => { createdSandbox.setPhase(sandboxListCalls >= 2 ? "Ready" : "Pending"); } const sandboxCapture = createdSandbox.capture(command); - if (sandboxCapture !== null) return sandboxCapture; + if (sandboxCapture !== null) { + if ( + identityMismatchRefusal && + cmd.includes("sandbox get") && + sandboxCapture.includes("Id: " + createdSandbox.state.sandboxId) + ) { + identityMismatchGetCalls += 1; + return sandboxCapture.replace(createdSandbox.state.sandboxId, mismatchedSandboxId); + } + return sandboxCapture; + } if (cmd.startsWith("docker ps -a --no-trunc ")) { dockerPsCalls += 1; if (dockerPsCalls === 1) return "a".repeat(64); @@ -371,6 +395,7 @@ runner.run = (command, opts = {}) => { apfInterceptorRequested, getSandbox: (name) => retainedRegistryEntry ?? durableGetSandbox(name), onVerifyCreatedPolicy: (input) => { + policyVerificationCalls += 1; if (postCreateAuthorityRefusal) { throw new Error("external policy authority changed"); } @@ -503,6 +528,9 @@ const writePayload = (sandboxName, creationError, exitCode = 0) => { lifecycleObservationCommands, registeredSandbox, credentialReadCalls, + identityMismatchGetCalls, + mismatchedSandboxId, + policyVerificationCalls, routeReservationCalls, checkpointReadCalls, registryMutationCalls, @@ -791,6 +819,27 @@ if (${JSON.stringify( assert.equal(payload.credentialReadCalls, 0); assert.deepEqual(providerExposureCommands, []); }; + const assertIdentityMismatchRefusal = () => { + assert.equal(payload.sandboxName, null); + assert.equal(payload.sandboxCreated, true); + assert.equal(payload.deleted, false); + assert.match(payload.creationError, /automatic sandbox cleanup was not safe/u); + assert.notEqual(payload.mismatchedSandboxId, payload.sandboxId); + assert.ok(payload.identityMismatchGetCalls >= 1); + assert.equal(payload.policyVerificationCalls, 0); + assert.equal(payload.registeredSandbox, null); + assert.equal(payload.credentialReadCalls, 0); + assert.deepEqual(payload.registryMutationCalls, [ + { operation: "update", name: "my-assistant" }, + ]); + assert.deepEqual(providerEffectCommands, []); + assert.equal( + payload.commandNames.some((command: string) => + /(?:^|\s)policy (?:set|apply)(?:\s|$)/u.test(command), + ), + false, + ); + }; const assertPostCreateAuthorityRefusal = () => { assert.equal(payload.sandboxName, null); assert.equal(payload.sandboxCreated, true); @@ -1070,6 +1119,7 @@ if (${JSON.stringify( "unsupported-agent-refusal": assertUnsupportedAgentRefusal, "resolved-agent-refusal": assertUnsupportedAgentRefusal, "providerless-apf": assertProviderlessApfCreation, + "identity-mismatch-refusal": assertIdentityMismatchRefusal, "post-create-authority-refusal": assertPostCreateAuthorityRefusal, "post-create-runner-refusal": assertPostCreateRunnerRefusal, "post-create-registration-refusal": assertPostCreateRegistrationRefusal, diff --git a/test/onboarding/onboard-installer-restore-intent.test.ts b/test/onboarding/onboard-installer-restore-intent.test.ts index dd669410cb8..9130f0e15ab 100644 --- a/test/onboarding/onboard-installer-restore-intent.test.ts +++ b/test/onboarding/onboard-installer-restore-intent.test.ts @@ -51,7 +51,7 @@ describe("createSandbox installer restore intent", () => { ); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -70,6 +70,7 @@ const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ lifecycleState: "created", phase: "NotReady", }); +createdSandbox.installRuntimeObservation(); runner.run = (command) => { const cmd = _n(command); events.push({ kind: "run", cmd }); @@ -419,7 +420,7 @@ const { createSandbox } = require(${onboardPath}); ); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -433,6 +434,7 @@ const existingSandbox = fixtureMocks.createCreatedSandboxFixture({ lifecycleState: "created", phase: "NotReady", }); +existingSandbox.installRuntimeObservation(); runner.run = (command) => { if (_n(command).includes("sandbox delete")) { diff --git a/test/onboarding/onboard-mcp-observability-redirect.test.ts b/test/onboarding/onboard-mcp-observability-redirect.test.ts index a29c06354ac..a2ccd5a391d 100644 --- a/test/onboarding/onboard-mcp-observability-redirect.test.ts +++ b/test/onboarding/onboard-mcp-observability-redirect.test.ts @@ -17,7 +17,7 @@ describe("onboard managed MCP recreation redirect", () => { const fakeBin = path.join(tmpDir, "bin"); const scriptPath = path.join(tmpDir, "redirect.js"); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const onboardPath = JSON.stringify(path.join(repoRoot, "src", "lib", "onboard.ts")); const runnerPath = JSON.stringify(path.join(repoRoot, "src", "lib", "runner.ts")); @@ -33,11 +33,17 @@ const runner = require(${runnerPath}); const registry = require(${registryPath}); const fixtureMocks = require(${mocksPath}); const normalize = (command) => (Array.isArray(command) ? command.join(" ") : String(command)).replace(/'/g, ""); -runner.run = () => ({ status: 0 }); +const existingSandbox = fixtureMocks.createCreatedSandboxFixture({ + sandboxName: "alpha", + lifecycleState: "created", +}); +existingSandbox.installRuntimeObservation(); +const sandboxCommand = (command) => Array.isArray(command) ? command : normalize(command).split(/\s+/u); +runner.run = (command) => existingSandbox.run(sandboxCommand(command)) ?? { status: 0 }; runner.runCapture = (command) => { const value = normalize(command); - if (value.includes("sandbox get --gateway nemoclaw alpha")) return "alpha"; - if (value.includes("sandbox list")) return "alpha Ready"; + const sandboxResult = existingSandbox.run(sandboxCommand(command)); + if (sandboxResult !== null) return sandboxResult.status === 0 ? sandboxResult.stdout.toString() : ""; if (value.includes("/usr/local/bin/dcode identity")) { return "Route: inference\nProvider: provider\nModel: openai:model\nEndpoint: https://inference.local/v1"; } @@ -66,7 +72,7 @@ registry.getSandbox = () => fixtureMocks.managedSandboxPolicyReceiptFixture({ } } } -}); +}, { sandboxId: existingSandbox.state.sandboxId }); registry.getDefault = () => null; const { createSandbox } = require(${onboardPath}); createSandbox( diff --git a/test/onboarding/onboard-messaging.test.ts b/test/onboarding/onboard-messaging.test.ts index 3333334558c..84c376c5693 100644 --- a/test/onboarding/onboard-messaging.test.ts +++ b/test/onboarding/onboard-messaging.test.ts @@ -68,7 +68,7 @@ describe("onboard messaging", () => { ); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -81,7 +81,7 @@ const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); const fs = require("node:fs"); const commands = []; -const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName: "my-assistant" }); +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName: "my-assistant" }); createdSandbox.installRuntimeObservation(); runner.run = fixtureMocks.createStatefulMessagingProviderRunner({ commands, createdSandbox }); runner.runCapture = (command) => { const sandboxCapture = createdSandbox.capture(command); @@ -332,7 +332,7 @@ const { createSandbox, setupMessagingChannels } = require(${onboardPath}); customDockerfilePath, "FROM scratch\nARG NEMOCLAW_MESSAGING_PLAN_B64=\nARG NEMOCLAW_TOOL_DISCLOSURE=progressive\nENV NEMOCLAW_TOOL_DISCLOSURE=${NEMOCLAW_TOOL_DISCLOSURE}\n", ); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -361,7 +361,7 @@ const nonSlackMessagingEnvKeys = [ const commands = []; let registeredSandbox = null; -const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName: "my-assistant" }); +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName: "my-assistant" }); createdSandbox.installRuntimeObservation(); runner.run = fixtureMocks.createStatefulMessagingProviderRunner({ commands, createdSandbox }); runner.runCapture = (command) => { const sandboxCapture = createdSandbox.capture(command); @@ -515,14 +515,14 @@ const { createSandbox } = require(${onboardPath}); const expectedProviders = Object.keys(providerCredentialKeys).sort(); const rawGatewayCredential = "gateway-only-provider-secret"; fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}), registry = require(${registryPath}), preflight = require(${preflightPath}), credentials = require(${credentialsPath}); const fixtureMocks = require(${onboardScriptMocksPath}); const _n = (c) => (Array.isArray(c) ? c.join(" ") : String(c)).replace(/'/g, ""); const childProcess = require("node:child_process"), { EventEmitter } = require("node:events"); const commands = [], credentialKeys = ${JSON.stringify(providerCredentialKeys)}; let registered = null; -const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName: "my-assistant" }); +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName: "my-assistant" }); createdSandbox.installRuntimeObservation(); const providers = Object.keys(credentialKeys), revisions = new Map(providers.map((name) => [name, 1])), providerGetCounts = new Map(); const rawGatewayCredential = ${JSON.stringify(rawGatewayCredential)}, gatewaySecrets = new Map(providers.map((name) => [name, rawGatewayCredential])); registry.registerSandbox({ name: "my-assistant", messaging: { schemaVersion: 1, plan: ${messagingPlanLiteral(["slack", "telegram", "whatsapp"])} } }); @@ -686,7 +686,7 @@ const { createSandbox } = require(${onboardPath}); const messagingPlanB64 = encodeMessagingPlanForChannels(["telegram"], ["telegram"]); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -703,7 +703,7 @@ const commands = []; let dockerfileContent; const registerCalls = []; const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName: "my-assistant", -}); +}); createdSandbox.installRuntimeObservation(); registry.registerSandbox({ name: "my-assistant", messaging: { schemaVersion: 1, plan: ${messagingPlanLiteral(["telegram"], ["telegram"])} }, @@ -856,7 +856,7 @@ const { createSandbox } = require(${onboardPath}); const messagingPlanB64 = encodeMessagingPlanForChannels(["whatsapp"]); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -871,7 +871,7 @@ const fs = require("node:fs"); const commands = []; let dockerfileContent; const registerCalls = []; -const createdSandbox = fixtureMocks.createCreatedSandboxFixture(); +const createdSandbox = fixtureMocks.createCreatedSandboxFixture(); createdSandbox.installRuntimeObservation(); runner.run = (command, opts = {}) => { const normalized = _n(command); commands.push({ command: normalized, env: opts.env || null }); @@ -1021,7 +1021,7 @@ const { createSandbox } = require(${onboardPath}); const messagingPlanB64 = encodeMessagingPlanForChannels(["whatsapp"], ["whatsapp"]); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -1041,7 +1041,7 @@ registry.registerSandbox({ const commands = []; let dockerfileContent; const registerCalls = []; -const createdSandbox = fixtureMocks.createCreatedSandboxFixture(); +const createdSandbox = fixtureMocks.createCreatedSandboxFixture(); createdSandbox.installRuntimeObservation(); runner.run = (command, opts = {}) => { const normalized = _n(command); commands.push({ command: normalized, env: opts.env || null }); @@ -1259,7 +1259,7 @@ const { createSandbox } = require(${onboardPath}); ); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -1268,7 +1268,7 @@ const registry = require(${registryPath}); const fixtureMocks = require(${onboardScriptMocksPath}); const commands = []; -const existingSandbox = fixtureMocks.createCreatedSandboxFixture({ lifecycleState: "created" }); +const existingSandbox = fixtureMocks.createCreatedSandboxFixture({ lifecycleState: "created" }); existingSandbox.installRuntimeObservation(); const messagingProviderRunner = require(${onboardScriptMocksPath}).createStatefulMessagingProviderRunner({ commands, createdSandbox: existingSandbox, @@ -1367,7 +1367,7 @@ const { createSandbox } = require(${onboardPath}); ); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -1380,7 +1380,7 @@ const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); const commands = []; -const createdSandbox = fixtureMocks.createCreatedSandboxFixture(); +const createdSandbox = fixtureMocks.createCreatedSandboxFixture(); createdSandbox.installRuntimeObservation(); runner.run = require(${onboardScriptMocksPath}).createStatefulMessagingProviderRunner({ commands, createdSandbox, @@ -1508,7 +1508,7 @@ const { createSandbox } = require(${onboardPath}); ); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -1521,7 +1521,7 @@ const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); const commands = []; -const createdSandbox = fixtureMocks.createCreatedSandboxFixture(); +const createdSandbox = fixtureMocks.createCreatedSandboxFixture(); createdSandbox.installRuntimeObservation(); runner.run = (command, opts = {}) => { commands.push({ command: _n(command), env: opts.env || null }); return createdSandbox.run(command) ?? { status: 0 }; diff --git a/test/onboarding/onboard-prepared-build-context.test.ts b/test/onboarding/onboard-prepared-build-context.test.ts index 33d259a9acb..37622f2f3ce 100644 --- a/test/onboarding/onboard-prepared-build-context.test.ts +++ b/test/onboarding/onboard-prepared-build-context.test.ts @@ -36,7 +36,7 @@ function runPreparedContextScenario(scenario: PreparedContextScenario): Prepared fs.mkdirSync(fakeBin, { recursive: true }); fs.mkdirSync(preparedBuildCtx, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); fs.writeFileSync( path.join(preparedBuildCtx, "Dockerfile"), ["FROM scratch", `ARG NEMOCLAW_BUILD_ID=${buildId}`, 'CMD ["/bin/true"]', ""].join("\n"), @@ -83,6 +83,7 @@ const buildCtx = ${JSON.stringify(preparedBuildCtx)}; const buildId = ${JSON.stringify(buildId)}; const sandboxName = "prepared-dcode"; const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName }); +createdSandbox.installRuntimeObservation(); const commands = []; const registerCalls = []; const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, { diff --git a/test/onboarding/onboard-sandbox-build.test.ts b/test/onboarding/onboard-sandbox-build.test.ts index e3dfc5e3062..3f761646a2e 100644 --- a/test/onboarding/onboard-sandbox-build.test.ts +++ b/test/onboarding/onboard-sandbox-build.test.ts @@ -45,7 +45,7 @@ describe("onboard helpers", () => { ); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -53,6 +53,7 @@ const fixtureMocks = require(${onboardScriptMocksPath}); const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName: "my-assistant", }); +createdSandbox.installRuntimeObservation(); const _n = (c) => (Array.isArray(c) ? c.join(" ") : String(c)).replace(/'/g, ""); const registry = require(${registryPath}); const preflight = require(${preflightPath}); @@ -216,7 +217,7 @@ const { createSandbox } = require(${onboardPath}); ); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const fs = require("node:fs"); @@ -227,6 +228,7 @@ const fixtureMocks = require(${onboardScriptMocksPath}); const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName: "hermes-sandbox", }); +createdSandbox.installRuntimeObservation(); const _n = (c) => (Array.isArray(c) ? c.join(" ") : String(c)).replace(/'/g, ""); const registry = require(${registryPath}); const preflight = require(${preflightPath}); @@ -442,7 +444,7 @@ const { createSandbox } = require(${onboardPath}); const platformPath = JSON.stringify(path.join(repoRoot, "src", "lib", "platform.ts")); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const fs = require("node:fs"); @@ -464,6 +466,7 @@ platform.isWsl = () => false; const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName: "my-assistant", }); +createdSandbox.installRuntimeObservation(); const commands = []; const logs = []; @@ -609,7 +612,7 @@ const { createSandbox } = require(${onboardPath}); ); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -625,6 +628,7 @@ const commands = []; const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName: "my-assistant", }); +createdSandbox.installRuntimeObservation(); runner.run = (command, opts = {}) => { const normalized = _n(command); commands.push({ command: normalized, env: opts.env || null }); @@ -721,7 +725,7 @@ const { createSandbox } = require(${onboardPath}); ); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -737,6 +741,7 @@ const commands = []; const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName: "my-assistant", }); +createdSandbox.installRuntimeObservation(); runner.run = (command, opts = {}) => { const normalized = _n(command); commands.push({ command: normalized, env: opts.env || null }); diff --git a/test/onboarding/onboard-terminal-dashboard.test.ts b/test/onboarding/onboard-terminal-dashboard.test.ts index 44618a156f6..855c9f907ca 100644 --- a/test/onboarding/onboard-terminal-dashboard.test.ts +++ b/test/onboarding/onboard-terminal-dashboard.test.ts @@ -50,7 +50,7 @@ function runTerminalDashboardScenario(scenario: "create" | "reuse") { ); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const fs = require("node:fs"); @@ -69,6 +69,7 @@ const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName, lifecycleState: scenario === "reuse" ? "created" : "absent", }); +createdSandbox.installRuntimeObservation(); const commands = []; const registerCalls = []; const updateCalls = []; diff --git a/test/onboarding/onboard.test.ts b/test/onboarding/onboard.test.ts index ce0a00afc49..1e088eeb578 100644 --- a/test/onboarding/onboard.test.ts +++ b/test/onboarding/onboard.test.ts @@ -678,7 +678,7 @@ startGateway(null).catch((error) => { ); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -689,22 +689,25 @@ startGateway(null).catch((error) => { const { EventEmitter } = require("node:events"); const commands = []; +const existingSandbox = fixtureMocks.createCreatedSandboxFixture({ lifecycleState: "created" }); +existingSandbox.installRuntimeObservation(); +const sandboxCommand = (command) => Array.isArray(command) ? command : _n(command).split(/\s+/u); runner.run = (command, opts = {}) => { commands.push({ command: _n(command), env: opts.env || null }); const profileResult = fixtureMocks.mockEndpointlessProviderProfileRun(command, "nemoclaw-mcp-v1", false); if (profileResult !== null) return profileResult; - return { status: 0 }; + return existingSandbox.run(sandboxCommand(command)) ?? { status: 0 }; }; runner.runCapture = (command) => { - if (_n(command).includes("sandbox get") && _n(command).includes("my-assistant")) return ["my-assistant", "Id: sbx-4f2a91c0d7"].join(String.fromCharCode(10)); - if (_n(command).includes("sandbox list")) return "my-assistant Ready"; + const sandboxResult = existingSandbox.run(sandboxCommand(command)); + if (sandboxResult !== null) return sandboxResult.status === 0 ? sandboxResult.stdout.toString() : ""; if (_n(command).includes("forward list")) return "my-assistant 127.0.0.1 18789 12345 running"; return ""; }; registry.getSandbox = () => fixtureMocks.managedSandboxPolicyReceiptFixture({ name: "my-assistant", toolDisclosure: "progressive", - }); + }, { sandboxId: existingSandbox.state.sandboxId }); childProcess.spawn = (...args) => { const child = new EventEmitter(); diff --git a/test/security/shellquote-sandbox.test.ts b/test/security/shellquote-sandbox.test.ts index 5e960b4e055..af27470fc5d 100644 --- a/test/security/shellquote-sandbox.test.ts +++ b/test/security/shellquote-sandbox.test.ts @@ -71,7 +71,7 @@ describe("sandboxName command hardening in onboard.js", () => { ); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); fs.writeFileSync( scriptPath, String.raw` @@ -90,6 +90,7 @@ process.env.NEMOCLAW_OPENSHELL_BIN = ${JSON.stringify(path.join(fakeBin, "opensh const commands = []; const asText = (command) => Array.isArray(command) ? command.join(" ") : String(command); const createdSandbox = fixtureMocks.createCreatedSandboxFixture(); +createdSandbox.installRuntimeObservation(); runner.run = (command, opts = {}) => { const text = asText(command); commands.push({ type: "run", command: text, env: opts.env || null }); From 09cd396fbff70d9f2d8d5af67f81337e2891b54e Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Fri, 28 Aug 2026 13:38:18 -0700 Subject: [PATCH 12/13] test(onboard): complete fixture watch selection Signed-off-by: Apurv Kumaria --- test/helpers/vitest-watch-triggers.ts | 2 ++ test/repository/vitest-watch-triggers.test.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/test/helpers/vitest-watch-triggers.ts b/test/helpers/vitest-watch-triggers.ts index 8302a6801b5..b43db71816f 100644 --- a/test/helpers/vitest-watch-triggers.ts +++ b/test/helpers/vitest-watch-triggers.ts @@ -59,12 +59,14 @@ export const vitestWatchTriggerPatterns: VitestWatchTriggerPattern[] = [ "test/onboarding/onboard-fresh-create-identity.test.ts", "test/onboarding/onboard-installer-restore-intent.test.ts", "test/onboarding/onboard-managed-image-buildless-e2e.test.ts", + "test/onboarding/onboard-mcp-observability-redirect.test.ts", "test/onboarding/onboard-messaging.test.ts", "test/onboarding/onboard-prepared-build-context.test.ts", "test/onboarding/onboard-reservation-recreate.test.ts", "test/onboarding/onboard-sandbox-build.test.ts", "test/onboarding/onboard-sandbox-recreation.test.ts", "test/onboarding/onboard-terminal-dashboard.test.ts", + "test/onboarding/onboard.test.ts", "test/security/shellquote-sandbox.test.ts", "test/repository/source-require-loader.test.ts", ), diff --git a/test/repository/vitest-watch-triggers.test.ts b/test/repository/vitest-watch-triggers.test.ts index e5074786f37..ef95152caee 100644 --- a/test/repository/vitest-watch-triggers.test.ts +++ b/test/repository/vitest-watch-triggers.test.ts @@ -124,12 +124,14 @@ describe("Vitest opaque-input watch triggers", () => { "test/onboarding/onboard-fresh-create-identity.test.ts", "test/onboarding/onboard-installer-restore-intent.test.ts", "test/onboarding/onboard-managed-image-buildless-e2e.test.ts", + "test/onboarding/onboard-mcp-observability-redirect.test.ts", "test/onboarding/onboard-messaging.test.ts", "test/onboarding/onboard-prepared-build-context.test.ts", "test/onboarding/onboard-reservation-recreate.test.ts", "test/onboarding/onboard-sandbox-build.test.ts", "test/onboarding/onboard-sandbox-recreation.test.ts", "test/onboarding/onboard-terminal-dashboard.test.ts", + "test/onboarding/onboard.test.ts", "test/security/shellquote-sandbox.test.ts", "test/repository/source-require-loader.test.ts", ]); From d6a3a76bc9c181041ec3b7aea85a1f19cb329ff8 Mon Sep 17 00:00:00 2001 From: Apurv Kumaria Date: Fri, 28 Aug 2026 14:45:34 -0700 Subject: [PATCH 13/13] test(onboard): finish fixture identity cutover Signed-off-by: Apurv Kumaria --- test/helpers/onboard-script-mocks.cjs | 4 - .../onboard-script-mocks-contract.test.ts | 96 ------------------- 2 files changed, 100 deletions(-) diff --git a/test/helpers/onboard-script-mocks.cjs b/test/helpers/onboard-script-mocks.cjs index 60d212ed1a1..3dbbf88023b 100644 --- a/test/helpers/onboard-script-mocks.cjs +++ b/test/helpers/onboard-script-mocks.cjs @@ -1216,11 +1216,7 @@ function mockManagedImageBootstrap() { const authorityStore = require( path.resolve(__dirname, "../../src/lib/onboard/managed-bootstrap/docker-authority-store.ts"), ); - const sandboxIdentity = require( - path.resolve(__dirname, "../../src/lib/adapters/openshell/sandbox-identity.ts"), - ); - sandboxIdentity.resolveOpenShellSandboxId = () => ONBOARD_READY_SANDBOX_ID; authorityStore.createDockerManagedBootstrapAuthorityStore = () => ({ async recordPreparedAuthority(authority) { return { diff --git a/test/onboarding/onboard-script-mocks-contract.test.ts b/test/onboarding/onboard-script-mocks-contract.test.ts index 6a31971e200..5bec1f46d9d 100644 --- a/test/onboarding/onboard-script-mocks-contract.test.ts +++ b/test/onboarding/onboard-script-mocks-contract.test.ts @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { createHash } from "node:crypto"; import { createRequire } from "node:module"; import { describe, expect, it } from "vitest"; @@ -17,19 +16,6 @@ type Runner = { }; type OnboardScriptMocks = { - createCreatedSandboxFixture: (options?: { - gatewayName?: string; - sandboxName?: string; - }) => { - capture: (command: readonly string[]) => string | null; - create: (command: readonly string[]) => void; - run: (command: readonly string[]) => CommandResult | null; - readonly state: { sandboxId: string }; - }; - managedSandboxPolicyReceiptFixture: ( - entry: { name: string }, - options?: { sandboxId?: string }, - ) => { lifecycleLiveIdentityFingerprint: string }; mockDockerSandboxLifecycleReleaseFromRunner: () => void; }; @@ -38,88 +24,6 @@ const fixtureMocks = requireForTest("../helpers/onboard-script-mocks.cjs") as On const runner = requireForTest("../../src/lib/runner.ts") as Runner; describe("shared onboarding process fixture contracts", () => { - it("uses one stateful identity for creation, list, and get observations (#10463)", () => { - const fixture = fixtureMocks.createCreatedSandboxFixture({ - gatewayName: "nemoclaw", - sandboxName: "my-assistant", - }); - const createAttemptNonce = "a".repeat(62); - const createAttemptLabel = `ai.nvidia.nemoclaw.create-attempt=${createAttemptNonce}`; - fixture.create([ - "openshell", - "sandbox", - "create", - "-g", - "nemoclaw", - "--label", - createAttemptLabel, - ]); - const createAttemptList = fixture.capture([ - "openshell", - "sandbox", - "list", - "-g", - "nemoclaw", - "--selector", - createAttemptLabel, - "--output", - "json", - "--limit", - "2", - ]); - const sandboxList = fixture.run(["openshell", "sandbox", "list", "-g", "nemoclaw"]); - const scopedSandboxGet = fixture.capture([ - "openshell", - "sandbox", - "get", - "-g", - "nemoclaw", - "my-assistant", - ]); - const readinessGet = fixture.run(["openshell", "sandbox", "get", "my-assistant"]); - const receipt = fixtureMocks.managedSandboxPolicyReceiptFixture( - { name: "my-assistant" }, - { sandboxId: fixture.state.sandboxId }, - ); - - expect(JSON.parse(createAttemptList ?? "[]")).toEqual([ - expect.objectContaining({ - id: fixture.state.sandboxId, - labels: { "ai.nvidia.nemoclaw.create-attempt": createAttemptNonce }, - name: "my-assistant", - }), - ]); - expect( - fixture.capture( - [ - "openshell", - "sandbox", - "list", - "-g", - "nemoclaw", - "--selector", - createAttemptLabel, - "--output", - "json", - "--limit", - "2", - ].map((argument) => - argument.replace( - "ai.nvidia.nemoclaw.create-attempt=", - "aiXnvidiaXnemoclawXcreate-attempt=", - ), - ), - ), - "the selector label prefix must match literally", - ).toBeNull(); - expect(String(sandboxList?.stdout)).toContain("my-assistant Ready"); - expect(scopedSandboxGet).toContain(`Id: ${fixture.state.sandboxId}`); - expect(String(readinessGet?.stdout)).toContain(`Id: ${fixture.state.sandboxId}`); - expect(receipt.lifecycleLiveIdentityFingerprint).toBe( - createHash("sha256").update(fixture.state.sandboxId).digest("hex"), - ); - }); - it("composes Docker lifecycle state across run and runCapture", () => { const originalRun = runner.run; const originalRunCapture = runner.runCapture;