diff --git a/ci/test-file-size-budget.json b/ci/test-file-size-budget.json index 1f0be15383f..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": 1979, + "test/onboarding/onboard-messaging.test.ts": 1976, "test/onboarding/onboard-selection.test.ts": 4176 } } 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 d46a0200fb0..444781b3b05 100644 --- a/src/lib/onboard/__test-helpers__/sandbox-gpu-create-flow.ts +++ b/src/lib/onboard/__test-helpers__/sandbox-gpu-create-flow.ts @@ -57,13 +57,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 f949f720390..a039df5bad3 100644 --- a/src/lib/onboard/sandbox-gpu-create-flow.test.ts +++ b/src/lib/onboard/sandbox-gpu-create-flow.test.ts @@ -346,12 +346,13 @@ 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); - deps.runOpenshell = vi.fn(() => readySandboxGetResult("alpha-sandbox-id")); + deps.runOpenshell = vi.fn(() => readySandboxGetResult(sandboxId)); vi.mocked(deps.runCaptureOpenshell).mockImplementation((args) => - args[1] === "get" ? "ID: alpha-sandbox-id\n" : "alpha Ready", + args[1] === "get" ? `ID: ${sandboxId}\n` : "alpha Ready", ); recoverUnfinished.mockRejectedValueOnce(new Error("unfinished recovery failed")); @@ -422,7 +423,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 "); diff --git a/test/helpers/managed-image-buildless-e2e.ts b/test/helpers/managed-image-buildless-e2e.ts index 418e04591cd..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[]; } @@ -149,7 +151,6 @@ const managedBootstrapCalls = []; const registerCalls = []; const runnerCommands = []; const spawnCalls = []; -let sandboxCreated = recreate; let existingEntryAvailable = recreate; let registeredSandbox = null; let managedHermesVolume = recreate ? { @@ -193,6 +194,12 @@ 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", +}); +createdSandbox.installRuntimeObservation(); const coreVersion = require(${source("src/lib/core/version.ts")}); replace(coreVersion, "getVersion", () => catalogRelease); @@ -420,16 +427,16 @@ 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.state.lifecycleState === "created" + ) { + 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 +456,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 +478,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"; } @@ -543,7 +541,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 = () => []; @@ -578,7 +576,13 @@ 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([command, ...argv]); + } else { + createdSandbox.create([command, ...argv]); + } + } spawnCalls.push({ command: String(command), args: argv }); const child = new EventEmitter(); child.stdout = new EventEmitter(); @@ -625,6 +629,7 @@ const { createSandbox } = require(${source("src/lib/onboard.ts")}); managedBootstrapCalls, registerCalls, runnerCommands, + sandboxId: createdSandbox.state.sandboxId, spawnCalls, })); })().catch((error) => { @@ -645,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"), @@ -878,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 new file mode 100644 index 00000000000..481b04bdbfb --- /dev/null +++ b/test/helpers/onboard-created-sandbox-fixture.test.ts @@ -0,0 +1,242 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// 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, + isOpenShellSandboxId, + parseOpenShellSandboxId, + parseStrictOpenShellSandboxListJson, +} from "../../src/lib/adapters/openshell/sandbox-identity"; + +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; + readonly state: Readonly<{ + sandboxName: string; + sandboxId: string; + gatewayName: string; + phase: string; + lifecycleState: string; + generation: number; + createAttemptNonce: string | null; + ownerScopedIdentityObserved: boolean; + }>; +}; + +const requireCjs = createRequire(import.meta.url); +const { createCreatedSandboxFixture } = requireCjs("./onboard-script-mocks.cjs") as { + createCreatedSandboxFixture: (options?: Record) => CreatedSandboxFixture; +}; + +const CREATE_ATTEMPT_NONCE = "a".repeat(NEMOCLAW_CREATE_ATTEMPT_NONCE_HEX_LENGTH); + +function selectorListCommand(gatewayName: string | null, nonce = CREATE_ATTEMPT_NONCE): string[] { + return [ + "openshell", + "sandbox", + "list", + ...(gatewayName === null ? [] : ["-g", gatewayName]), + "--selector", + `${NEMOCLAW_CREATE_ATTEMPT_LABEL}=${nonce}`, + "--output", + "json", + "--limit", + "2", + ]; +} + +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", () => { + 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(createCommand()); + 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); + expect(fixture.run(["openshell", "sandbox", "get", "alpha"])).toBeNull(); + + const getOutput = fixture.capture([ + "openshell", + "sandbox", + "get", + "-g", + "gateway-alpha", + "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)", () => { + const fixture = createCreatedSandboxFixture({ + sandboxName: "alpha", + sandboxId: "sandbox-alpha", + gatewayName: "gateway-alpha", + }); + fixture.create(createCommand()); + 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( + "", + ); + expect( + fixture.run(["openshell", "sandbox", "get", "-g", "gateway-alpha", "alpha"]), + ).toMatchObject({ status: 1 }); + + const replacementNonce = "b".repeat(NEMOCLAW_CREATE_ATTEMPT_NONCE_HEX_LENGTH); + fixture.recreate(createCommand(replacementNonce)); + const replacementSandboxId = fixture.state.sandboxId; + expect(replacementSandboxId).not.toBe(priorSandboxId); + const replacementRows = parseStrictOpenShellSandboxListJson( + 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( + parseOpenShellSandboxId( + fixture.capture(["openshell", "sandbox", "get", "-g", "gateway-alpha", "alpha"]) ?? "", + ), + ).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 }); + 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", ""], + ["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(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("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()); + + expect( + fixture.capture( + selectorListCommand("gateway-alpha", "b".repeat(NEMOCLAW_CREATE_ATTEMPT_NONCE_HEX_LENGTH)), + ), + ).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-openshell-fixture.ts b/test/helpers/onboard-openshell-fixture.ts index f216c996c7e..e74e0297dc8 100644 --- a/test/helpers/onboard-openshell-fixture.ts +++ b/test/helpers/onboard-openshell-fixture.ts @@ -4,25 +4,18 @@ import fs from "node:fs"; import path from "node:path"; -import onboardFixtureContract from "./onboard-fixture-contract.json"; - -export const ONBOARD_CREATED_SANDBOX_ID = onboardFixtureContract.createdSandboxId; - function writeExecutable(target: string, contents: string): void { fs.writeFileSync(target, contents, { mode: 0o755 }); } 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_CREATED_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-policy-authority.test.ts b/test/helpers/onboard-script-mocks-policy-authority.test.ts index 04118e677f2..303514487b8 100644 --- a/test/helpers/onboard-script-mocks-policy-authority.test.ts +++ b/test/helpers/onboard-script-mocks-policy-authority.test.ts @@ -7,8 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { CaptureOpenshellResult } from "../../src/lib/adapters/openshell/client"; import { - ONBOARD_CREATED_SANDBOX_ID, - mockCreatedSandboxIdentityList, + createCreatedSandboxFixture, mockStructuredOpenShellCaptureFromRunner, } from "./onboard-script-mocks.cjs"; @@ -27,36 +26,31 @@ const exactCreateQuery = [ "--limit", "2", ] as const; +const exactCreateCommand = [ + "openshell", + "sandbox", + "create", + "--label", + selector, +] as const; afterEach(() => { vi.restoreAllMocks(); }); -describe("mockCreatedSandboxIdentityList", () => { - it("publishes identity for the exact gateway-scoped create-attempt query (#9833)", () => { - expect( - mockCreatedSandboxIdentityList(exactCreateQuery, { - gatewayName: "nemoclaw-test", - sandboxName: "my-assistant", - }), - ).toContain('"name":"my-assistant"'); - }); - +describe("created sandbox fixture selector observations", () => { it("publishes identity through the Linux process-tree timeout wrapper (#10238, #9833)", () => { + const fixture = createCreatedSandboxFixture({ gatewayName: "nemoclaw-test" }); + fixture.create(exactCreateCommand); + expect( - mockCreatedSandboxIdentityList( - [ - "/usr/bin/timeout", - "--signal=KILL", - "29.75s", - "/opt/openshell", - ...exactCreateQuery.slice(1), - ], - { - gatewayName: "nemoclaw-test", - sandboxName: "my-assistant", - }, - ), + fixture.capture([ + "/usr/bin/timeout", + "--signal=KILL", + "29.75s", + "/opt/openshell", + ...exactCreateQuery.slice(1), + ]), ).toContain('"name":"my-assistant"'); }); @@ -93,7 +87,10 @@ describe("mockCreatedSandboxIdentityList", () => { ], ], ])("rejects %s (#9833)", (_case, command) => { - expect(mockCreatedSandboxIdentityList(command, { gatewayName: "nemoclaw-test" })).toBeNull(); + const fixture = createCreatedSandboxFixture({ gatewayName: "nemoclaw-test" }); + fixture.create(exactCreateCommand); + + expect(fixture.capture(command)).toBeNull(); }); }); @@ -106,15 +103,22 @@ describe("mockStructuredOpenShellCaptureFromRunner", () => { ) => CaptureOpenshellResult; }; let restoreCapture: () => void; + let createdSandbox: ReturnType; beforeEach(() => { const runner = require("../../src/lib/runner.ts") as { runCapture: (command: readonly string[]) => string; }; client = require("../../src/lib/adapters/openshell/client.ts"); - vi.spyOn(runner, "runCapture").mockReturnValue(""); - restoreCapture = mockStructuredOpenShellCaptureFromRunner(); - mockCreatedSandboxIdentityList(exactCreateQuery, { + createdSandbox = createCreatedSandboxFixture({ + gatewayName: "nemoclaw-test", + sandboxName: "my-assistant", + }); + createdSandbox.create(exactCreateCommand); + vi.spyOn(runner, "runCapture").mockImplementation( + (command) => createdSandbox.capture([...command]) ?? "", + ); + restoreCapture = mockStructuredOpenShellCaptureFromRunner({ gatewayName: "nemoclaw-test", sandboxName: "my-assistant", }); @@ -124,8 +128,15 @@ describe("mockStructuredOpenShellCaptureFromRunner", () => { restoreCapture(); }); - it("clears a published identity when a new fixture is installed (#9833)", () => { - const restoreSecondCapture = mockStructuredOpenShellCaptureFromRunner(); + it("does not synthesize an identity without a fixture observation (#10463)", () => { + const runner = require("../../src/lib/runner.ts") as { + runCapture: (command: readonly string[]) => string; + }; + vi.mocked(runner.runCapture).mockReturnValue(""); + const restoreSecondCapture = mockStructuredOpenShellCaptureFromRunner({ + gatewayName: "nemoclaw-test", + sandboxName: "my-assistant", + }); try { const result = client.captureOpenshellCommand( "/opt/openshell", @@ -165,7 +176,7 @@ describe("mockStructuredOpenShellCaptureFromRunner", () => { ["sandbox", "get", "-g", "nemoclaw-test", "my-assistant"], { includeStreams: true }, ).stdout, - ).toContain(`Id: ${ONBOARD_CREATED_SANDBOX_ID}`); + ).toContain(`Id: ${createdSandbox.state.sandboxId}`); }); it.each([ diff --git a/test/helpers/onboard-script-mocks.cjs b/test/helpers/onboard-script-mocks.cjs index 096b945c287..3dbbf88023b 100644 --- a/test/helpers/onboard-script-mocks.cjs +++ b/test/helpers/onboard-script-mocks.cjs @@ -88,6 +88,8 @@ Module._resolveFilename = function resolveLazySourceFilename(request, parent, is }; Module._extensions[".ts"] = lazySourceRequire; +const { createdSandboxId: ONBOARD_READY_SANDBOX_ID } = require("./onboard-fixture-contract.json"); + function normalizeCommand(command) { return (Array.isArray(command) ? command.join(" ") : String(command)).replace(/'/g, ""); } @@ -141,7 +143,7 @@ function mockManagedEndpointlessProviderProfileRun(command) { function createStatefulMessagingProviderRunner({ commands, initialProviders = [], - readySandboxName = null, + createdSandbox = null, }) { const providers = new Map( initialProviders.map(([name, type, credential]) => [name, { type, credential }]), @@ -160,6 +162,8 @@ function createStatefulMessagingProviderRunner({ 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") { @@ -246,20 +250,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: ${ONBOARD_CREATED_SANDBOX_ID}\nPhase: Ready\n`, - ), - stderr: Buffer.alloc(0), - }; - } return { status: 0 }; }; } @@ -291,7 +281,6 @@ const OPENCLAW_SECURITY_INVENTORY_PROBE = [ const ONBOARD_SANDBOX_OLD_CONTAINER_ID = "a".repeat(64); const ONBOARD_SANDBOX_NEW_CONTAINER_ID = "b".repeat(64); -const { createdSandboxId: ONBOARD_CREATED_SANDBOX_ID } = require("./onboard-fixture-contract.json"); const ONBOARD_SANDBOX_INSPECT = { Id: ONBOARD_SANDBOX_OLD_CONTAINER_ID, Image: `sha256:${"c".repeat(64)}`, @@ -378,14 +367,6 @@ function mockOnboardRunCapture(command, options = {}) { return mockSandboxExecCurl(command, options); } -let publishedCreatedSandboxIdentity = null; -let publishedCreatedGatewayName = "nemoclaw"; -let publishedCreatedGatewayPort = 8080; - -function clearMockCreatedSandboxIdentity() { - publishedCreatedSandboxIdentity = null; -} - function exactOpenShellArgs(command) { const args = Array.isArray(command) ? command.map(String) : []; const verbs = new Set(["gateway", "policy", "sandbox"]); @@ -405,48 +386,266 @@ function exactOpenShellArgs(command) { return null; } -function mockCreatedSandboxIdentityList(command, options = {}) { - const args = exactOpenShellArgs(command); - if (!args) return null; - const prefix = "ai.nvidia.nemoclaw.create-attempt="; - const selector = args[5] || ""; - const gatewayName = options.gatewayName || publishedCreatedGatewayName; - if ( - args.length !== 10 || - args[0] !== "sandbox" || - args[1] !== "list" || - args[2] !== "-g" || - args[3] !== gatewayName || - args[4] !== "--selector" || - !selector.startsWith(prefix) || - !/^[0-9a-f]{62}$/u.test(selector.slice(prefix.length)) || - args[6] !== "--output" || - args[7] !== "json" || - args[8] !== "--limit" || - args[9] !== "2" - ) { - return null; - } - const nonce = selector.slice(prefix.length); - publishedCreatedGatewayName = gatewayName; - publishedCreatedSandboxIdentity = { - id: options.sandboxId || ONBOARD_CREATED_SANDBOX_ID, - 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 + : ONBOARD_READY_SANDBOX_ID; + 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, + 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 ( + 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."); + } + if ( + state.createAttemptNonce !== null && + !createAttemptNoncePattern.test(state.createAttemptNonce) + ) { + throw new Error("Created sandbox fixture requires one valid create-attempt nonce."); + } + }; + + 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; + 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."); + } + 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] || ""]; + 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 observe = (command, allowPublishedUnscopedGet) => { + const details = commandDetails(command); + if (!details) return null; + 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` + : ""; + } + if (action !== "list") return null; + + const selectorIndex = args.indexOf("--selector"); + if (selectorIndex >= 0) { + const prefix = `${sandboxIdentity.NEMOCLAW_CREATE_ATTEMPT_LABEL}=`; + const exactArgs = exactOpenShellArgs(command); + if ( + !exactArgs || + exactArgs.length !== 10 || + exactArgs[0] !== "sandbox" || + exactArgs[1] !== "list" || + exactArgs[2] !== "-g" || + exactArgs[3] !== state.gatewayName || + exactArgs[4] !== "--selector" || + !exactArgs[5].startsWith(prefix) || + exactArgs[6] !== "--output" || + exactArgs[7] !== "json" || + exactArgs[8] !== "--limit" || + exactArgs[9] !== "2" + ) { + return null; + } + const selector = exactArgs[5]; + if (!isCreated()) return "[]"; + const nonce = selector.slice(prefix.length); + if (nonce !== state.createAttemptNonce) return "[]"; + 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, + }, + ]); + } + + 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 = observe(command, true); + 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) => { + 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; + }; + + const deleteSandbox = () => { + if (state.lifecycleState !== "created") { + throw new Error("Created sandbox fixture can delete only a created sandbox."); + } + state.lifecycleState = "deleted"; + state.ownerScopedIdentityObserved = false; + }; + + 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; + const replacementFingerprint = sandboxIdentity.fingerprintOpenShellSandboxId(initialSandboxId); + state.sandboxId = `sbx-recreated-${state.generation}-${replacementFingerprint}`; + state.createAttemptNonce = createAttemptNonce; + state.ownerScopedIdentityObserved = false; + assertState(); + state.lifecycleState = "created"; }; - return JSON.stringify([publishedCreatedSandboxIdentity]); + + const setPhase = (phase) => { + state.phase = phase; + 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, + get state() { + return Object.freeze({ ...state }); + }, + }); } function installVerifiedSandboxCreateFixture(registry, options) { - mockStructuredOpenShellCaptureFromRunner(); const sandboxName = options.sandboxName; const gatewayName = options.gatewayName || "nemoclaw"; - publishedCreatedGatewayName = gatewayName; - publishedCreatedGatewayPort = options.gatewayPort || 8080; + const gatewayPort = options.gatewayPort || 8080; + mockStructuredOpenShellCaptureFromRunner({ gatewayName, gatewayPort, sandboxName }); const sessionId = options.sessionId || "integration-fixture-session"; const selection = { provider: options.provider, @@ -727,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 || ONBOARD_CREATED_SANDBOX_ID; + const sandboxId = options.sandboxId || ONBOARD_READY_SANDBOX_ID; const sandboxIdentityFingerprint = require("node:crypto") .createHash("sha256") .update(sandboxId) @@ -755,20 +954,22 @@ function managedSandboxPolicyReceiptFixture(entry, options = {}) { }; } -function mockStructuredOpenShellCaptureFromRunner() { +function mockStructuredOpenShellCaptureFromRunner(options = {}) { const runner = require(path.resolve(__dirname, "../../src/lib/runner.ts")); const client = require(path.resolve(__dirname, "../../src/lib/adapters/openshell/client.ts")); const originalCaptureOpenshellCommand = client.captureOpenshellCommand; - publishedCreatedSandboxIdentity = null; + const gatewayName = options.gatewayName || "nemoclaw"; + const gatewayPort = options.gatewayPort || 8080; + const sandboxName = options.sandboxName || null; client.captureOpenshellCommand = (binary, args, options = {}) => { const exactGatewayInfo = args.length === 4 && args[0] === "gateway" && args[1] === "info" && args[2] === "-g" && - args[3] === publishedCreatedGatewayName; + args[3] === gatewayName; if (exactGatewayInfo) { - const stdout = `Gateway endpoint: http://127.0.0.1:${publishedCreatedGatewayPort}\n`; + const stdout = `Gateway endpoint: http://127.0.0.1:${gatewayPort}\n`; return { status: 0, output: stdout.trim(), @@ -780,17 +981,17 @@ function mockStructuredOpenShellCaptureFromRunner() { args[0] === "policy" && args[1] === "get" && args[2] === "-g" && - args[3] === publishedCreatedGatewayName && + args[3] === gatewayName && args[4] === "--full" && args[5] === "--output" && args[6] === "json" && - publishedCreatedSandboxIdentity?.name === args[7]; + sandboxName === args[7]; const isFreshGlobalPolicyHistoryRead = args.length === 7 && args[0] === "policy" && args[1] === "list" && args[2] === "-g" && - args[3] === publishedCreatedGatewayName && + args[3] === gatewayName && args[4] === "--global" && args[5] === "--limit" && args[6] === "1"; @@ -812,7 +1013,7 @@ function mockStructuredOpenShellCaptureFromRunner() { if (isCreatedSandboxPolicyRead && stdout.trim().length === 0) { const fallback = JSON.stringify({ scope: "sandbox", - sandbox: publishedCreatedSandboxIdentity.name, + sandbox: sandboxName, status: "effective", policy_source: "sandbox", hash: "fixture-policy", @@ -830,23 +1031,10 @@ function mockStructuredOpenShellCaptureFromRunner() { args[0] === "sandbox" && args[1] === "get" && args[2] === "-g" && - args[3] === publishedCreatedGatewayName; + args[3] === gatewayName; if (isSandboxGet && stdout.trim().length === 0) { - const sandboxName = String(args.at(-1) || "unknown"); - if (publishedCreatedSandboxIdentity?.name === sandboxName) { - const readyOutput = [ - `Name: ${sandboxName}`, - `Id: ${publishedCreatedSandboxIdentity.id}`, - "Phase: Ready", - "", - ].join("\n"); - return { - status: 0, - output: readyOutput.trim(), - ...(options.includeStreams === true ? { stdout: readyOutput, stderr: "" } : {}), - }; - } - const stderr = `Error: sandbox ${sandboxName} not found\n`; + const requestedSandboxName = String(args.at(-1) || "unknown"); + const stderr = `Error: sandbox ${requestedSandboxName} not found\n`; return { status: 1, output: options.includeStderr === true ? stderr.trim() : "", @@ -861,7 +1049,6 @@ function mockStructuredOpenShellCaptureFromRunner() { }; return () => { client.captureOpenshellCommand = originalCaptureOpenshellCommand; - publishedCreatedSandboxIdentity = null; }; } @@ -1029,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_CREATED_SANDBOX_ID; authorityStore.createDockerManagedBootstrapAuthorityStore = () => ({ async recordPreparedAuthority(authority) { return { @@ -1182,15 +1365,13 @@ if (process.env.NEMOCLAW_TEST_MANAGED_IMAGE_CATALOG === "1") { } module.exports = { - ONBOARD_CREATED_SANDBOX_ID, mockEndpointlessProviderProfileRun, mockManagedEndpointlessProviderProfileRun, createStatefulMessagingProviderRunner, isOpenClawSecurityInventoryProbe, mockDockerSandboxLifecycleReleaseFromRunner, mockFreshOpenClawPluginDiscovery, - clearMockCreatedSandboxIdentity, - mockCreatedSandboxIdentityList, + createCreatedSandboxFixture, mockStructuredOpenShellCaptureFromRunner, installVerifiedSandboxCreateFixture, managedSandboxPolicyReceiptFixture, diff --git a/test/helpers/vitest-watch-triggers.ts b/test/helpers/vitest-watch-triggers.ts index 940a5b779f0..d7be9462f0c 100644 --- a/test/helpers/vitest-watch-triggers.ts +++ b/test/helpers/vitest-watch-triggers.ts @@ -51,15 +51,25 @@ export const vitestWatchTriggerPatterns: VitestWatchTriggerPattern[] = [ testsToRun: runTests("test/repository/github-actions-workflow-names.test.ts"), }, { - pattern: /(?:^|\/)test\/helpers\/onboard-script-mocks\.cjs$/, + pattern: + /(?:^|\/)test\/helpers\/(?:onboard-fixture-contract\.json|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-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-script-mocks-contract.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/onboarding/onboard-custom-dockerfile.test.ts b/test/onboarding/onboard-custom-dockerfile.test.ts index 2e6c5fb4d91..5d359ab0c33 100644 --- a/test/onboarding/onboard-custom-dockerfile.test.ts +++ b/test/onboarding/onboard-custom-dockerfile.test.ts @@ -185,13 +185,17 @@ 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")); const script = String.raw` const runner = require(${runnerPath}); 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}); @@ -233,21 +237,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: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\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 +263,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + 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 256c7ad4b1a..a4db0fd14b0 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}); @@ -69,7 +69,7 @@ const { EventEmitter } = require("node:events"); const _n = (command) => (Array.isArray(command) ? command.join(" ") : String(command)).replace(/'/g, ""); const commands = []; -let sandboxCreated = false; +let createdSandbox = null; runner.run = (command, opts = {}) => { const normalized = _n(command); @@ -77,7 +77,7 @@ runner.run = (command, opts = {}) => { const profileResult = require(${onboardScriptMocksPath}).mockManagedEndpointlessProviderProfileRun(command); if (profileResult !== null) return profileResult; if (normalized.includes("sandbox delete") && normalized.includes("my-assistant")) { - sandboxCreated = false; + if (createdSandbox?.state.lifecycleState === "created") createdSandbox.delete(); } if (normalized.includes("sandbox list")) return { status: 0, stdout: "No sandboxes found." }; if (normalized.includes("provider get -g nemoclaw tavily-search")) { @@ -92,38 +92,13 @@ runner.run = (command, opts = {}) => { if (normalized.includes("provider get -g nemoclaw ")) { return { status: 0, stdout: "" }; } -if (normalized.includes("sandbox get") && normalized.includes("my-assistant")) { - if (sandboxCreated) { - return { - status: 0, - stdout: Buffer.from( - "my-assistant\nId: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\nPhase: Ready\n", - ), - stderr: Buffer.alloc(0), - }; - } - const stderr = Buffer.from("Error: sandbox my-assistant not found\n"); - return { - status: 1, - stdout: Buffer.alloc(0), - stderr, - output: [null, Buffer.alloc(0), stderr], - }; - } - return { status: 0 }; + const sandboxResult = createdSandbox?.run(command) ?? null; + return sandboxResult ?? { status: 0 }; }; runner.runCapture = (command) => { const normalized = _n(command); - const createdIdentity = sandboxCreated - ? fixtureMocks.mockCreatedSandboxIdentityList(command) - : null; - if (createdIdentity !== null) return createdIdentity; - if (normalized.includes("sandbox get") && normalized.includes("my-assistant")) { - return sandboxCreated - ? "my-assistant\nId: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID - : ""; - } - if (normalized.includes("sandbox list")) return sandboxCreated ? "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")) { @@ -145,7 +120,7 @@ sandboxBaseImage.resolveSandboxBaseImage = () => ({ }); childProcess.spawn = (...args) => { - sandboxCreated = true; + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -164,19 +139,24 @@ childProcess.spawn = (...args) => { const { createSandbox } = require(${onboardPath}); -const createReservedSandbox = () => createSandbox( - ...fixtureMocks.sandboxCreateArgsWithVerifiedReservation( - [null, "gpt-5.4", "nvidia-prod", null, null, null, null, null, null, null, null, null, []], - createFixture, - ), -); +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, []], + createFixture, + ), + ); +}; (async () => { process.env.OPENSHELL_GATEWAY = "nemoclaw"; const firstSandboxName = await createReservedSandbox(); registry.removeSandbox("my-assistant"); - sandboxCreated = false; - fixtureMocks.clearMockCreatedSandboxIdentity(); + createdSandbox.delete(); const sandboxNames = [ firstSandboxName, await createReservedSandbox(), diff --git a/test/onboarding/onboard-fresh-create-identity.test.ts b/test/onboarding/onboard-fresh-create-identity.test.ts index 76b29a34030..42cefb93c78 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, @@ -207,12 +215,19 @@ const fs = require("node:fs"); const commands = []; const lifecycleObservationCommands = []; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ + sandboxName: "my-assistant", + sandboxId: "sbx-fresh-create", + gatewayName: "nemoclaw-18080", +}); +const mismatchedSandboxId = createdSandbox.state.sandboxId + "-mismatch"; let sandboxListCalls = 0; let dockerPsCalls = 0; -let sandboxCreated = false; 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)}; @@ -227,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", @@ -266,12 +284,11 @@ runner.run = (command, opts = {}) => { commands.push({ command: cmd, env: opts.env || null }); const profileResult = require(${onboardScriptMocksPath}).mockEndpointlessProviderProfileRun(command, "nemoclaw-mcp-v1", false); if (profileResult !== null) return profileResult; - 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 }; + const sandboxResult = createdSandbox.run(command); + return sandboxResult ?? { status: 0 }; }; runner.runCapture = (command) => { const cmd = _n(command); @@ -285,22 +302,26 @@ 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) { + 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); } - 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; @@ -374,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"); } @@ -423,8 +445,9 @@ process.kill = (pid, signal) => { }; childProcess.spawn = (...args) => { - sandboxCreated = true; - _deleted = false; + const command = [args[0], ...(Array.isArray(args[1]) ? args[1] : [])]; + createdSandbox.create(command); + if (_n(command).includes("sandbox create")) _deleted = false; const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -448,7 +471,7 @@ childProcess.spawn = (...args) => { process.nextTick(() => child.emit("close", signal === "SIGTERM" ? 0 : 1)); return true; }; - commands.push({ command: _n([args[0], ...(Array.isArray(args[1]) ? args[1] : [])]), env: args[2]?.env || null, child }); + commands.push({ command: _n(command), env: args[2]?.env || null, child }); process.nextTick(() => { child.stdout.emit("data", Buffer.from("Created sandbox: my-assistant\n")); child.stderr.emit("data", Buffer.from("Setting up NemoClaw...\n")); @@ -494,7 +517,8 @@ const writePayload = (sandboxName, creationError, exitCode = 0) => { creationError, exitCode, deleted: _deleted, - sandboxCreated, + sandboxCreated: createdSandbox.state.lifecycleState === "created", + sandboxId: createdSandbox.state.sandboxId, sandboxListCalls, killCalls: createCommand?.child?.killCalls ?? [], groupKillCalls, @@ -504,6 +528,9 @@ const writePayload = (sandboxName, creationError, exitCode = 0) => { lifecycleObservationCommands, registeredSandbox, credentialReadCalls, + identityMismatchGetCalls, + mismatchedSandboxId, + policyVerificationCalls, routeReservationCalls, checkpointReadCalls, registryMutationCalls, @@ -688,7 +715,7 @@ if (${JSON.stringify( command, ), ); - const identityFingerprint = createHash("sha256").update("sbx-fresh-create").digest("hex"); + const identityFingerprint = createHash("sha256").update(payload.sandboxId).digest("hex"); const assertRecoveryTuple = (record: Record) => { assert.equal(record.gatewayName, "nemoclaw-18080"); assert.equal(record.gatewayPort, 18080); @@ -754,7 +781,7 @@ if (${JSON.stringify( 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"), ); assert.match( payload.createCommand, @@ -803,6 +830,27 @@ if (${JSON.stringify( .map(({ index }: { index: number }) => index); assert.ok(deferredEffectIndexes.every((index: number) => index > createIndex)); }; + 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); @@ -1082,6 +1130,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 3754d86bb20..9238038f71e 100644 --- a/test/onboarding/onboard-installer-restore-intent.test.ts +++ b/test/onboarding/onboard-installer-restore-intent.test.ts @@ -52,7 +52,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}); @@ -66,34 +66,30 @@ 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", +}); +createdSandbox.installRuntimeObservation(); 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: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\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: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID].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, { @@ -146,13 +142,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(args.flat()); + 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); @@ -424,7 +424,7 @@ const { createSandbox } = require(${onboardPath}); ); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -434,6 +434,11 @@ const _n = (c) => (Array.isArray(c) ? c.join(" ") : String(c)).replace(/'/g, "") const registry = require(${registryPath}); const sandboxState = require(${sandboxStatePath}); const childProcess = require("node:child_process"); +const existingSandbox = fixtureMocks.createCreatedSandboxFixture({ + lifecycleState: "created", + phase: "NotReady", +}); +existingSandbox.installRuntimeObservation(); runner.run = (command) => { if (_n(command).includes("sandbox delete")) { @@ -445,8 +450,8 @@ 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: {} }); - if (normalized.includes("sandbox get") && normalized.includes("my-assistant")) return ["my-assistant", "Id: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID].join(String.fromCharCode(10)); - if (normalized.includes("sandbox list")) return "my-assistant NotReady"; + const sandboxCapture = existingSandbox.capture(command); + if (sandboxCapture !== null) return sandboxCapture; // Keep dashboard allocation inside this restore-intent fixture; host port // occupancy is unrelated to the not-ready decision under test. if (normalized.includes("forward list")) { @@ -458,7 +463,7 @@ registry.getSandbox = () => fixtureMocks.managedSandboxPolicyReceiptFixture({ name: "my-assistant", gpuEnabled: false, toolDisclosure: "progressive", -}); +}, { sandboxId: existingSandbox.state.sandboxId }); sandboxState.getLatestBackup = () => { throw new Error("unexpected getLatestBackup without installer restore intent"); }; 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 65f7869122f..ff15d2a24f2 100644 --- a/test/onboarding/onboard-messaging.test.ts +++ b/test/onboarding/onboard-messaging.test.ts @@ -67,7 +67,7 @@ describe("onboard messaging", () => { ); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); @@ -80,15 +80,11 @@ 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" }); createdSandbox.installRuntimeObservation(); +runner.run = fixtureMocks.createStatefulMessagingProviderRunner({ commands, createdSandbox }); 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"; { @@ -111,6 +107,7 @@ const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -333,7 +330,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}); @@ -362,15 +359,11 @@ const nonSlackMessagingEnvKeys = [ const commands = []; let registeredSandbox = null; -runner.run = require(${onboardScriptMocksPath}).createStatefulMessagingProviderRunner({ - commands, - readySandboxName: "my-assistant", -}); +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ sandboxName: "my-assistant" }); createdSandbox.installRuntimeObservation(); +runner.run = fixtureMocks.createStatefulMessagingProviderRunner({ commands, createdSandbox }); 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, { @@ -397,6 +390,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -521,13 +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" }); 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"])} } }); @@ -539,13 +534,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: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\nPhase: Ready\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"; @@ -561,6 +554,7 @@ const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, }); preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + 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) }); @@ -695,7 +689,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}); @@ -710,6 +704,9 @@ const fs = require("node:fs"); 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"])} }, @@ -719,13 +716,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: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\nPhase: Ready\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; @@ -751,6 +746,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -860,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}); @@ -875,17 +871,16 @@ const fs = require("node:fs"); const commands = []; let dockerfileContent; const registerCalls = []; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture(); createdSandbox.installRuntimeObservation(); 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: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\nPhase: Ready\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 +905,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -1025,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}); @@ -1045,17 +1041,16 @@ registry.registerSandbox({ const commands = []; let dockerfileContent; const registerCalls = []; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture(); createdSandbox.installRuntimeObservation(); 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: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\nPhase: Ready\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 +1076,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -1263,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}); @@ -1272,20 +1268,20 @@ const registry = require(${registryPath}); const fixtureMocks = require(${onboardScriptMocksPath}); const commands = []; -runner.run = require(${onboardScriptMocksPath}).createStatefulMessagingProviderRunner({ +const existingSandbox = fixtureMocks.createCreatedSandboxFixture({ lifecycleState: "created" }); existingSandbox.installRuntimeObservation(); +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 = messagingProviderRunner; runner.runCapture = (command) => { - // Existing sandbox that is ready - if (_n(command).includes("sandbox get") && _n(command).includes("my-assistant")) { - return "Name: my-assistant\nId: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\n"; - } - if (_n(command).includes("sandbox list")) return "my-assistant Ready"; + const sandboxCapture = existingSandbox.capture(command); + if (sandboxCapture !== null) return sandboxCapture; // All messaging providers already exist in gateway if (_n(command).includes("provider get")) return "Provider: exists"; 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"; @@ -1293,6 +1289,7 @@ runner.runCapture = (command) => { }; registry.getSandbox = () => fixtureMocks.managedSandboxPolicyReceiptFixture( { name: "my-assistant", toolDisclosure: "progressive" }, + { sandboxId: existingSandbox.state.sandboxId }, ); const { createSandbox } = require(${onboardPath}); @@ -1370,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}); @@ -1383,15 +1380,14 @@ const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); const commands = []; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture(); createdSandbox.installRuntimeObservation(); runner.run = require(${onboardScriptMocksPath}).createStatefulMessagingProviderRunner({ commands, - readySandboxName: "my-assistant", + createdSandbox, }); 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 +1408,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -1511,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}); @@ -1524,15 +1521,14 @@ const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); const commands = []; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture(); createdSandbox.installRuntimeObservation(); 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: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\nPhase: Ready\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 +1549,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + 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 a0a792deb63..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"), @@ -82,6 +82,8 @@ const scenario = ${JSON.stringify(scenario)}; 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, { @@ -109,7 +111,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; @@ -155,9 +156,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: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\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])); @@ -165,8 +165,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 " + @@ -181,12 +181,6 @@ runner.runCapture = (command) => { "Endpoint: https://inference.local/v1", ].join("\n"); } - if (normalized.includes("sandbox get")) { - return sandboxCreated - ? sandboxName + "\nId: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\n" - : ""; - } - if (normalized.includes("sandbox list")) return sandboxCreated ? sandboxName + " Ready" : ""; return ""; }; registry.getDefault = () => null; @@ -198,6 +192,7 @@ policyAuthorityPreflight.qualifySandboxPolicyAuthority = () => ({ credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -205,7 +200,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 e0cd4764144..9e2c7ff16de 100644 --- a/test/onboarding/onboard-reservation-recreate.test.ts +++ b/test/onboarding/onboard-reservation-recreate.test.ts @@ -93,30 +93,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: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\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: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID].join(String.fromCharCode(10)) : sandboxDeleted ? "" : ["my-assistant", "Id: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID].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, { @@ -177,7 +174,7 @@ const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, sessionId: "session-owner", getSandbox: registry.getSandbox, removeSandbox, - sourceSandboxId: fixtureMocks.ONBOARD_CREATED_SANDBOX_ID, + sourceSandboxId: createdSandbox.state.sandboxId, }); const preflight = require(${JSON.stringify(path.join(repoRoot, "src", "lib", "onboard", "preflight.ts"))}); @@ -190,13 +187,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(args.flat()); + 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); @@ -449,19 +450,17 @@ if (mode === "resume" && scenario === "changed-checkpoint") { }; } -let sandboxCreated = mode === "resume"; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ + sandboxName: "my-assistant", + sandboxId: "sbx-resumable-create", + lifecycleState: mode === "resume" ? "created" : "absent", +}); let createChild = null; runner.run = (command) => { const cmd = normalize(command); const profile = fixtureMocks.mockEndpointlessProviderProfileRun(command, "nemoclaw-mcp-v1", false); if (profile !== null) return profile; - if (cmd.includes("sandbox list")) { - return { status: 0, stdout: Buffer.from(sandboxCreated ? "my-assistant Ready\n" : "No sandboxes found.\n"), stderr: Buffer.alloc(0) }; - } - if (cmd.includes("sandbox get") && cmd.includes("my-assistant") && sandboxCreated) { - return { status: 0, stdout: Buffer.from("my-assistant\nId: sbx-resumable-create\n"), stderr: Buffer.alloc(0) }; - } - return { status: 0 }; + return createdSandbox.run(command) ?? { status: 0 }; }; runner.runCapture = (command) => { const cmd = normalize(command); @@ -477,15 +476,8 @@ runner.runCapture = (command) => { policy: {}, }); } - const createdIdentity = fixtureMocks.mockCreatedSandboxIdentityList(command, { - sandboxName: "my-assistant", - sandboxId: "sbx-resumable-create", - }); - if (createdIdentity !== null) return createdIdentity; - if (cmd.includes("sandbox get") && cmd.includes("my-assistant")) { - return sandboxCreated ? ["my-assistant", "Id: sbx-resumable-create"].join(String.fromCharCode(10)) : ""; - } - if (cmd.includes("sandbox list")) return sandboxCreated ? "my-assistant Ready" : ""; + 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 mocked = fixtureMocks.mockOnboardRunCapture(command, { defaultCurlOutput: "ok" }); return mocked === null ? "" : mocked; @@ -501,8 +493,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"); - sandboxCreated = true; + 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-build.test.ts b/test/onboarding/onboard-sandbox-build.test.ts index 7c0c269d95d..3f761646a2e 100644 --- a/test/onboarding/onboard-sandbox-build.test.ts +++ b/test/onboarding/onboard-sandbox-build.test.ts @@ -45,11 +45,15 @@ describe("onboard helpers", () => { ); fs.mkdirSync(fakeBin, { recursive: true }); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); + writeOkOpenshell(fakeBin); const script = String.raw` const runner = require(${runnerPath}); 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}); @@ -66,19 +70,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: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\nPhase: Ready\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 +96,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -218,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"); @@ -226,6 +225,10 @@ 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", +}); +createdSandbox.installRuntimeObservation(); const _n = (c) => (Array.isArray(c) ? c.join(" ") : String(c)).replace(/'/g, ""); const registry = require(${registryPath}); const preflight = require(${preflightPath}); @@ -239,7 +242,6 @@ const commands = []; const logs = []; const warnings = []; const baseResolutionCalls = []; -let sandboxCreated = false; const originalLog = console.log; const originalWarn = console.warn; console.log = (...args) => { @@ -294,14 +296,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 sandboxCreated && - normalized.includes("sandbox get") && - normalized.split(/\s+/).includes("hermes-sandbox") - ? { status: 0, stdout: Buffer.from("Name: hermes-sandbox\nId: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\nPhase: Ready\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 }); @@ -309,16 +305,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 ( - sandboxCreated && - normalized.includes("sandbox get") && - normalized.split(/\s+/).includes("hermes-sandbox") - ) { - return "Name: hermes-sandbox\nId: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\nPhase: Ready\n"; - } - 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; @@ -335,7 +323,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { - sandboxCreated = true; + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -456,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"); @@ -475,11 +463,14 @@ const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); platform.isWsl = () => false; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ + sandboxName: "my-assistant", +}); +createdSandbox.installRuntimeObservation(); const commands = []; const logs = []; const baseResolutionCalls = []; -let sandboxCreated = false; const originalLog = console.log; console.log = (...args) => { logs.push(args.join(" ")); @@ -524,12 +515,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: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\nPhase: Ready\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 }); @@ -537,14 +524,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: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\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; @@ -561,7 +542,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { - sandboxCreated = true; + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -631,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}); @@ -644,24 +625,22 @@ const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); 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 }); 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: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\nPhase: Ready\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; @@ -678,6 +657,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + createdSandbox.create(args.flat()); const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); @@ -745,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}); @@ -758,17 +738,17 @@ const childProcess = require("node:child_process"); const { EventEmitter } = require("node:events"); 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 }); 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: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\nPhase: Ready\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 }); @@ -776,10 +756,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); @@ -797,6 +775,7 @@ preflight.checkPortAvailable = async () => ({ ok: true }); credentials.prompt = async () => ""; childProcess.spawn = (...args) => { + 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 ec361f0a813..a406ebd856c 100644 --- a/test/onboarding/onboard-sandbox-recreation.test.ts +++ b/test/onboarding/onboard-sandbox-recreation.test.ts @@ -9,7 +9,7 @@ import os from "node:os"; import path from "node:path"; import { beforeEach, describe, it, vi } from "vitest"; -import { ONBOARD_CREATED_SANDBOX_ID, writeOkOpenshell } from "../helpers/onboard-openshell-fixture"; +import { writeOkOpenshell } from "../helpers/onboard-openshell-fixture"; import { type CommandEntry, onboardScriptMocksPath } from "../helpers/onboard-split-context"; beforeEach(() => { @@ -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: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID].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: fixtureMocks.ONBOARD_CREATED_SANDBOX_ID }); + }, { 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 = fixtureMocks.ONBOARD_CREATED_SANDBOX_ID; 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: fixtureMocks.ONBOARD_CREATED_SANDBOX_ID }); + }, { 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.state.lifecycleState === "created") 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.state.lifecycleState === "deleted") createdSandbox.recreate(args.flat()); 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,10 +262,10 @@ const { createSandbox } = require(${onboardPath}); ), "must defer source image retirement until replacement registration is proven", ); - const sourceFingerprint = createHash("sha256") - .update(ONBOARD_CREATED_SANDBOX_ID) + const sourceFingerprint = createHash("sha256").update(payload.sourceSandboxId).digest("hex"); + const replacementFingerprint = createHash("sha256") + .update(payload.replacementSandboxId) .digest("hex"); - const replacementFingerprint = createHash("sha256").update("sbx-8e6b10fd33").digest("hex"); assert.match(payload.registeredSandbox?.lifecycleGeneration ?? "", /^[0-9a-f-]{36}$/); assert.equal( payload.registeredSandbox?.lifecycleLiveIdentityFingerprint, @@ -311,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 = fixtureMocks.ONBOARD_CREATED_SANDBOX_ID; 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.state.lifecycleState === "created") 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, { @@ -351,7 +338,7 @@ runner.run = (command) => { const sourceSandbox = fixtureMocks.managedSandboxPolicyReceiptFixture({ name: "my-assistant", gpuEnabled: false, - }, { sandboxId: fixtureMocks.ONBOARD_CREATED_SANDBOX_ID }); + }, { sandboxId: createdSandbox.state.sandboxId }); registry.getSandbox = () => sourceSandbox; const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, { sandboxName: "my-assistant", @@ -386,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.state.lifecycleState === "deleted") createdSandbox.recreate(args.flat()); 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); @@ -497,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 = fixtureMocks.ONBOARD_CREATED_SANDBOX_ID; 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.state.lifecycleState === "created") 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, { @@ -537,7 +517,7 @@ runner.run = (command) => { const sourceSandbox = fixtureMocks.managedSandboxPolicyReceiptFixture({ name: "my-assistant", gpuEnabled: false, - }, { sandboxId: fixtureMocks.ONBOARD_CREATED_SANDBOX_ID }); + }, { sandboxId: createdSandbox.state.sandboxId }); registry.getSandbox = () => sourceSandbox; const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, { sandboxName: "my-assistant", @@ -559,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.state.lifecycleState === "deleted") createdSandbox.recreate(args.flat()); 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); @@ -651,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 = fixtureMocks.ONBOARD_CREATED_SANDBOX_ID; 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.state.lifecycleState === "created") 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, { @@ -696,7 +667,7 @@ runner.run = (command) => { const sourceSandbox = fixtureMocks.managedSandboxPolicyReceiptFixture({ name: "my-assistant", gpuEnabled: false, - }, { sandboxId: fixtureMocks.ONBOARD_CREATED_SANDBOX_ID }); + }, { sandboxId: createdSandbox.state.sandboxId }); registry.getSandbox = () => sourceSandbox; const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, { sandboxName: "my-assistant", @@ -731,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.state.lifecycleState === "deleted") { + createdSandbox.recreate(args.flat()); + 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); @@ -829,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 = fixtureMocks.ONBOARD_CREATED_SANDBOX_ID; 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.state.lifecycleState === "created") 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, { @@ -875,7 +842,7 @@ runner.run = (command, opts = {}) => { gpuEnabled: false, policies: ["npm"], policyTier: "balanced", - }, { sandboxId: fixtureMocks.ONBOARD_CREATED_SANDBOX_ID }); + }, { sandboxId: createdSandbox.state.sandboxId }); registry.getSandbox = () => sourceSandbox; const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, { sandboxName: "my-assistant", @@ -888,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.state.lifecycleState === "deleted") createdSandbox.recreate(args.flat()); 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); @@ -978,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"); @@ -987,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.state.lifecycleState === "created") createdSandbox.delete(); const commandString = Array.isArray(command) ? command.join(" ") : String(command); if (cmd.includes("sandbox download")) { const parts = commandString.match(/'([^']*)'/g) || []; @@ -1009,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: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\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 }); @@ -1024,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: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID].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: fixtureMocks.ONBOARD_CREATED_SANDBOX_ID }); + }, { 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(); @@ -1134,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 = fixtureMocks.ONBOARD_CREATED_SANDBOX_ID; const registry = require(${registryPath}); const credentials = require(${credentialsPath}); const childProcess = require("node:child_process"); @@ -1143,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.state.lifecycleState === "created") createdSandbox.delete(); const commandString = Array.isArray(command) ? command.join(" ") : String(command); if (cmd.includes("sandbox download")) { const parts = commandString.match(/'([^']*)'/g) || []; @@ -1165,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 }); @@ -1180,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, { @@ -1196,7 +1150,7 @@ runner.runFile = (file, args = [], opts = {}) => { const sourceSandbox = fixtureMocks.managedSandboxPolicyReceiptFixture({ name: "my-assistant", toolDisclosure: "progressive", - }, { sandboxId: fixtureMocks.ONBOARD_CREATED_SANDBOX_ID }); + }, { sandboxId: createdSandbox.state.sandboxId }); registry.getSandbox = () => sourceSandbox; const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, { sandboxName: "my-assistant", @@ -1212,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.state.lifecycleState === "deleted") createdSandbox.recreate(args.flat()); 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); @@ -1316,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 = fixtureMocks.ONBOARD_CREATED_SANDBOX_ID; 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.state.lifecycleState === "created") 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, { @@ -1362,7 +1307,7 @@ runner.run = (command, opts = {}) => { const sourceSandbox = fixtureMocks.managedSandboxPolicyReceiptFixture({ name: "my-assistant", toolDisclosure: "progressive", - }, { sandboxId: fixtureMocks.ONBOARD_CREATED_SANDBOX_ID }); + }, { sandboxId: createdSandbox.state.sandboxId }); registry.getSandbox = () => sourceSandbox; const createFixture = fixtureMocks.installVerifiedSandboxCreateFixture(registry, { sandboxName: "my-assistant", @@ -1378,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(args.flat()); + 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); @@ -1391,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-script-mocks-contract.test.ts b/test/onboarding/onboard-script-mocks-contract.test.ts index b8f8ab945a3..5bec1f46d9d 100644 --- a/test/onboarding/onboard-script-mocks-contract.test.ts +++ b/test/onboarding/onboard-script-mocks-contract.test.ts @@ -1,15 +1,8 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { spawnSync } from "node:child_process"; -import { createHash } from "node:crypto"; -import { mkdirSync, mkdtempSync, rmSync } from "node:fs"; import { createRequire } from "node:module"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; - -import { ONBOARD_CREATED_SANDBOX_ID, writeOkOpenshell } from "../helpers/onboard-openshell-fixture"; +import { describe, expect, it } from "vitest"; type CommandResult = { status: number; @@ -23,106 +16,14 @@ type Runner = { }; type OnboardScriptMocks = { - ONBOARD_CREATED_SANDBOX_ID: string; - createStatefulMessagingProviderRunner: (options: { - commands: Array<{ command: string }>; - readySandboxName: string; - }) => (command: readonly string[]) => CommandResult; - managedSandboxPolicyReceiptFixture: ( - entry: { name: string }, - options?: { sandboxId?: string }, - ) => { lifecycleLiveIdentityFingerprint: string }; - mockCreatedSandboxIdentityList: ( - command: readonly string[], - options?: { sandboxName?: string; sandboxId?: string }, - ) => string | null; mockDockerSandboxLifecycleReleaseFromRunner: () => void; }; const requireForTest = createRequire(import.meta.url); const fixtureMocks = requireForTest("../helpers/onboard-script-mocks.cjs") as OnboardScriptMocks; const runner = requireForTest("../../src/lib/runner.ts") as Runner; -const temporaryDirectories: string[] = []; - -afterEach(() => { - for (const directory of temporaryDirectories.splice(0)) { - rmSync(directory, { force: true, recursive: true }); - } -}); describe("shared onboarding process fixture contracts", () => { - it("uses one durable sandbox ID across create discovery and structured OpenShell probes", () => { - const fakeRoot = mkdtempSync(join(tmpdir(), "nemoclaw-onboard-fixture-contract-")); - temporaryDirectories.push(fakeRoot); - const fakeBin = join(fakeRoot, "bin"); - mkdirSync(fakeBin); - writeOkOpenshell(fakeBin, { readySandboxGet: true }); - - const createAttemptNonce = "a".repeat(62); - const createAttemptCommand = [ - "openshell", - "sandbox", - "list", - "-g", - "nemoclaw", - "--selector", - `ai.nvidia.nemoclaw.create-attempt=${createAttemptNonce}`, - "--output", - "json", - "--limit", - "2", - ]; - const createAttemptList = fixtureMocks.mockCreatedSandboxIdentityList(createAttemptCommand); - const sandboxGet = spawnSync( - join(fakeBin, "openshell"), - ["sandbox", "get", "-g", "nemoclaw", "my-assistant"], - { - encoding: "utf8", - timeout: 5_000, - killSignal: "SIGKILL", - }, - ); - const receipt = fixtureMocks.managedSandboxPolicyReceiptFixture({ name: "my-assistant" }); - const messagingRunner = fixtureMocks.createStatefulMessagingProviderRunner({ - commands: [], - readySandboxName: "my-assistant", - }); - const messagingGet = messagingRunner([ - "openshell", - "sandbox", - "get", - "-g", - "nemoclaw", - "my-assistant", - ]); - - expect(fixtureMocks.ONBOARD_CREATED_SANDBOX_ID).toBe(ONBOARD_CREATED_SANDBOX_ID); - expect(JSON.parse(createAttemptList ?? "[]")).toEqual([ - expect.objectContaining({ - id: ONBOARD_CREATED_SANDBOX_ID, - labels: { "ai.nvidia.nemoclaw.create-attempt": createAttemptNonce }, - name: "my-assistant", - }), - ]); - expect( - fixtureMocks.mockCreatedSandboxIdentityList( - createAttemptCommand.map((argument) => - argument.replace( - "ai.nvidia.nemoclaw.create-attempt=", - "aiXnvidiaXnemoclawXcreate-attempt=", - ), - ), - ), - "the selector label prefix must match literally", - ).toBeNull(); - expect(sandboxGet.status, sandboxGet.stderr).toBe(0); - expect(sandboxGet.stdout).toContain(`Id: ${ONBOARD_CREATED_SANDBOX_ID}`); - expect(String(messagingGet.stdout)).toContain(`Id: ${ONBOARD_CREATED_SANDBOX_ID}`); - expect(receipt.lifecycleLiveIdentityFingerprint).toBe( - createHash("sha256").update(ONBOARD_CREATED_SANDBOX_ID).digest("hex"), - ); - }); - it("composes Docker lifecycle state across run and runCapture", () => { const originalRun = runner.run; const originalRunCapture = runner.runCapture; diff --git a/test/onboarding/onboard-terminal-dashboard.test.ts b/test/onboarding/onboard-terminal-dashboard.test.ts index 08f5a3f3ce1..81a66bee850 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"); @@ -65,6 +65,11 @@ const dockerGpuSandboxCreate = require(${dockerGpuSandboxCreatePath}); const sandboxCreateStream = require(${sandboxCreateStreamPath}); const scenario = ${JSON.stringify(scenario)}; const sandboxName = "deepagents-box"; +const createdSandbox = fixtureMocks.createCreatedSandboxFixture({ + sandboxName, + lifecycleState: scenario === "reuse" ? "created" : "absent", +}); +createdSandbox.installRuntimeObservation(); const commands = []; const registerCalls = []; const updateCalls = []; @@ -124,17 +129,11 @@ 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; const providerResult = managedProviderResult(normalized); - return profileResult ?? providerResult ?? - (normalized.includes("sandbox get") && normalized.includes(sandboxName) - ? { - status: 0, - stdout: Buffer.from( - "Name: " + sandboxName + "\nId: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\n", - ), - stderr: Buffer.alloc(0), - } - : { status: 0 }); + if (providerResult !== null) return providerResult; + const sandboxResult = createdSandbox.run(command); + return sandboxResult ?? { status: 0 }; }; runner.runFile = (file, args = [], opts = {}) => { commands.push({ command: _n([file, ...args]), env: opts.env || null }); @@ -142,10 +141,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( @@ -161,12 +158,6 @@ runner.runCapture = (command) => { "Endpoint: https://inference.local/v1", ].join("\n"); } - if (normalized.includes("sandbox get") && normalized.includes(sandboxName)) { - return scenario === "reuse" - ? [sandboxName, "Id: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID].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 ""; }; @@ -205,6 +196,7 @@ const createFixture = sandboxCreateStream.streamSandboxCreate = async (command, args, env) => { if (scenario === "reuse") throw new Error("unexpected sandbox create"); + createdSandbox.create([command, ...args]); commands.push({ command: _n([command, ...args]), env }); return { status: 0, output: "Created sandbox: " + sandboxName, sawProgress: true }; }; diff --git a/test/onboarding/onboard.test.ts b/test/onboarding/onboard.test.ts index fba42ff0422..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: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID].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/repository/vitest-watch-triggers.test.ts b/test/repository/vitest-watch-triggers.test.ts index 524d0144dac..ea4dd8fe4e4 100644 --- a/test/repository/vitest-watch-triggers.test.ts +++ b/test/repository/vitest-watch-triggers.test.ts @@ -116,15 +116,27 @@ function triggeredBy(relativePath: string): string[] { } describe("Vitest opaque-input watch triggers", () => { - it("maps the onboard child-process preload to its managed-image fixtures", () => { - expect(triggeredBy("test/helpers/onboard-script-mocks.cjs")).toEqual([ + it.each([ + "test/helpers/onboard-fixture-contract.json", + "test/helpers/onboard-script-mocks.cjs", + ])("maps %s to every sandbox identity consumer (#10463)", (fixturePath) => { + expect(triggeredBy(fixturePath)).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-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-script-mocks-contract.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/security/shellquote-sandbox.test.ts b/test/security/shellquote-sandbox.test.ts index 767cb92c897..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` @@ -89,6 +89,8 @@ 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(); +createdSandbox.installRuntimeObservation(); runner.run = (command, opts = {}) => { const text = asText(command); commands.push({ type: "run", command: text, env: opts.env || null }); @@ -105,16 +107,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: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\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 }); @@ -122,10 +115,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"; @@ -145,11 +136,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 (...args) => { + createdSandbox.create(args.flat()); + return { + status: 0, + output: "Built image openshell/sandbox-from:123\nCreated sandbox: my-assistant", + sawProgress: true, + }; +}; const { createSandbox } = require(${onboardPath}); (async () => { try {