diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc9857da3e3f..04769331383b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,8 +17,8 @@ env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" jobs: - # Preflight: establish routing truth and planner-owned matrices once, then let - # real work fan out from a single source of truth. + # Preflight: establish routing truth and job matrices once, then let real + # work fan out from a single source of truth. preflight: if: github.event_name != 'pull_request' || !github.event.pull_request.draft runs-on: blacksmith-16vcpu-ubuntu-2404 @@ -302,8 +302,6 @@ jobs: - name: Run ${{ matrix.task }} (${{ matrix.runtime }}) env: TASK: ${{ matrix.task }} - SHARD_COUNT: ${{ matrix.shard_count || '' }} - SHARD_INDEX: ${{ matrix.shard_index || '' }} shell: bash run: | set -euo pipefail @@ -312,10 +310,6 @@ jobs: pnpm test:bundled ;; extensions) - if [ -n "$SHARD_COUNT" ] && [ -n "$SHARD_INDEX" ]; then - export OPENCLAW_TEST_SHARDS="$SHARD_COUNT" - export OPENCLAW_TEST_SHARD_INDEX="$SHARD_INDEX" - fi pnpm test:extensions ;; contracts|contracts-protocol) @@ -363,21 +357,13 @@ jobs: if: (github.event_name != 'pull_request' || matrix.task != 'compat-node22') && matrix.runtime == 'node' && (matrix.task == 'test' || matrix.task == 'channels' || matrix.task == 'compat-node22') env: TASK: ${{ matrix.task }} - SHARD_COUNT: ${{ matrix.shard_count || '' }} - SHARD_INDEX: ${{ matrix.shard_index || '' }} run: | - # `pnpm test:planner` runs `scripts/test-parallel.mjs`, which spawns multiple Node processes. - # Default heap limits have been too low on Linux CI (V8 OOM near 4GB). - echo "OPENCLAW_TEST_WORKERS=2" >> "$GITHUB_ENV" - echo "OPENCLAW_TEST_MAX_OLD_SPACE_SIZE_MB=6144" >> "$GITHUB_ENV" + echo "NODE_OPTIONS=--max-old-space-size=6144" >> "$GITHUB_ENV" + echo "OPENCLAW_VITEST_MAX_WORKERS=2" >> "$GITHUB_ENV" if [ "$TASK" = "channels" ]; then - echo "OPENCLAW_TEST_WORKERS=1" >> "$GITHUB_ENV" + echo "OPENCLAW_VITEST_MAX_WORKERS=1" >> "$GITHUB_ENV" echo "OPENCLAW_TEST_ISOLATE=1" >> "$GITHUB_ENV" fi - if [ -n "$SHARD_COUNT" ] && [ -n "$SHARD_INDEX" ]; then - echo "OPENCLAW_TEST_SHARDS=$SHARD_COUNT" >> "$GITHUB_ENV" - echo "OPENCLAW_TEST_SHARD_INDEX=$SHARD_INDEX" >> "$GITHUB_ENV" - fi - name: Download dist artifact if: matrix.task == 'test' @@ -402,7 +388,7 @@ jobs: set -euo pipefail case "$TASK" in test) - pnpm test:planner + pnpm test ;; channels) pnpm test:channels @@ -738,8 +724,7 @@ jobs: env: NODE_OPTIONS: --max-old-space-size=6144 # Keep total concurrency predictable on the 32 vCPU runner. - # Windows shard 2 has shown intermittent instability at 2 workers. - OPENCLAW_TEST_WORKERS: 1 + OPENCLAW_VITEST_MAX_WORKERS: 1 defaults: run: shell: bash @@ -812,15 +797,6 @@ jobs: # caches can skip repeated rebuild/download work on later shards/runs. pnpm install --frozen-lockfile --prefer-offline --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true --config.side-effects-cache=true || pnpm install --frozen-lockfile --prefer-offline --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true --config.side-effects-cache=true - - name: Configure test shard (Windows) - if: matrix.task == 'test' - env: - SHARD_COUNT: ${{ matrix.shard_count }} - SHARD_INDEX: ${{ matrix.shard_index }} - run: | - echo "OPENCLAW_TEST_SHARDS=$SHARD_COUNT" >> "$GITHUB_ENV" - echo "OPENCLAW_TEST_SHARD_INDEX=$SHARD_INDEX" >> "$GITHUB_ENV" - - name: Download dist artifact if: matrix.task == 'test' uses: actions/download-artifact@v8 @@ -843,7 +819,7 @@ jobs: set -euo pipefail case "$TASK" in test) - pnpm test:planner + pnpm test ;; *) echo "Unsupported Windows checks task: $TASK" >&2 @@ -884,24 +860,17 @@ jobs: name: canvas-a2ui-bundle path: src/canvas-host/a2ui/ - - name: Configure test shard (macOS) - env: - SHARD_COUNT: ${{ matrix.shard_count }} - SHARD_INDEX: ${{ matrix.shard_index }} - run: | - echo "OPENCLAW_TEST_SHARDS=$SHARD_COUNT" >> "$GITHUB_ENV" - echo "OPENCLAW_TEST_SHARD_INDEX=$SHARD_INDEX" >> "$GITHUB_ENV" - - name: TS tests (macOS) env: NODE_OPTIONS: --max-old-space-size=4096 + OPENCLAW_VITEST_MAX_WORKERS: 2 TASK: ${{ matrix.task }} shell: bash run: | set -euo pipefail case "$TASK" in test) - pnpm test:planner + pnpm test ;; *) echo "Unsupported macOS node task: $TASK" >&2 diff --git a/package.json b/package.json index 64901d27a844..e05793295189 100644 --- a/package.json +++ b/package.json @@ -1047,13 +1047,13 @@ "test:all": "pnpm lint && pnpm build && pnpm test && pnpm test:e2e && pnpm test:live && pnpm test:docker:all", "test:auth:compat": "vitest run --config vitest.gateway.config.ts src/gateway/server.auth.compat-baseline.test.ts src/gateway/client.test.ts src/gateway/reconnect-gating.test.ts src/gateway/protocol/connect-error-details.test.ts", "test:build:singleton": "node scripts/test-built-plugin-singleton.mjs", - "test:bundled": "node scripts/test-parallel.mjs --surface bundled", + "test:bundled": "vitest run --config vitest.bundled.config.ts", "test:changed": "pnpm test -- --changed origin/main", - "test:changed:max": "node scripts/test-parallel.mjs --profile max --changed origin/main", - "test:channels": "node scripts/test-parallel.mjs --surface channels", + "test:changed:max": "OPENCLAW_VITEST_MAX_WORKERS=8 node scripts/test-projects.mjs --changed origin/main", + "test:channels": "vitest run --config vitest.channels.config.ts", "test:contracts": "pnpm test:contracts:channels && pnpm test:contracts:plugins", - "test:contracts:channels": "OPENCLAW_TEST_PROFILE=serial pnpm exec vitest run --config vitest.contracts.config.ts src/channels/plugins/contracts", - "test:contracts:plugins": "OPENCLAW_TEST_PROFILE=serial pnpm exec vitest run --config vitest.contracts.config.ts src/plugins/contracts", + "test:contracts:channels": "pnpm exec vitest run --config vitest.contracts.config.ts --maxWorkers=1 src/channels/plugins/contracts", + "test:contracts:plugins": "pnpm exec vitest run --config vitest.contracts.config.ts --maxWorkers=1 src/plugins/contracts", "test:coverage": "vitest run --config vitest.unit.config.ts --coverage", "test:coverage:changed": "vitest run --config vitest.unit.config.ts --coverage --changed origin/main", "test:docker:all": "pnpm test:docker:live-build && OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-models && OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-gateway && pnpm test:docker:openwebui && pnpm test:docker:onboard && pnpm test:docker:gateway-network && pnpm test:docker:mcp-channels && pnpm test:docker:qr && pnpm test:docker:doctor-switch && pnpm test:docker:plugins && pnpm test:docker:cleanup", @@ -1073,7 +1073,7 @@ "test:e2e": "vitest run --config vitest.e2e.config.ts", "test:e2e:openshell": "OPENCLAW_E2E_OPENSHELL=1 vitest run --config vitest.e2e.config.ts test/openshell-sandbox.e2e.test.ts", "test:extension": "node scripts/test-extension.mjs", - "test:extensions": "node scripts/test-parallel.mjs --surface extensions", + "test:extensions": "vitest run --config vitest.extensions.config.ts", "test:extensions:memory": "node scripts/profile-extension-memory.mjs", "test:fast": "vitest run --config vitest.unit.config.ts", "test:force": "node --import tsx scripts/test-force.ts", @@ -1086,25 +1086,20 @@ "test:live": "node scripts/test-live.mjs", "test:live:gateway-profiles": "node scripts/test-live.mjs -- src/gateway/gateway-models.profiles.live.test.ts", "test:live:models-profiles": "node scripts/test-live.mjs -- src/agents/models.profiles.live.test.ts", - "test:max": "node scripts/test-parallel.mjs --profile max", + "test:max": "OPENCLAW_VITEST_MAX_WORKERS=8 node scripts/test-projects.mjs", "test:parallels:linux": "bash scripts/e2e/parallels-linux-smoke.sh", "test:parallels:macos": "bash scripts/e2e/parallels-macos-smoke.sh", "test:parallels:npm-update": "bash scripts/e2e/parallels-npm-update-smoke.sh", "test:parallels:windows": "bash scripts/e2e/parallels-windows-smoke.sh", "test:perf:budget": "node scripts/test-perf-budget.mjs", "test:perf:hotspots": "node scripts/test-hotspots.mjs", - "test:perf:imports": "OPENCLAW_VITEST_IMPORT_DURATIONS=1 OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN=1 pnpm test", - "test:perf:imports:changed": "OPENCLAW_VITEST_IMPORT_DURATIONS=1 OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN=1 pnpm test -- --changed origin/main", + "test:perf:imports": "OPENCLAW_VITEST_IMPORT_DURATIONS=1 OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN=1 node scripts/test-projects.mjs", + "test:perf:imports:changed": "OPENCLAW_VITEST_IMPORT_DURATIONS=1 OPENCLAW_VITEST_PRINT_IMPORT_BREAKDOWN=1 node scripts/test-projects.mjs --changed origin/main", "test:perf:profile:main": "node scripts/run-vitest-profile.mjs main", "test:perf:profile:runner": "node scripts/run-vitest-profile.mjs runner", - "test:perf:update-memory-hotspots": "node scripts/test-update-memory-hotspots.mjs", - "test:perf:update-memory-hotspots:extensions": "node scripts/test-update-memory-hotspots.mjs --config vitest.extensions.config.ts --out test/fixtures/test-memory-hotspots.extensions.json --lane extensions --lane-prefix extensions-batch- --min-delta-kb 1048576 --limit 20", - "test:perf:update-timings": "node scripts/test-update-timings.mjs", - "test:perf:update-timings:extensions": "node scripts/test-update-timings.mjs --config vitest.extensions.config.ts", - "test:planner": "node scripts/test-parallel.mjs", "test:projects": "node scripts/test-projects.mjs", "test:sectriage": "pnpm exec vitest run --config vitest.gateway.config.ts && vitest run --config vitest.unit.config.ts --exclude src/daemon/launchd.integration.test.ts --exclude src/process/exec.test.ts", - "test:serial": "node scripts/test-parallel.mjs --profile serial", + "test:serial": "OPENCLAW_VITEST_MAX_WORKERS=1 node scripts/test-projects.mjs", "test:startup:bench": "node --import tsx scripts/bench-cli-startup.ts", "test:startup:bench:check": "node scripts/test-cli-startup-bench-budget.mjs", "test:startup:bench:save": "node --import tsx scripts/bench-cli-startup.ts --preset all --runs 5 --warmup 1 --output .artifacts/cli-startup-bench-all.json", diff --git a/scripts/ci-write-manifest-outputs.mjs b/scripts/ci-write-manifest-outputs.mjs index 428ea8194bcd..e21c16578a41 100644 --- a/scripts/ci-write-manifest-outputs.mjs +++ b/scripts/ci-write-manifest-outputs.mjs @@ -1,5 +1,6 @@ import { appendFileSync } from "node:fs"; -import { buildCIExecutionManifest } from "./test-planner/planner.mjs"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; const WORKFLOWS = new Set(["ci", "install-smoke"]); @@ -23,50 +24,159 @@ const parseArgs = (argv) => { return parsed; }; -const outputPath = process.env.GITHUB_OUTPUT; - -if (!outputPath) { - throw new Error("GITHUB_OUTPUT is required"); -} - -const { workflow } = parseArgs(process.argv.slice(2)); -const manifest = buildCIExecutionManifest(undefined, { env: process.env }); - -const writeOutput = (name, value) => { - appendFileSync(outputPath, `${name}=${value}\n`, "utf8"); +const parseBooleanEnv = (value, defaultValue = false) => { + if (value === undefined) { + return defaultValue; + } + const normalized = value.trim().toLowerCase(); + if (normalized === "true" || normalized === "1") { + return true; + } + if (normalized === "false" || normalized === "0" || normalized === "") { + return false; + } + return defaultValue; }; -if (workflow === "ci") { - writeOutput("docs_only", String(manifest.scope.docsOnly)); - writeOutput("docs_changed", String(manifest.scope.docsChanged)); - writeOutput("run_node", String(manifest.scope.runNode)); - writeOutput("run_macos", String(manifest.scope.runMacos)); - writeOutput("run_android", String(manifest.scope.runAndroid)); - writeOutput("run_skills_python", String(manifest.scope.runSkillsPython)); - writeOutput("run_windows", String(manifest.scope.runWindows)); - writeOutput("has_changed_extensions", String(manifest.scope.hasChangedExtensions)); - writeOutput("changed_extensions_matrix", JSON.stringify(manifest.scope.changedExtensionsMatrix)); - writeOutput("run_build_artifacts", String(manifest.jobs.buildArtifacts.enabled)); - writeOutput("run_checks_fast", String(manifest.jobs.checksFast.enabled)); - writeOutput("checks_fast_matrix", JSON.stringify(manifest.jobs.checksFast.matrix)); - writeOutput("run_checks", String(manifest.jobs.checks.enabled)); - writeOutput("checks_matrix", JSON.stringify(manifest.jobs.checks.matrix)); - writeOutput("run_extension_fast", String(manifest.jobs.extensionFast.enabled)); - writeOutput("extension_fast_matrix", JSON.stringify(manifest.jobs.extensionFast.matrix)); - writeOutput("run_check", String(manifest.jobs.check.enabled)); - writeOutput("run_check_additional", String(manifest.jobs.checkAdditional.enabled)); - writeOutput("run_build_smoke", String(manifest.jobs.buildSmoke.enabled)); - writeOutput("run_check_docs", String(manifest.jobs.checkDocs.enabled)); - writeOutput("run_skills_python_job", String(manifest.jobs.skillsPython.enabled)); - writeOutput("run_checks_windows", String(manifest.jobs.checksWindows.enabled)); - writeOutput("checks_windows_matrix", JSON.stringify(manifest.jobs.checksWindows.matrix)); - writeOutput("run_macos_node", String(manifest.jobs.macosNode.enabled)); - writeOutput("macos_node_matrix", JSON.stringify(manifest.jobs.macosNode.matrix)); - writeOutput("run_macos_swift", String(manifest.jobs.macosSwift.enabled)); - writeOutput("run_android_job", String(manifest.jobs.android.enabled)); - writeOutput("android_matrix", JSON.stringify(manifest.jobs.android.matrix)); - writeOutput("required_check_names", JSON.stringify(manifest.requiredCheckNames)); -} else if (workflow === "install-smoke") { - writeOutput("docs_only", String(manifest.scope.docsOnly)); - writeOutput("run_install_smoke", String(manifest.jobs.installSmoke.enabled)); +const parseJsonEnv = (value, fallback) => { + try { + return value ? JSON.parse(value) : fallback; + } catch { + return fallback; + } +}; + +const createMatrix = (include) => ({ include }); + +export function buildWorkflowManifest(env = process.env, workflow = "ci") { + const eventName = env.GITHUB_EVENT_NAME ?? "pull_request"; + const isPush = eventName === "push"; + const docsOnly = parseBooleanEnv(env.OPENCLAW_CI_DOCS_ONLY); + const docsChanged = parseBooleanEnv(env.OPENCLAW_CI_DOCS_CHANGED); + const runNode = parseBooleanEnv(env.OPENCLAW_CI_RUN_NODE); + const runMacos = parseBooleanEnv(env.OPENCLAW_CI_RUN_MACOS); + const runAndroid = parseBooleanEnv(env.OPENCLAW_CI_RUN_ANDROID); + const runWindows = parseBooleanEnv(env.OPENCLAW_CI_RUN_WINDOWS); + const runSkillsPython = parseBooleanEnv(env.OPENCLAW_CI_RUN_SKILLS_PYTHON); + const hasChangedExtensions = parseBooleanEnv(env.OPENCLAW_CI_HAS_CHANGED_EXTENSIONS); + const changedExtensionsMatrix = parseJsonEnv(env.OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX, { + include: [], + }); + const runChangedSmoke = parseBooleanEnv(env.OPENCLAW_CI_RUN_CHANGED_SMOKE); + + const checksFastMatrix = createMatrix( + runNode + ? [ + { check_name: "checks-fast-bundled", runtime: "node", task: "bundled" }, + { check_name: "checks-fast-extensions", runtime: "node", task: "extensions" }, + { + check_name: "checks-fast-contracts-protocol", + runtime: "node", + task: "contracts-protocol", + }, + ] + : [], + ); + + const checksMatrixInclude = runNode + ? [ + { check_name: "checks-node-test", runtime: "node", task: "test" }, + { check_name: "checks-node-channels", runtime: "node", task: "channels" }, + ...(isPush + ? [ + { + check_name: "checks-node-compat-node22", + runtime: "node", + task: "compat-node22", + node_version: "22.x", + cache_key_suffix: "node22", + }, + ] + : []), + ] + : []; + + const windowsMatrix = createMatrix( + runWindows ? [{ check_name: "checks-windows-node-test", runtime: "node", task: "test" }] : [], + ); + const macosNodeMatrix = createMatrix( + runMacos ? [{ check_name: "macos-node", runtime: "node", task: "test" }] : [], + ); + const androidMatrix = createMatrix( + runAndroid + ? [ + { check_name: "android-test-play", task: "test-play" }, + { check_name: "android-test-third-party", task: "test-third-party" }, + { check_name: "android-build-play", task: "build-play" }, + { check_name: "android-build-third-party", task: "build-third-party" }, + ] + : [], + ); + const extensionFastMatrix = createMatrix( + hasChangedExtensions + ? (changedExtensionsMatrix.include ?? []).map((entry) => ({ + check_name: `extension-fast-${entry.extension}`, + extension: entry.extension, + })) + : [], + ); + + if (workflow === "install-smoke") { + return { + docs_only: docsOnly, + run_install_smoke: !docsOnly && runChangedSmoke, + }; + } + + return { + docs_only: docsOnly, + docs_changed: docsChanged, + run_node: !docsOnly && runNode, + run_macos: !docsOnly && runMacos, + run_android: !docsOnly && runAndroid, + run_skills_python: !docsOnly && runSkillsPython, + run_windows: !docsOnly && runWindows, + has_changed_extensions: !docsOnly && hasChangedExtensions, + changed_extensions_matrix: changedExtensionsMatrix, + run_build_artifacts: !docsOnly && runNode, + run_checks_fast: !docsOnly && runNode, + checks_fast_matrix: checksFastMatrix, + run_checks: !docsOnly && runNode, + checks_matrix: createMatrix(checksMatrixInclude), + run_extension_fast: !docsOnly && hasChangedExtensions, + extension_fast_matrix: extensionFastMatrix, + run_check: !docsOnly && runNode, + run_check_additional: !docsOnly && runNode, + run_build_smoke: !docsOnly && runNode, + run_check_docs: docsChanged, + run_skills_python_job: !docsOnly && runSkillsPython, + run_checks_windows: !docsOnly && runWindows, + checks_windows_matrix: windowsMatrix, + run_macos_node: !docsOnly && runMacos, + macos_node_matrix: macosNodeMatrix, + run_macos_swift: !docsOnly && runMacos, + run_android_job: !docsOnly && runAndroid, + android_matrix: androidMatrix, + }; +} + +const entryHref = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : ""; + +if (import.meta.url === entryHref) { + const outputPath = process.env.GITHUB_OUTPUT; + + if (!outputPath) { + throw new Error("GITHUB_OUTPUT is required"); + } + + const { workflow } = parseArgs(process.argv.slice(2)); + const manifest = buildWorkflowManifest(process.env, workflow); + + const writeOutput = (name, value) => { + appendFileSync(outputPath, `${name}=${value}\n`, "utf8"); + }; + + for (const [key, value] of Object.entries(manifest)) { + writeOutput(key, typeof value === "string" ? value : JSON.stringify(value)); + } } diff --git a/scripts/pr-lib/gates.sh b/scripts/pr-lib/gates.sh index 24e6b16c40cd..7988418b7d4d 100644 --- a/scripts/pr-lib/gates.sh +++ b/scripts/pr-lib/gates.sh @@ -5,7 +5,7 @@ run_prepare_push_retry_gates() { run_quiet_logged "pnpm build (lease-retry)" ".local/lease-retry-build.log" pnpm build run_quiet_logged "pnpm check (lease-retry)" ".local/lease-retry-check.log" pnpm check if [ "$docs_only" != "true" ]; then - run_quiet_logged "pnpm test:planner (lease-retry)" ".local/lease-retry-test.log" pnpm test:planner + run_quiet_logged "pnpm test (lease-retry)" ".local/lease-retry-test.log" pnpm test fi } @@ -103,13 +103,11 @@ prepare_gates() { echo "Docs-only change detected with high confidence; skipping pnpm test." else gates_mode="full" - local prepare_unit_fast_batch_target_ms - prepare_unit_fast_batch_target_ms="${OPENCLAW_PREPARE_TEST_UNIT_FAST_BATCH_TARGET_MS:-5000}" - echo "Running pnpm test:planner with OPENCLAW_TEST_UNIT_FAST_BATCH_TARGET_MS=$prepare_unit_fast_batch_target_ms for shorter-lived unit-fast workers." + echo "Running pnpm test with OPENCLAW_VITEST_MAX_WORKERS=${OPENCLAW_VITEST_MAX_WORKERS:-4}." run_quiet_logged \ - "pnpm test:planner" \ + "pnpm test" \ ".local/gates-test.log" \ - env OPENCLAW_TEST_UNIT_FAST_BATCH_TARGET_MS="$prepare_unit_fast_batch_target_ms" pnpm test:planner + env OPENCLAW_VITEST_MAX_WORKERS="${OPENCLAW_VITEST_MAX_WORKERS:-4}" pnpm test previous_full_gates_head="$current_head" fi fi diff --git a/scripts/prepush-ci.sh b/scripts/prepush-ci.sh index e831d1b4300c..2f3b7b24a7e1 100644 --- a/scripts/prepush-ci.sh +++ b/scripts/prepush-ci.sh @@ -59,10 +59,10 @@ run_linux_ci_mirror() { run_step pnpm vitest run --config vitest.extensions.config.ts --maxWorkers=1 run_step env CI=true pnpm exec vitest run --config vitest.unit.config.ts --maxWorkers=1 - log_step "OPENCLAW_TEST_WORKERS=${OPENCLAW_TEST_WORKERS:-1} OPENCLAW_TEST_MAX_OLD_SPACE_SIZE_MB=${OPENCLAW_TEST_MAX_OLD_SPACE_SIZE_MB:-6144} pnpm test:planner" - OPENCLAW_TEST_WORKERS="${OPENCLAW_TEST_WORKERS:-1}" \ - OPENCLAW_TEST_MAX_OLD_SPACE_SIZE_MB="${OPENCLAW_TEST_MAX_OLD_SPACE_SIZE_MB:-6144}" \ - pnpm test:planner + log_step "OPENCLAW_VITEST_MAX_WORKERS=${OPENCLAW_VITEST_MAX_WORKERS:-1} NODE_OPTIONS=${NODE_OPTIONS:---max-old-space-size=6144} pnpm test" + OPENCLAW_VITEST_MAX_WORKERS="${OPENCLAW_VITEST_MAX_WORKERS:-1}" \ + NODE_OPTIONS="${NODE_OPTIONS:---max-old-space-size=6144}" \ + pnpm test } run_macos_ci_mirror() { diff --git a/scripts/test-cli-startup-bench-budget.mjs b/scripts/test-cli-startup-bench-budget.mjs index b20a97898d6a..50f35fd55e3c 100644 --- a/scripts/test-cli-startup-bench-budget.mjs +++ b/scripts/test-cli-startup-bench-budget.mjs @@ -2,7 +2,8 @@ import { spawnSync } from "node:child_process"; import fs from "node:fs"; import { floatFlag, intFlag, parseFlagArgs, readEnvNumber, stringFlag } from "./lib/arg-utils.mjs"; import { readJsonFile } from "./test-report-utils.mjs"; -import { cliStartupBenchManifestPath } from "./test-runner-manifest.mjs"; + +const CLI_STARTUP_BENCH_FIXTURE_PATH = "test/fixtures/cli-startup-bench.json"; function formatMs(value) { return `${value.toFixed(1)}ms`; @@ -42,7 +43,7 @@ if (process.argv.slice(2).includes("--help")) { const opts = parseFlagArgs( process.argv.slice(2), { - baseline: cliStartupBenchManifestPath, + baseline: CLI_STARTUP_BENCH_FIXTURE_PATH, report: "", entry: "openclaw.mjs", preset: "all", diff --git a/scripts/test-extension.mjs b/scripts/test-extension.mjs index 75aa30231b2d..c02d5f282046 100644 --- a/scripts/test-extension.mjs +++ b/scripts/test-extension.mjs @@ -9,13 +9,11 @@ import { BUNDLED_PLUGIN_PATH_PREFIX, BUNDLED_PLUGIN_ROOT_DIR, } from "./lib/bundled-plugin-paths.mjs"; -import { loadTestRunnerBehavior } from "./test-runner-manifest.mjs"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const repoRoot = path.resolve(__dirname, ".."); const pnpm = "pnpm"; -const testRunnerBehavior = loadTestRunnerBehavior(); function runGit(args, options = {}) { return execFileSync("git", args, { @@ -236,42 +234,15 @@ export function resolveExtensionTestPlan(params = {}) { const testFiles = roots .flatMap((root) => collectTestFiles(path.join(repoRoot, root))) .map((filePath) => normalizeRelative(path.relative(repoRoot, filePath))); - const { isolatedTestFiles, sharedTestFiles } = partitionExtensionTestFiles({ config, testFiles }); - return { config, extensionDir: relativeExtensionDir, extensionId, - isolatedTestFiles, roots, - sharedTestFiles, testFiles, }; } -export function partitionExtensionTestFiles(params) { - const testFiles = params.testFiles.map((filePath) => normalizeRelative(filePath)); - let isolatedEntries = []; - let isolatedPrefixes = []; - - if (params.config === "vitest.channels.config.ts") { - isolatedEntries = testRunnerBehavior.channels.isolated; - isolatedPrefixes = testRunnerBehavior.channels.isolatedPrefixes; - } else if (params.config === "vitest.extensions.config.ts") { - isolatedEntries = testRunnerBehavior.extensions.isolated; - } - - const isolatedEntrySet = new Set(isolatedEntries.map((entry) => entry.file)); - const isolatedTestFiles = testFiles.filter( - (file) => - isolatedEntrySet.has(file) || isolatedPrefixes.some((prefix) => file.startsWith(prefix)), - ); - const isolatedTestFileSet = new Set(isolatedTestFiles); - const sharedTestFiles = testFiles.filter((file) => !isolatedTestFileSet.has(file)); - - return { isolatedTestFiles, sharedTestFiles }; -} - async function runVitestBatch(params) { return await new Promise((resolve, reject) => { const child = spawn( @@ -412,8 +383,6 @@ async function run() { console.log(`config: ${plan.config}`); console.log(`roots: ${plan.roots.join(", ")}`); console.log(`tests: ${plan.testFiles.length}`); - console.log(`shared: ${plan.sharedTestFiles.length}`); - console.log(`isolated: ${plan.isolatedTestFiles.length}`); } return; } @@ -425,36 +394,13 @@ async function run() { console.log( `[test-extension] Running ${plan.testFiles.length} test files for ${plan.extensionId} with ${plan.config}`, ); - - if (plan.sharedTestFiles.length > 0 && plan.isolatedTestFiles.length > 0) { - console.log( - `[test-extension] Split into ${plan.sharedTestFiles.length} shared and ${plan.isolatedTestFiles.length} isolated files`, - ); - } - - if (plan.sharedTestFiles.length > 0) { - const sharedExitCode = await runVitestBatch({ - args: passthroughArgs, - config: plan.config, - env: process.env, - files: plan.sharedTestFiles, - }); - if (sharedExitCode !== 0) { - process.exit(sharedExitCode); - } - } - - if (plan.isolatedTestFiles.length > 0) { - const isolatedExitCode = await runVitestBatch({ - args: passthroughArgs, - config: plan.config, - env: { ...process.env, OPENCLAW_TEST_ISOLATE: "1" }, - files: plan.isolatedTestFiles, - }); - process.exit(isolatedExitCode); - } - - process.exit(0); + const exitCode = await runVitestBatch({ + args: passthroughArgs, + config: plan.config, + env: process.env, + files: plan.testFiles, + }); + process.exit(exitCode); } const entryHref = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : ""; diff --git a/scripts/test-parallel-memory.mjs b/scripts/test-parallel-memory.mjs deleted file mode 100644 index d56b1e9259dd..000000000000 --- a/scripts/test-parallel-memory.mjs +++ /dev/null @@ -1,283 +0,0 @@ -import { spawnSync } from "node:child_process"; -import fs from "node:fs"; -import path from "node:path"; - -const ESCAPE = String.fromCodePoint(27); -const BELL = String.fromCodePoint(7); -const ANSI_ESCAPE_PATTERN = new RegExp( - // Strip CSI/OSC-style control sequences from Vitest output before parsing file lines. - `${ESCAPE}(?:\\][^${BELL}]*(?:${BELL}|${ESCAPE}\\\\)|\\[[0-?]*[ -/]*[@-~]|[@-Z\\\\-_])`, - "g", -); -const GITHUB_CLI_LOG_PREFIX_PATTERN = - /^[^\t\r\n]+\t[^\t\r\n]+\t\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\s+/u; -const GITHUB_ACTIONS_LOG_PREFIX_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\s+/u; - -const COMPLETED_TEST_FILE_LINE_PATTERN = - /(?(?:src|extensions|test|ui)\/\S+?\.(?:live\.test|e2e\.test|test)\.ts)\s+\(.*\)\s+(?\d+(?:\.\d+)?)(?ms|s)\s*$/; -const MEMORY_TRACE_SUMMARY_PATTERN = - /^\[test-parallel\]\[mem\] summary (?\S+) files=(?\d+) peak=(?[0-9]+(?:\.[0-9]+)?(?:GiB|MiB|KiB)) totalDelta=(?[+-]?[0-9]+(?:\.[0-9]+)?(?:GiB|MiB|KiB)) peakAt=(?\S+) top=(?.*)$/u; -const MEMORY_TRACE_TOP_ENTRY_PATTERN = - /^(?(?:src|extensions|test|ui)\/\S+?\.(?:live\.test|e2e\.test|test)\.ts):(?[+-]?[0-9]+(?:\.[0-9]+)?(?:GiB|MiB|KiB))$/u; - -const PS_COLUMNS = ["pid=", "ppid=", "rss=", "comm="]; - -function parseDurationMs(rawValue, unit) { - const parsed = Number.parseFloat(rawValue); - if (!Number.isFinite(parsed)) { - return null; - } - return unit === "s" ? Math.round(parsed * 1000) : Math.round(parsed); -} - -export function parseMemoryValueKb(rawValue) { - const match = rawValue.match(/^(?[+-]?)(?\d+(?:\.\d+)?)(?GiB|MiB|KiB)$/u); - if (!match?.groups) { - return null; - } - const value = Number.parseFloat(match.groups.value); - if (!Number.isFinite(value)) { - return null; - } - const multiplier = - match.groups.unit === "GiB" ? 1024 ** 2 : match.groups.unit === "MiB" ? 1024 : 1; - const signed = Math.round(value * multiplier); - return match.groups.sign === "-" ? -signed : signed; -} - -function stripAnsi(text) { - return text.replaceAll(ANSI_ESCAPE_PATTERN, ""); -} - -function normalizeLogLine(line) { - return line - .replace(GITHUB_CLI_LOG_PREFIX_PATTERN, "") - .replace(GITHUB_ACTIONS_LOG_PREFIX_PATTERN, ""); -} - -export function parseCompletedTestFileLines(text) { - return stripAnsi(text) - .split(/\r?\n/u) - .map((line) => normalizeLogLine(line)) - .map((line) => { - const match = line.match(COMPLETED_TEST_FILE_LINE_PATTERN); - if (!match?.groups) { - return null; - } - return { - file: match.groups.file, - durationMs: parseDurationMs(match.groups.duration, match.groups.unit), - }; - }) - .filter((entry) => entry !== null); -} - -export function parseMemoryTraceSummaryLines(text) { - return stripAnsi(text) - .split(/\r?\n/u) - .map((line) => normalizeLogLine(line)) - .map((line) => { - const match = line.match(MEMORY_TRACE_SUMMARY_PATTERN); - if (!match?.groups) { - return null; - } - const peakRssKb = parseMemoryValueKb(match.groups.peak); - const totalDeltaKb = parseMemoryValueKb(match.groups.totalDelta); - const fileCount = Number.parseInt(match.groups.files, 10); - if (!Number.isInteger(fileCount) || peakRssKb === null || totalDeltaKb === null) { - return null; - } - const top = - match.groups.top === "none" - ? [] - : match.groups.top - .split(/,\s+/u) - .map((entry) => { - const topMatch = entry.match(MEMORY_TRACE_TOP_ENTRY_PATTERN); - if (!topMatch?.groups) { - return null; - } - const deltaKb = parseMemoryValueKb(topMatch.groups.delta); - if (deltaKb === null) { - return null; - } - return { - file: topMatch.groups.file, - deltaKb, - }; - }) - .filter((entry) => entry !== null); - return { - lane: match.groups.lane, - files: fileCount, - peakRssKb, - totalDeltaKb, - peakAt: match.groups.peakAt, - top, - }; - }) - .filter((entry) => entry !== null); -} - -export function getProcessTreeRecords(rootPid) { - if (!Number.isInteger(rootPid) || rootPid <= 0 || process.platform === "win32") { - return null; - } - - const result = spawnSync("ps", ["-axo", PS_COLUMNS.join(",")], { - encoding: "utf8", - }); - if (result.status !== 0 || result.error) { - return null; - } - - const childPidsByParent = new Map(); - const recordsByPid = new Map(); - for (const line of result.stdout.split(/\r?\n/u)) { - const trimmed = line.trim(); - if (!trimmed) { - continue; - } - const [pidRaw, parentRaw, rssRaw, commandRaw] = trimmed.split(/\s+/u, 4); - const pid = Number.parseInt(pidRaw ?? "", 10); - const parentPid = Number.parseInt(parentRaw ?? "", 10); - const rssKb = Number.parseInt(rssRaw ?? "", 10); - if (!Number.isInteger(pid) || !Number.isInteger(parentPid) || !Number.isInteger(rssKb)) { - continue; - } - const siblings = childPidsByParent.get(parentPid) ?? []; - siblings.push(pid); - childPidsByParent.set(parentPid, siblings); - recordsByPid.set(pid, { - pid, - parentPid, - rssKb, - command: commandRaw ?? "", - }); - } - - if (!recordsByPid.has(rootPid)) { - return null; - } - - const queue = [rootPid]; - const visited = new Set(); - const records = []; - while (queue.length > 0) { - const pid = queue.shift(); - if (pid === undefined || visited.has(pid)) { - continue; - } - visited.add(pid); - const record = recordsByPid.get(pid); - if (record) { - records.push(record); - } - for (const childPid of childPidsByParent.get(pid) ?? []) { - if (!visited.has(childPid)) { - queue.push(childPid); - } - } - } - - return records; -} - -export function sampleProcessTreeRssKb(rootPid) { - const records = getProcessTreeRecords(rootPid); - if (!records) { - return null; - } - - let rssKb = 0; - let processCount = 0; - for (const record of records) { - rssKb += record.rssKb; - processCount += 1; - } - - return { rssKb, processCount }; -} - -const REPORT_FILE_PATTERN = - /^report\.(?\d+)\.(?