Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
8813de5
test(onboard): align sandbox fixture contracts
prekshivyas Aug 27, 2026
e094695
Merge origin/main into codex/fix-post-10339-merge-test-contracts
prekshivyas Aug 27, 2026
1d09d73
test: tighten fixture contract coverage
prekshivyas Aug 27, 2026
fe82942
test: isolate shared fixture contract data
prekshivyas Aug 27, 2026
2268707
merge: resolve conflicts with main
github-actions[bot] Aug 27, 2026
1a089c3
merge: resolve conflicts with main
github-actions[bot] Aug 27, 2026
2b1e7b0
merge: resolve conflicts with main
github-actions[bot] Aug 27, 2026
a77b19a
test(e2e): close fast coverage gaps
prekshivyas Aug 27, 2026
70b7256
Merge remote-tracking branch 'origin/codex/fix-post-10339-merge-test-…
prekshivyas Aug 27, 2026
7bb095d
test(e2e): map latest messaging coverage
prekshivyas Aug 27, 2026
0038615
test(onboard): restore fixture identity contract
prekshivyas Aug 27, 2026
bbe7261
test(onboard): satisfy merged size guard
prekshivyas Aug 27, 2026
d2b1334
Merge branch 'main' into codex/fix-post-10339-merge-test-contracts
prekshivyas Aug 27, 2026
ad69d70
Merge branch 'main' into codex/fix-post-10339-merge-test-contracts
prekshivyas Aug 28, 2026
ba5790d
test(e2e): fix proof and fixture contracts
prekshivyas Aug 28, 2026
185d4db
test(onboarding): centralize sandbox fixture identity
prekshivyas Aug 28, 2026
dd6f7fb
test(e2e): enforce parity for live helpers
prekshivyas Aug 28, 2026
37bde88
test(e2e): secure Slack credential scan transport
prekshivyas Aug 28, 2026
91b223d
test(e2e): align provider proof claims
prekshivyas Aug 28, 2026
4873647
Merge branch 'main' into codex/fix-post-10339-merge-test-contracts
prekshivyas Aug 28, 2026
3127999
test(e2e): keep raw Slack tokens host-side
prekshivyas Aug 28, 2026
4c28a1b
merge: refresh from main
prekshivyas Aug 28, 2026
ee5334e
Merge branch 'main' into codex/fix-post-10339-merge-test-contracts
prekshivyas Aug 28, 2026
cf40569
merge: refresh from main
prekshivyas Aug 28, 2026
1c6aa63
fix(e2e): preserve retained Hermes Slack resources
prekshivyas Aug 28, 2026
9305eab
merge: refresh from main
prekshivyas Aug 28, 2026
6e1c0e6
fix(ci): accept SDK archive not required
prekshivyas Aug 28, 2026
9296c72
fix(e2e): enforce reviewer safety boundaries
prekshivyas Aug 28, 2026
8c70c34
Merge branch 'main' into codex/fix-post-10339-merge-test-contracts
cv Aug 28, 2026
fa83d12
merge: refresh from main
prekshivyas Aug 28, 2026
d043671
merge: resolve conflicts with main
github-actions[bot] Aug 28, 2026
9124d24
merge: reconcile upstream branch
prekshivyas Aug 28, 2026
6bcc994
test(e2e): remove unused Google Chat entrypoint
prekshivyas Aug 28, 2026
41e6b9b
test(e2e): consolidate channel lifecycle cleanup
prekshivyas Aug 28, 2026
bdd3a0f
test(e2e): fail closed on cleanup evidence
prekshivyas Aug 28, 2026
9efe608
chore: merge latest main
prekshivyas 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": 1971,
"test/onboarding/onboard-selection.test.ts": 4176
}
}
33 changes: 28 additions & 5 deletions scripts/checks/e2e-mock-parity.mts
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,19 @@ export function validateMockParity(options: {
}

for (const liveFile of [...new Set(changedFiles)].filter((file) => LIVE_TEST.test(file))) {
if (!entries.has(liveFile)) {
const entry = entries.get(liveFile);
if (!entry) {
errors.push(`${liveFile}: changed live E2E needs an entry in ${DEFAULT_PARITY_MANIFEST}`);
continue;
}
const mappedFastTests = Array.isArray(entry.fast)
? entry.fast.filter((fastFile): fastFile is string => typeof fastFile === "string")
: [];
if (
mappedFastTests.length > 0 &&
!mappedFastTests.some((fastFile) => changedFiles.includes(fastFile))
) {
errors.push(`${liveFile}: change at least one mapped fast PR test with the live E2E`);
}
}

Expand All @@ -161,6 +172,18 @@ function sourceAtRef(ref: string, file: string): string | null {
}
}

/** Remove metadata-only live and fast test changes before parity validation. */
export function filterMockParityRelevantChangedFiles(
files: readonly string[],
sourceAtBase: (file: string) => string | null,
sourceAtHead: (file: string) => string | null,
): string[] {
return files.filter((file) => {
if (!LIVE_TEST.test(file) && !isFastPrTest(file)) return true;
return isMockParityRelevantSourceChange(sourceAtBase(file), sourceAtHead(file));
});
}

function changedFiles(base: string, head: string): string[] {
const files = execFileSync(
"git",
Expand All @@ -172,10 +195,10 @@ function changedFiles(base: string, head: string): string[] {
)
.split(/\r?\n/u)
.filter(Boolean);
return files.filter(
(file) =>
!LIVE_TEST.test(file) ||
isMockParityRelevantSourceChange(sourceAtRef(base, file), sourceAtRef(head, file)),
return filterMockParityRelevantChangedFiles(
files,
(file) => sourceAtRef(base, file),
(file) => sourceAtRef(head, file),
);
}

Expand Down
50 changes: 48 additions & 2 deletions test/automation/e2e/e2e-mock-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
filterMockParityRelevantChangedFiles,
isMockParityRelevantSourceChange,
type MockParityManifest,
validateMockParity,
Expand Down Expand Up @@ -68,12 +69,57 @@ describe("changed live E2E mock parity", () => {
expect(
validateMockParity({
manifest: manifest([{ live, fast: [fast] }]),
changedFiles: [live],
changedFiles: [live, fast],
fileExists: exists,
}),
).toEqual([]);
});

it("rejects a stale fast-test mapping for changed live behavior", () => {
expect(
validateMockParity({
manifest: manifest([{ live, fast: [fast] }]),
changedFiles: [live],
fileExists: exists,
}),
).toEqual([`${live}: change at least one mapped fast PR test with the live E2E`]);
});

it.each([
{
expected: [],
fastHead: "export const fastBehavior = 2;\n",
title: "retains a token-changing mapped fast test",
},
{
expected: [`${live}: change at least one mapped fast PR test with the live E2E`],
fastHead: "// formatting only\n\nexport const fastBehavior = 1;\n",
title: "filters a comment-and-whitespace-only mapped fast test",
},
])("$title", ({ expected, fastHead }) => {
const baseSources = new Map([
[live, "export const liveBehavior = 1;\n"],
[fast, "export const fastBehavior = 1;\n"],
]);
const headSources = new Map([
[live, "export const liveBehavior = 2;\n"],
[fast, fastHead],
]);
const relevantFiles = filterMockParityRelevantChangedFiles(
[live, fast],
(file) => baseSources.get(file) ?? null,
(file) => headSources.get(file) ?? null,
);

expect(
validateMockParity({
manifest: manifest([{ live, fast: [fast] }]),
changedFiles: relevantFiles,
fileExists: exists,
}),
).toEqual(expected);
});

it("rejects a changed live E2E without a parity decision", () => {
expect(
validateMockParity({ manifest: manifest([]), changedFiles: [live], fileExists: exists }),
Expand All @@ -84,7 +130,7 @@ describe("changed live E2E mock parity", () => {
expect(
validateMockParity({
manifest: manifest([{ live, fast: ["test/e2e/live/not-fast.test.ts", fast] }]),
changedFiles: [live],
changedFiles: [live, fast],
fileExists: (file) => file === live,
}),
).toEqual([
Expand Down
1 change: 1 addition & 0 deletions test/e2e/mock-parity.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
"fast": [
"src/lib/actions/sandbox/supervisor-relaunch.test.ts",
"src/lib/onboard/docker-gpu-patch-finalize.test.ts",
"src/lib/onboard/docker-gpu-supervisor-reconnect.test.ts",
"src/lib/onboard/docker-startup-command-patch.test.ts",
"src/lib/sandbox/privileged-exec.test.ts",
"test/inference/managed/managed-gateway-control.test.ts",
Expand Down
6 changes: 6 additions & 0 deletions test/helpers/onboard-child-process-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ export interface RunOnboardProcessOptions {
cwd?: string;
/** Kill the child after this many milliseconds. */
timeoutMs?: number;
/** Signal used when the timeout expires. */
killSignal?: NodeJS.Signals;
/** Optional stdin for interactive process fixtures. */
input?: string;
}

/** The decoded outcome of one spawned process run. */
Expand All @@ -96,6 +100,8 @@ export function runOnboardProcess(
encoding: "utf-8",
env: options.env,
...(options.timeoutMs === undefined ? {} : { timeout: options.timeoutMs }),
...(options.killSignal === undefined ? {} : { killSignal: options.killSignal }),
...(options.input === undefined ? {} : { input: options.input }),
});
const stdout = result.stdout ?? "";
const stderr = result.stderr ?? "";
Expand Down
3 changes: 3 additions & 0 deletions test/helpers/onboard-fixture-contract.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"createdSandboxId": "sbx-4f2a91c0d7"
}
6 changes: 5 additions & 1 deletion test/helpers/onboard-openshell-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
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 });
}
Expand All @@ -14,7 +18,7 @@ export function writeOkOpenshell(
): 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_CREATED_SANDBOX_ID}\\n Name: %s\\n Phase: Ready\\n" "\${!#}"; fi\n`
: "";
writeExecutable(
path.join(fakeBin, "openshell"),
Expand Down
12 changes: 8 additions & 4 deletions test/helpers/onboard-script-mocks.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,9 @@ function createStatefulMessagingProviderRunner({
) {
return {
status: 0,
stdout: Buffer.from(`Name: ${readySandboxName}\nId: sbx-4f2a91c0d7\nPhase: Ready\n`),
stdout: Buffer.from(
`Name: ${readySandboxName}\nId: ${ONBOARD_CREATED_SANDBOX_ID}\nPhase: Ready\n`,
),
stderr: Buffer.alloc(0),
};
}
Expand Down Expand Up @@ -247,6 +249,7 @@ 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)}`,
Expand Down Expand Up @@ -382,9 +385,9 @@ function mockCreatedSandboxIdentityList(command, options = {}) {
return null;
}
const nonce = selector.slice(prefix.length);
publishedCreatedGatewayName = gatewayName;
publishedCreatedGatewayName = gatewayName;
publishedCreatedSandboxIdentity = {
id: options.sandboxId || "sbx-4f2a91c0d7",
id: options.sandboxId || ONBOARD_CREATED_SANDBOX_ID,
name: options.sandboxName || "my-assistant",
labels: { "ai.nvidia.nemoclaw.create-attempt": nonce },
resource_version: 1,
Expand Down Expand Up @@ -681,7 +684,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 || "sbx-4f2a91c0d7";
const sandboxId = options.sandboxId || ONBOARD_CREATED_SANDBOX_ID;
const sandboxIdentityFingerprint = require("node:crypto")
.createHash("sha256")
.update(sandboxId)
Expand Down Expand Up @@ -1136,6 +1139,7 @@ if (process.env.NEMOCLAW_TEST_MANAGED_IMAGE_CATALOG === "1") {
}

module.exports = {
ONBOARD_CREATED_SANDBOX_ID,
mockEndpointlessProviderProfileRun,
mockManagedEndpointlessProviderProfileRun,
createStatefulMessagingProviderRunner,
Expand Down
9 changes: 6 additions & 3 deletions test/onboarding/onboard-extra-provider-reconciliation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +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 (normalized.includes("sandbox get") && normalized.includes("my-assistant")) {
if (sandboxCreated) {
return {
status: 0,
stdout: Buffer.from("my-assistant\nId: sbx-4f2a91c0d7\n"),
stdout: Buffer.from(
"my-assistant\nId: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\nPhase: Ready\n",
),
stderr: Buffer.alloc(0),
};
}
Expand Down Expand Up @@ -192,7 +194,8 @@ const createReservedSandbox = () => createSandbox(
const result = spawnSync(process.execPath, [scriptPath], {
cwd: repoRoot,
encoding: "utf-8",
timeout: 30_000,
timeout: 60_000,
killSignal: "SIGKILL",
env: {
...process.env,
HOME: tmpDir,
Expand Down
2 changes: 2 additions & 0 deletions test/onboarding/onboard-fresh-create-identity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@ const { createSandbox } = require(${onboardPath});
assert.ok(payload.sandboxListCalls >= 2);
assert.deepEqual(payload.groupKillCalls, [{ pid: -4242, signal: "SIGTERM" }]);
assert.deepEqual(payload.killCalls, []);
// The ownership handoff now waits for the create client to close. The
// detach-only cleanup path must not run after that confirmed close.
assert.equal(payload.unrefCalls, 0);
assert.equal(payload.stdoutDestroyCalls, 0);
assert.equal(payload.stderrDestroyCalls, 0);
Expand Down
14 changes: 10 additions & 4 deletions test/onboarding/onboard-installer-restore-intent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const repoRoot = path.join(import.meta.dirname, "../..");
const onboardScriptMocksPath = JSON.stringify(
path.join(repoRoot, "test", "helpers", "onboard-script-mocks.cjs"),
);
const ONBOARD_SUBPROCESS_TIMEOUT_MS = 30_000;
const createdTmpDirs: string[] = [];

function makeTmpDir(prefix: string): string {
Expand Down Expand Up @@ -78,7 +79,7 @@ runner.run = (command) => {
return { status: 0, stdout: Buffer.from("No sandboxes found.\n"), stderr: Buffer.alloc(0) };
}
return cmd.includes("sandbox get") && cmd.includes("my-assistant")
? { status: 0, stdout: Buffer.from("my-assistant\nId: sbx-4f2a91c0d7\n"), stderr: Buffer.alloc(0) }
? { status: 0, stdout: Buffer.from("my-assistant\nId: " + fixtureMocks.ONBOARD_CREATED_SANDBOX_ID + "\n"), stderr: Buffer.alloc(0) }
: { status: 0 };
};
runner.runCapture = (command) => {
Expand All @@ -89,7 +90,7 @@ runner.runCapture = (command) => {
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: sbx-4f2a91c0d7"].join(String.fromCharCode(10));
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";
}
Expand Down Expand Up @@ -217,7 +218,8 @@ const MARKER_SHA = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852
cwd: repoRoot,
encoding: "utf-8",
env,
timeout: 30_000,
timeout: ONBOARD_SUBPROCESS_TIMEOUT_MS,
killSignal: "SIGKILL",
});

assert.equal(result.status, 0, result.stderr || result.error?.message);
Expand Down Expand Up @@ -374,6 +376,8 @@ const { createSandbox } = require(${onboardPath});
const result = spawnSync(process.execPath, [scriptPath], {
cwd: repoRoot,
encoding: "utf-8",
timeout: ONBOARD_SUBPROCESS_TIMEOUT_MS,
killSignal: "SIGKILL",
env: {
...process.env,
HOME: tmpDir,
Expand Down Expand Up @@ -441,7 +445,7 @@ 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: sbx-4f2a91c0d7"].join(String.fromCharCode(10));
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";
// Keep dashboard allocation inside this restore-intent fixture; host port
// occupancy is unrelated to the not-ready decision under test.
Expand Down Expand Up @@ -490,6 +494,8 @@ const { createSandbox } = require(${onboardPath});
cwd: repoRoot,
encoding: "utf-8",
env,
timeout: ONBOARD_SUBPROCESS_TIMEOUT_MS,
killSignal: "SIGKILL",
});

assert.notEqual(
Expand Down
Loading