From fb1803401104c1631fd2b2012106c2e7dbc94601 Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Thu, 19 Mar 2026 07:47:07 -0500 Subject: [PATCH] test: add macmini test profile --- package.json | 2 +- scripts/test-parallel.mjs | 189 +++++++++++++++++++++++++++++--------- 2 files changed, 146 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index e70c7dc3061..72ab6fb3b9a 100644 --- a/package.json +++ b/package.json @@ -655,7 +655,7 @@ "test:install:e2e:openai": "OPENCLAW_E2E_MODELS=openai CLAWDBOT_E2E_MODELS=openai bash scripts/test-install-sh-e2e-docker.sh", "test:install:smoke": "bash scripts/test-install-sh-docker.sh", "test:live": "OPENCLAW_LIVE_TEST=1 CLAWDBOT_LIVE_TEST=1 vitest run --config vitest.live.config.ts", - "test:macmini": "OPENCLAW_TEST_VM_FORKS=0 OPENCLAW_TEST_PROFILE=serial node scripts/test-parallel.mjs", + "test:macmini": "OPENCLAW_TEST_VM_FORKS=0 OPENCLAW_TEST_PROFILE=macmini node scripts/test-parallel.mjs", "test:parallels:linux": "bash scripts/e2e/parallels-linux-smoke.sh", "test:parallels:macos": "bash scripts/e2e/parallels-macos-smoke.sh", "test:parallels:windows": "bash scripts/e2e/parallels-windows-smoke.sh", diff --git a/scripts/test-parallel.mjs b/scripts/test-parallel.mjs index 8c63e61aeb4..1a128cf70dd 100644 --- a/scripts/test-parallel.mjs +++ b/scripts/test-parallel.mjs @@ -55,11 +55,13 @@ const includeExtensionsSuite = process.env.OPENCLAW_TEST_INCLUDE_EXTENSIONS === const rawTestProfile = process.env.OPENCLAW_TEST_PROFILE?.trim().toLowerCase(); const testProfile = rawTestProfile === "low" || + rawTestProfile === "macmini" || rawTestProfile === "max" || rawTestProfile === "normal" || rawTestProfile === "serial" ? rawTestProfile : "normal"; +const isMacMiniProfile = testProfile === "macmini"; // Even on low-memory hosts, keep the isolated lane split so files like // git-commit.test.ts still get the worker/process isolation they require. const shouldSplitUnitRuns = testProfile !== "serial"; @@ -162,6 +164,17 @@ const parsePassthroughArgs = (args) => { }; const { fileFilters: passthroughFileFilters, optionArgs: passthroughOptionArgs } = parsePassthroughArgs(passthroughArgs); +const passthroughMetadataFlags = new Set(["-h", "--help", "--listTags", "--clearCache"]); +const passthroughMetadataOnly = + passthroughArgs.length > 0 && + passthroughFileFilters.length === 0 && + passthroughOptionArgs.every((arg) => { + if (!arg.startsWith("-")) { + return false; + } + const [flag] = arg.split("=", 1); + return passthroughMetadataFlags.has(flag); + }); const countExplicitEntryFilters = (entryArgs) => { const { fileFilters } = parsePassthroughArgs(entryArgs.slice(2)); return fileFilters.length > 0 ? fileFilters.length : null; @@ -242,9 +255,25 @@ const allKnownUnitFiles = allKnownTestFiles.filter((file) => { return isUnitConfigTestFile(file); }); const defaultHeavyUnitFileLimit = - testProfile === "serial" ? 0 : testProfile === "low" ? 20 : highMemLocalHost ? 80 : 60; + testProfile === "serial" + ? 0 + : isMacMiniProfile + ? 90 + : testProfile === "low" + ? 20 + : highMemLocalHost + ? 80 + : 60; const defaultHeavyUnitLaneCount = - testProfile === "serial" ? 0 : testProfile === "low" ? 2 : highMemLocalHost ? 5 : 4; + testProfile === "serial" + ? 0 + : isMacMiniProfile + ? 6 + : testProfile === "low" + ? 2 + : highMemLocalHost + ? 5 + : 4; const heavyUnitFileLimit = parseEnvNumber( "OPENCLAW_TEST_HEAVY_UNIT_FILE_LIMIT", defaultHeavyUnitFileLimit, @@ -538,12 +567,16 @@ const targetedEntries = (() => { // Node 25 local runs still show cross-process worker shutdown contention even // after moving the known heavy files into singleton lanes. const topLevelParallelEnabled = - testProfile !== "low" && testProfile !== "serial" && !(!isCI && nodeMajor >= 25); + testProfile !== "low" && + testProfile !== "serial" && + !(!isCI && nodeMajor >= 25) && + !isMacMiniProfile; const overrideWorkers = Number.parseInt(process.env.OPENCLAW_TEST_WORKERS ?? "", 10); const resolvedOverride = Number.isFinite(overrideWorkers) && overrideWorkers > 0 ? overrideWorkers : null; const parallelGatewayEnabled = - process.env.OPENCLAW_TEST_PARALLEL_GATEWAY === "1" || (!isCI && highMemLocalHost); + !isMacMiniProfile && + (process.env.OPENCLAW_TEST_PARALLEL_GATEWAY === "1" || (!isCI && highMemLocalHost)); // Keep gateway serial by default except when explicitly requested or on high-memory local hosts. const keepGatewaySerial = isWindowsCi || @@ -570,45 +603,52 @@ const defaultWorkerBudget = extensions: 4, gateway: 1, } - : testProfile === "serial" + : isMacMiniProfile ? { - unit: 1, + unit: 3, unitIsolated: 1, extensions: 1, gateway: 1, } - : testProfile === "max" + : testProfile === "serial" ? { - unit: localWorkers, - unitIsolated: Math.min(4, localWorkers), - extensions: Math.max(1, Math.min(6, Math.floor(localWorkers / 2))), - gateway: Math.max(1, Math.min(2, Math.floor(localWorkers / 4))), + unit: 1, + unitIsolated: 1, + extensions: 1, + gateway: 1, } - : highMemLocalHost + : testProfile === "max" ? { - // After peeling measured hotspots into dedicated lanes, the shared - // unit-fast lane shuts down more reliably with a slightly smaller - // worker fan-out than the old "max it out" local default. - unit: Math.max(4, Math.min(10, Math.floor((localWorkers * 5) / 8))), - unitIsolated: Math.max(1, Math.min(2, Math.floor(localWorkers / 6) || 1)), - extensions: Math.max(1, Math.min(4, Math.floor(localWorkers / 4))), - gateway: Math.max(2, Math.min(6, Math.floor(localWorkers / 2))), + unit: localWorkers, + unitIsolated: Math.min(4, localWorkers), + extensions: Math.max(1, Math.min(6, Math.floor(localWorkers / 2))), + gateway: Math.max(1, Math.min(2, Math.floor(localWorkers / 4))), } - : lowMemLocalHost + : highMemLocalHost ? { - // Sub-64 GiB local hosts are prone to OOM with large vmFork runs. - unit: 2, - unitIsolated: 1, - extensions: 4, - gateway: 1, - } - : { - // 64-95 GiB local hosts: conservative split with some parallel headroom. - unit: Math.max(2, Math.min(8, Math.floor(localWorkers / 2))), - unitIsolated: 1, + // After peeling measured hotspots into dedicated lanes, the shared + // unit-fast lane shuts down more reliably with a slightly smaller + // worker fan-out than the old "max it out" local default. + unit: Math.max(4, Math.min(10, Math.floor((localWorkers * 5) / 8))), + unitIsolated: Math.max(1, Math.min(2, Math.floor(localWorkers / 6) || 1)), extensions: Math.max(1, Math.min(4, Math.floor(localWorkers / 4))), - gateway: 1, - }; + gateway: Math.max(2, Math.min(6, Math.floor(localWorkers / 2))), + } + : lowMemLocalHost + ? { + // Sub-64 GiB local hosts are prone to OOM with large vmFork runs. + unit: 2, + unitIsolated: 1, + extensions: 4, + gateway: 1, + } + : { + // 64-95 GiB local hosts: conservative split with some parallel headroom. + unit: Math.max(2, Math.min(8, Math.floor(localWorkers / 2))), + unitIsolated: 1, + extensions: Math.max(1, Math.min(4, Math.floor(localWorkers / 4))), + gateway: 1, + }; // Keep worker counts predictable for local runs; trim macOS CI workers to avoid worker crashes/OOM. // In CI on linux/windows, prefer Vitest defaults to avoid cross-test interference from lower worker counts. @@ -766,21 +806,52 @@ const run = async (entry, extraArgs = []) => { return 0; }; +const runEntriesWithLimit = async (entries, extraArgs = [], concurrency = 1) => { + if (entries.length === 0) { + return undefined; + } + + const normalizedConcurrency = Math.max(1, Math.floor(concurrency)); + if (normalizedConcurrency <= 1) { + for (const entry of entries) { + // eslint-disable-next-line no-await-in-loop + const code = await run(entry, extraArgs); + if (code !== 0) { + return code; + } + } + + return undefined; + } + + let nextIndex = 0; + let firstFailure; + const worker = async () => { + while (firstFailure === undefined) { + const entryIndex = nextIndex; + nextIndex += 1; + if (entryIndex >= entries.length) { + return; + } + const code = await run(entries[entryIndex], extraArgs); + if (code !== 0 && firstFailure === undefined) { + firstFailure = code; + } + } + }; + + const workerCount = Math.min(normalizedConcurrency, entries.length); + await Promise.all(Array.from({ length: workerCount }, () => worker())); + return firstFailure; +}; + const runEntries = async (entries, extraArgs = []) => { if (topLevelParallelEnabled) { const codes = await Promise.all(entries.map((entry) => run(entry, extraArgs))); return codes.find((code) => code !== 0); } - for (const entry of entries) { - // eslint-disable-next-line no-await-in-loop - const code = await run(entry, extraArgs); - if (code !== 0) { - return code; - } - } - - return undefined; + return runEntriesWithLimit(entries, extraArgs); }; const shutdown = (signal) => { @@ -800,6 +871,17 @@ if (process.env.OPENCLAW_TEST_LIST_LANES === "1") { process.exit(0); } +if (passthroughMetadataOnly) { + const exitCode = await runOnce( + { + name: "vitest-meta", + args: ["vitest", "run"], + }, + passthroughOptionArgs, + ); + process.exit(exitCode); +} + if (targetedEntries.length > 0) { if (passthroughRequiresSingleRun && targetedEntries.length > 1) { console.error( @@ -834,9 +916,28 @@ if (passthroughRequiresSingleRun && passthroughOptionArgs.length > 0) { process.exit(2); } -const failedParallel = await runEntries(parallelRuns, passthroughOptionArgs); -if (failedParallel !== undefined) { - process.exit(failedParallel); +if (isMacMiniProfile && targetedEntries.length === 0) { + const unitFastEntry = parallelRuns.find((entry) => entry.name === "unit-fast"); + if (unitFastEntry) { + const unitFastCode = await run(unitFastEntry, passthroughOptionArgs); + if (unitFastCode !== 0) { + process.exit(unitFastCode); + } + } + const deferredEntries = parallelRuns.filter((entry) => entry.name !== "unit-fast"); + const failedMacMiniParallel = await runEntriesWithLimit( + deferredEntries, + passthroughOptionArgs, + 3, + ); + if (failedMacMiniParallel !== undefined) { + process.exit(failedMacMiniParallel); + } +} else { + const failedParallel = await runEntries(parallelRuns, passthroughOptionArgs); + if (failedParallel !== undefined) { + process.exit(failedParallel); + } } for (const entry of serialRuns) {