From 9dea807b2826c0a69f5afed1e2a40adca9eb29ae Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 26 Mar 2026 21:58:32 +0000 Subject: [PATCH] test: share planner and sandbox test helpers --- src/agents/sandbox/ssh-backend.test.ts | 26 +++--- .../copy-bundled-plugin-metadata.test.ts | 13 +-- .../extensions/discord-component-runtime.ts | 56 ++++++------- test/helpers/sandbox-fixtures.ts | 48 +++++++++++ test/helpers/temp-repo.ts | 20 +++++ test/official-channel-catalog.test.ts | 13 +-- test/openshell-sandbox.e2e.test.ts | 29 ++----- test/scripts/test-parallel.test.ts | 81 +++++++------------ test/vitest-config.test.ts | 41 ++++------ 9 files changed, 165 insertions(+), 162 deletions(-) create mode 100644 test/helpers/sandbox-fixtures.ts create mode 100644 test/helpers/temp-repo.ts diff --git a/src/agents/sandbox/ssh-backend.test.ts b/src/agents/sandbox/ssh-backend.test.ts index 2c57312c850..07fd1962c80 100644 --- a/src/agents/sandbox/ssh-backend.test.ts +++ b/src/agents/sandbox/ssh-backend.test.ts @@ -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(), }; } diff --git a/src/plugins/copy-bundled-plugin-metadata.test.ts b/src/plugins/copy-bundled-plugin-metadata.test.ts index b08d0658360..8cc58362785 100644 --- a/src/plugins/copy-bundled-plugin-metadata.test.ts +++ b/src/plugins/copy-bundled-plugin-metadata.test.ts @@ -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", () => { diff --git a/test/helpers/extensions/discord-component-runtime.ts b/test/helpers/extensions/discord-component-runtime.ts index 9a84c87cd0e..d98b310e6f9 100644 --- a/test/helpers/extensions/discord-component-runtime.ts +++ b/test/helpers/extensions/discord-component-runtime.ts @@ -30,23 +30,31 @@ async function createConversationRuntimeMock( }; } -vi.mock("openclaw/plugin-sdk/security-runtime", async (importOriginal) => { - const actual = await importOriginal(); +async function createAllowFromRuntimeMock( + importOriginal: () => Promise, +): Promise { + 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(); - 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([]); diff --git a/test/helpers/sandbox-fixtures.ts b/test/helpers/sandbox-fixtures.ts new file mode 100644 index 00000000000..782bd6cbf67 --- /dev/null +++ b/test/helpers/sandbox-fixtures.ts @@ -0,0 +1,48 @@ +import type { + SandboxBrowserConfig, + SandboxPruneConfig, + SandboxSshConfig, +} from "../../src/agents/sandbox/types.js"; + +export function createSandboxBrowserConfig( + overrides: Partial = {}, +): 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 { + return { + idleHours: 24, + maxAgeDays: 7, + ...overrides, + }; +} + +export function createSandboxSshConfig( + workspaceRoot: string, + overrides: Partial = {}, +): SandboxSshConfig { + return { + command: "ssh", + workspaceRoot, + strictHostKeyChecking: true, + updateHostKeys: true, + ...overrides, + }; +} diff --git a/test/helpers/temp-repo.ts b/test/helpers/temp-repo.ts new file mode 100644 index 00000000000..f5633588adc --- /dev/null +++ b/test/helpers/temp-repo.ts @@ -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 }); + } +} diff --git a/test/official-channel-catalog.test.ts b/test/official-channel-catalog.test.ts index 6a91d72f1ab..23bd52c8336 100644 --- a/test/official-channel-catalog.test.ts +++ b/test/official-channel-catalog.test.ts @@ -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", () => { diff --git a/test/openshell-sandbox.e2e.test.ts b/test/openshell-sandbox.e2e.test.ts index 21824db38ee..37a4d738632 100644 --- a/test/openshell-sandbox.e2e.test.ts +++ b/test/openshell-sandbox.e2e.test.ts @@ -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({ diff --git a/test/scripts/test-parallel.test.ts b/test/scripts/test-parallel.test.ts index d84279bfe12..c095d8bbbbc 100644 --- a/test/scripts/test-parallel.test.ts +++ b/test/scripts/test-parallel.test.ts @@ -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"); diff --git a/test/vitest-config.test.ts b/test/vitest-config.test.ts index d7106f379e7..62b1cc088a4 100644 --- a/test/vitest-config.test.ts +++ b/test/vitest-config.test.ts @@ -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");