Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cd74a8a
test(onboard): centralize sandbox identity fixtures
apurvvkumaria Aug 27, 2026
5187132
test(onboard): align managed create identity
apurvvkumaria Aug 27, 2026
94d6e78
test(onboard): bind fixture create attempts
apurvvkumaria Aug 27, 2026
6e5014b
merge(onboard): refresh sandbox fixture changes
apurvvkumaria Aug 27, 2026
e49c4fc
merge: resolve conflicts with main
github-actions[bot] Aug 27, 2026
b0ccca4
merge(onboard): reconcile sandbox identity fixture
apurvvkumaria Aug 27, 2026
156479b
merge(e2e): refresh Brave fixture changes
apurvvkumaria Aug 27, 2026
4af7879
test(onboard): keep managed sandbox identity consistent
apurvvkumaria Aug 27, 2026
2f59c15
merge(onboard): reconcile remote fixture updates
apurvvkumaria Aug 27, 2026
f8c36a0
test(onboard): keep fixture observations consistent
apurvvkumaria Aug 27, 2026
4b594e2
test(onboard): report missing fixture sandbox
apurvvkumaria Aug 27, 2026
00d386b
test(onboard): remove duplicate selector case
apurvvkumaria Aug 27, 2026
30ed049
test(onboard): bound replacement fixture identity
apurvvkumaria Aug 27, 2026
7e736b2
test(onboard): defer shared fixture identity import
apurvvkumaria Aug 27, 2026
ddb456b
test(onboard): reject mismatched fixture gateways
apurvvkumaria Aug 27, 2026
bf28ca1
merge(onboard): reconcile durable identity fixture
apurvvkumaria Aug 27, 2026
b860d15
merge: resolve conflicts with main
github-actions[bot] Aug 27, 2026
783b622
merge(onboard): refresh identity fixture consumers
apurvvkumaria Aug 27, 2026
858e4e9
merge(onboard): preserve fixture identity on current main
apurvvkumaria Aug 27, 2026
c6d2235
merge: resolve conflicts with main
github-actions[bot] Aug 27, 2026
7eb34a2
merge(test): refresh retained runtime improvements
apurvvkumaria Aug 27, 2026
1130ac1
merge(main): integrate host PATH and onboarding fixes
apurvvkumaria Aug 28, 2026
4de2595
test(onboard): route runtime identity through fixture
apurvvkumaria Aug 28, 2026
31b3e0d
merge(main): integrate optional SDK decision
apurvvkumaria Aug 28, 2026
09cd396
test(onboard): complete fixture watch selection
apurvvkumaria Aug 28, 2026
ab01a40
merge: resolve conflicts with main
github-actions[bot] Aug 28, 2026
1f0efbf
merge(main): reconcile sandbox fixture infrastructure
apurvvkumaria Aug 28, 2026
e720637
merge(pr): retain validated fixture resolution
apurvvkumaria Aug 28, 2026
d6a3a76
test(onboard): finish fixture identity cutover
apurvvkumaria Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ci/test-file-size-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
4 changes: 2 additions & 2 deletions src/lib/onboard/__test-helpers__/sandbox-gpu-create-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ export function createGpuFlowInput(): SandboxGpuCreateFlowInput {
};
}

export function createGpuFlowDeps(): SandboxGpuCreateFlowDeps {
export function createGpuFlowDeps(sandboxId = "alpha-sandbox-id"): SandboxGpuCreateFlowDeps {
return {
runOpenshell: vi.fn((args: string[]) =>
args[0] === "sandbox" && args[1] === "get"
? {
status: 0,
stdout: "Name: alpha\nId: alpha-sandbox-id\nState: Ready\n",
stdout: `Name: alpha\nId: ${sandboxId}\nState: Ready\n`,
stderr: "",
}
: { status: 0, stdout: "", stderr: "" },
Expand Down
9 changes: 5 additions & 4 deletions src/lib/onboard/sandbox-gpu-create-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,12 +350,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"));

Expand Down Expand Up @@ -426,7 +427,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 <REDACTED>");
Expand Down
43 changes: 22 additions & 21 deletions test/helpers/managed-image-buildless-e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,6 @@ const managedBootstrapCalls = [];
const registerCalls = [];
const runnerCommands = [];
const spawnCalls = [];
let sandboxCreated = recreate;
let existingEntryAvailable = recreate;
let registeredSandbox = null;
let managedHermesVolume = recreate ? {
Expand Down Expand Up @@ -193,6 +192,11 @@ const replace = (target, name, value) => {
const childProcess = require("node:child_process");
const fixtureMocks = require(${source("test/helpers/onboard-script-mocks.cjs")});
fixtureMocks.mockStandaloneGatewayTeardownAuthority();
const createdSandbox = fixtureMocks.createCreatedSandboxFixture({
sandboxName,
sandboxId: "fixture-managed-sandbox",
lifecycleState: recreate ? "created" : "absent",
});

const coreVersion = require(${source("src/lib/core/version.ts")});
replace(coreVersion, "getVersion", () => catalogRelease);
Expand Down Expand Up @@ -420,16 +424,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") {
Expand All @@ -449,16 +453,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({
Expand All @@ -474,12 +475,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";
}
Expand Down Expand Up @@ -543,7 +538,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 = () => [];
Expand Down Expand Up @@ -578,7 +573,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();
Expand Down
188 changes: 188 additions & 0 deletions test/helpers/onboard-created-sandbox-fixture.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// 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 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<string, unknown>) => 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): string[] {
return ["openshell", "sandbox", "create", "--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() ?? "",
),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
).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("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("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.",
);
});
});
6 changes: 5 additions & 1 deletion test/helpers/onboard-openshell-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ function writeExecutable(target: string, contents: string): void {
fs.writeFileSync(target, contents, { mode: 0o755 });
}

export const ONBOARD_READY_SANDBOX_ID = "sbx-4f2a91c0d7";

export function writeOkOpenshell(
fakeBin: string,
options: { gatewayPort?: number; readySandboxGet?: boolean } = {},
): void {
const gatewayPort = options.gatewayPort ?? 8080;
const sandboxGet = options.readySandboxGet
? 'if [ "${1:-}" = sandbox ] && [ "${2:-}" = get ]; then printf "Sandbox:\\n\\n Id: sbx-4f2a91c0d7\\n Name: %s\\n Phase: Ready\\n" "${!#}"; fi\n'
? 'if [ "${1:-}" = sandbox ] && [ "${2:-}" = get ]; then printf "Sandbox:\\n\\n Id: ' +
ONBOARD_READY_SANDBOX_ID +
'\\n Name: %s\\n Phase: Ready\\n" "${!#}"; fi\n'
: "";
writeExecutable(
path.join(fakeBin, "openshell"),
Expand Down
Loading
Loading