test: share planner and sandbox test helpers

This commit is contained in:
Peter Steinberger
2026-03-26 21:58:32 +00:00
parent 672a24cbde
commit 9dea807b28
9 changed files with 165 additions and 162 deletions

View File

@@ -1,6 +1,11 @@
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
createSandboxBrowserConfig,
createSandboxPruneConfig,
createSandboxSshConfig,
} from "../../../test/helpers/sandbox-fixtures.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { SandboxConfig } from "./types.js";
@@ -92,28 +97,21 @@ function createBackendSandboxConfig(params?: { binds?: string[]; target?: string
...(params?.binds ? { binds: params.binds } : {}),
},
ssh: {
...(params?.target ? { target: params.target } : {}),
command: "ssh",
workspaceRoot: "/remote/openclaw",
strictHostKeyChecking: true,
updateHostKeys: true,
...createSandboxSshConfig(
"/remote/openclaw",
params?.target ? { target: params.target } : {},
),
},
browser: {
enabled: false,
browser: createSandboxBrowserConfig({
image: "img",
containerPrefix: "prefix-",
network: "bridge",
cdpPort: 1,
vncPort: 2,
noVncPort: 3,
headless: true,
enableNoVnc: false,
allowHostControl: false,
autoStart: false,
autoStartTimeoutMs: 1,
},
}),
tools: { allow: [], deny: [] },
prune: { idleHours: 24, maxAgeDays: 7 },
prune: createSandboxPruneConfig(),
};
}

View File

@@ -1,11 +1,11 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
copyBundledPluginMetadata,
rewritePackageExtensions,
} from "../../scripts/copy-bundled-plugin-metadata.mjs";
import { cleanupTempDirs, makeTempRepoRoot, writeJsonFile } from "../../test/helpers/temp-repo.js";
const tempDirs: string[] = [];
const excludeOptionalEnv = { OPENCLAW_INCLUDE_OPTIONAL_BUNDLED: "0" } as const;
@@ -15,20 +15,15 @@ const copyBundledPluginMetadataWithEnv = copyBundledPluginMetadata as (params?:
}) => void;
function makeRepoRoot(prefix: string): string {
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
tempDirs.push(repoRoot);
return repoRoot;
return makeTempRepoRoot(tempDirs, prefix);
}
function writeJson(filePath: string, value: unknown): void {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
writeJsonFile(filePath, value);
}
afterEach(() => {
for (const dir of tempDirs.splice(0, tempDirs.length)) {
fs.rmSync(dir, { recursive: true, force: true });
}
cleanupTempDirs(tempDirs);
});
describe("rewritePackageExtensions", () => {

View File

@@ -30,23 +30,31 @@ async function createConversationRuntimeMock(
};
}
vi.mock("openclaw/plugin-sdk/security-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/security-runtime")>();
async function createAllowFromRuntimeMock<TModule>(
importOriginal: () => Promise<TModule>,
): Promise<TModule & { readStoreAllowFromForDmPolicy: typeof readStoreAllowFromForDmPolicy }> {
const actual = await importOriginal();
return {
...actual,
readStoreAllowFromForDmPolicy: async (params: {
provider: string;
accountId: string;
dmPolicy?: string | null;
shouldRead?: boolean | null;
}) => {
if (params.shouldRead === false || params.dmPolicy === "allowlist") {
return [];
}
return await readAllowFromStoreMock(params.provider, params.accountId);
},
readStoreAllowFromForDmPolicy,
};
});
}
async function readStoreAllowFromForDmPolicy(params: {
provider: string;
accountId: string;
dmPolicy?: string | null;
shouldRead?: boolean | null;
}) {
if (params.shouldRead === false || params.dmPolicy === "allowlist") {
return [];
}
return await readAllowFromStoreMock(params.provider, params.accountId);
}
vi.mock("openclaw/plugin-sdk/security-runtime", (importOriginal) =>
createAllowFromRuntimeMock(importOriginal),
);
vi.mock("openclaw/plugin-sdk/conversation-runtime", createConversationRuntimeMock);
vi.mock("openclaw/plugin-sdk/conversation-runtime.js", createConversationRuntimeMock);
@@ -57,23 +65,9 @@ vi.mock("../../../src/pairing/pairing-store.js", async (importOriginal) => {
upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args),
};
});
vi.mock("../../../src/security/dm-policy-shared.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../../src/security/dm-policy-shared.js")>();
return {
...actual,
readStoreAllowFromForDmPolicy: async (params: {
provider: string;
accountId: string;
dmPolicy?: string | null;
shouldRead?: boolean | null;
}) => {
if (params.shouldRead === false || params.dmPolicy === "allowlist") {
return [];
}
return await readAllowFromStoreMock(params.provider, params.accountId);
},
};
});
vi.mock("../../../src/security/dm-policy-shared.js", (importOriginal) =>
createAllowFromRuntimeMock(importOriginal),
);
export function resetDiscordComponentRuntimeMocks() {
readAllowFromStoreMock.mockClear().mockResolvedValue([]);

View File

@@ -0,0 +1,48 @@
import type {
SandboxBrowserConfig,
SandboxPruneConfig,
SandboxSshConfig,
} from "../../src/agents/sandbox/types.js";
export function createSandboxBrowserConfig(
overrides: Partial<SandboxBrowserConfig> = {},
): SandboxBrowserConfig {
return {
enabled: false,
image: "openclaw-browser",
containerPrefix: "openclaw-browser-",
network: "bridge",
cdpPort: 9222,
vncPort: 5900,
noVncPort: 6080,
headless: true,
enableNoVnc: false,
allowHostControl: false,
autoStart: false,
autoStartTimeoutMs: 1000,
...overrides,
};
}
export function createSandboxPruneConfig(
overrides: Partial<SandboxPruneConfig> = {},
): SandboxPruneConfig {
return {
idleHours: 24,
maxAgeDays: 7,
...overrides,
};
}
export function createSandboxSshConfig(
workspaceRoot: string,
overrides: Partial<SandboxSshConfig> = {},
): SandboxSshConfig {
return {
command: "ssh",
workspaceRoot,
strictHostKeyChecking: true,
updateHostKeys: true,
...overrides,
};
}

20
test/helpers/temp-repo.ts Normal file
View File

@@ -0,0 +1,20 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
export function makeTempRepoRoot(tempDirs: string[], prefix: string): string {
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
tempDirs.push(repoRoot);
return repoRoot;
}
export function writeJsonFile(filePath: string, value: unknown): void {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}
export function cleanupTempDirs(tempDirs: string[]): void {
for (const dir of tempDirs.splice(0, tempDirs.length)) {
fs.rmSync(dir, { recursive: true, force: true });
}
}

View File

@@ -1,5 +1,4 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
@@ -7,24 +6,20 @@ import {
OFFICIAL_CHANNEL_CATALOG_RELATIVE_PATH,
writeOfficialChannelCatalog,
} from "../scripts/write-official-channel-catalog.mjs";
import { cleanupTempDirs, makeTempRepoRoot, writeJsonFile } from "./helpers/temp-repo.js";
const tempDirs: string[] = [];
function makeRepoRoot(prefix: string): string {
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
tempDirs.push(repoRoot);
return repoRoot;
return makeTempRepoRoot(tempDirs, prefix);
}
function writeJson(filePath: string, value: unknown): void {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
writeJsonFile(filePath, value);
}
afterEach(() => {
for (const dir of tempDirs.splice(0, tempDirs.length)) {
fs.rmSync(dir, { recursive: true, force: true });
}
cleanupTempDirs(tempDirs);
});
describe("buildOfficialChannelCatalog", () => {

View File

@@ -7,6 +7,11 @@ import { describe, expect, it } from "vitest";
import { createOpenShellSandboxBackendFactory } from "../extensions/openshell/src/backend.js";
import { resolveOpenShellPluginConfig } from "../extensions/openshell/src/config.js";
import { createSandboxTestContext } from "../src/agents/sandbox/test-fixtures.js";
import {
createSandboxBrowserConfig,
createSandboxPruneConfig,
createSandboxSshConfig,
} from "./helpers/sandbox-fixtures.js";
const OPENCLAW_OPENSHELL_E2E = process.env.OPENCLAW_E2E_OPENSHELL === "1";
const OPENCLAW_OPENSHELL_E2E_TIMEOUT_MS = 12 * 60_000;
@@ -366,28 +371,10 @@ describe("openshell sandbox backend e2e", () => {
capDrop: ["ALL"],
env: {},
},
ssh: {
command: "ssh",
workspaceRoot: "/tmp/openclaw-sandboxes",
strictHostKeyChecking: true,
updateHostKeys: true,
},
browser: {
enabled: false,
image: "openclaw-browser",
containerPrefix: "openclaw-browser-",
network: "bridge",
cdpPort: 9222,
vncPort: 5900,
noVncPort: 6080,
headless: true,
enableNoVnc: false,
allowHostControl: false,
autoStart: false,
autoStartTimeoutMs: 1000,
},
ssh: createSandboxSshConfig("/tmp/openclaw-sandboxes"),
browser: createSandboxBrowserConfig(),
tools: { allow: [], deny: [] },
prune: { idleHours: 24, maxAgeDays: 7 },
prune: createSandboxPruneConfig(),
};
const pluginConfig = resolveOpenShellPluginConfig({

View File

@@ -59,6 +59,31 @@ const targetedUnitProxyFiles = [
"src/cli/qr-dashboard.integration.test.ts",
];
const REPO_ROOT = path.resolve(import.meta.dirname, "../..");
function runPlannerPlan(args: string[], env: NodeJS.ProcessEnv): string {
return execFileSync("node", ["scripts/test-parallel.mjs", ...args], {
cwd: REPO_ROOT,
env,
encoding: "utf8",
});
}
function runHighMemoryLocalMultiSurfacePlan(): string {
return runPlannerPlan(
["--plan", "--surface", "unit", "--surface", "extensions", "--surface", "channels"],
{
...clearPlannerShardEnv(process.env),
CI: "",
GITHUB_ACTIONS: "",
RUNNER_OS: "macOS",
OPENCLAW_TEST_HOST_CPU_COUNT: "12",
OPENCLAW_TEST_HOST_MEMORY_GIB: "128",
OPENCLAW_TEST_LOAD_AWARE: "0",
},
);
}
describe("scripts/test-parallel fatal output guard", () => {
it("fails a zero exit when V8 reports an out-of-memory fatal", () => {
const output = [
@@ -264,33 +289,7 @@ describe("scripts/test-parallel lane planning", () => {
});
it("starts isolated channel lanes before shared extension batches on high-memory local hosts", () => {
const repoRoot = path.resolve(import.meta.dirname, "../..");
const output = execFileSync(
"node",
[
"scripts/test-parallel.mjs",
"--plan",
"--surface",
"unit",
"--surface",
"extensions",
"--surface",
"channels",
],
{
cwd: repoRoot,
env: {
...clearPlannerShardEnv(process.env),
CI: "",
GITHUB_ACTIONS: "",
RUNNER_OS: "macOS",
OPENCLAW_TEST_HOST_CPU_COUNT: "12",
OPENCLAW_TEST_HOST_MEMORY_GIB: "128",
OPENCLAW_TEST_LOAD_AWARE: "0",
},
encoding: "utf8",
},
);
const output = runHighMemoryLocalMultiSurfacePlan();
const firstChannelIsolated = output.indexOf(
"message-handler.preflight.acp-bindings-channels-isolated",
@@ -304,33 +303,7 @@ describe("scripts/test-parallel lane planning", () => {
});
it("uses coarser unit-fast batching for high-memory local multi-surface runs", () => {
const repoRoot = path.resolve(import.meta.dirname, "../..");
const output = execFileSync(
"node",
[
"scripts/test-parallel.mjs",
"--plan",
"--surface",
"unit",
"--surface",
"extensions",
"--surface",
"channels",
],
{
cwd: repoRoot,
env: {
...clearPlannerShardEnv(process.env),
CI: "",
GITHUB_ACTIONS: "",
RUNNER_OS: "macOS",
OPENCLAW_TEST_HOST_CPU_COUNT: "12",
OPENCLAW_TEST_HOST_MEMORY_GIB: "128",
OPENCLAW_TEST_LOAD_AWARE: "0",
},
encoding: "utf8",
},
);
const output = runHighMemoryLocalMultiSurfacePlan();
expect(output).toContain("unit-fast-batch-4");
expect(output).not.toContain("unit-fast-batch-5");

View File

@@ -5,6 +5,21 @@ import {
} from "../scripts/test-planner/runtime-profile.mjs";
import { resolveLocalVitestMaxWorkers } from "../vitest.config.ts";
function resolveHighMemoryLocalRuntime() {
return resolveRuntimeCapabilities(
{
RUNNER_OS: "macOS",
},
{
cpuCount: 16,
totalMemoryBytes: 128 * 1024 ** 3,
platform: "darwin",
mode: "local",
loadAverage: [0.2, 0.2, 0.2],
},
);
}
describe("resolveLocalVitestMaxWorkers", () => {
it("derives a mid-tier local cap for 64 GiB hosts", () => {
expect(
@@ -155,18 +170,7 @@ describe("resolveLocalVitestMaxWorkers", () => {
});
it("enables shared channel batching on high-memory local hosts", () => {
const runtime = resolveRuntimeCapabilities(
{
RUNNER_OS: "macOS",
},
{
cpuCount: 16,
totalMemoryBytes: 128 * 1024 ** 3,
platform: "darwin",
mode: "local",
loadAverage: [0.2, 0.2, 0.2],
},
);
const runtime = resolveHighMemoryLocalRuntime();
const budget = resolveExecutionBudget(runtime);
expect(runtime.memoryBand).toBe("high");
@@ -178,18 +182,7 @@ describe("resolveLocalVitestMaxWorkers", () => {
});
it("uses a coarser shared extension batch target on high-memory local hosts", () => {
const runtime = resolveRuntimeCapabilities(
{
RUNNER_OS: "macOS",
},
{
cpuCount: 16,
totalMemoryBytes: 128 * 1024 ** 3,
platform: "darwin",
mode: "local",
loadAverage: [0.2, 0.2, 0.2],
},
);
const runtime = resolveHighMemoryLocalRuntime();
const budget = resolveExecutionBudget(runtime);
expect(runtime.memoryBand).toBe("high");