diff --git a/docs/ci.md b/docs/ci.md index 6026190c668f..a678786123af 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -227,7 +227,11 @@ Hybrid is the normal degraded-capacity mode. If Blacksmith is down: rerun the fa gh variable set OPENCLAW_CI_RUNNER_BACKEND --repo openclaw/openclaw --body github ``` -Hosted paths use the same setup exercised by manual dispatches and fork pull requests. Blacksmith-only Docker and sticky-disk steps are skipped, dependency setup uses the ordinary Actions pnpm-store cache, and low-memory Android builds use separate Gradle processes. Hybrid attempt-1 Node and plateau lanes deliberately keep this backend-neutral Actions-cache profile when they run on Blacksmith because preflight remains hosted. The exact workspace dependency cache intentionally stays off when preflight is hosted: GitHub can roll the runner image and Node patch between preflight and fanout in one workflow, while the safe exact key then misses; the ordinary store archive is only slightly smaller and pnpm's measured relink is already single-digit seconds. Vitest transform and Node compile caches still use the upstream Actions cache API, which Blacksmith proxies; their Linux-only `runner.os != 'Windows'` conditions do not exclude Blacksmith labels, and the planner still elects one semantic transform-cache writer. Core oxlint splits into five deterministic hosted stripes, with extension/scripts lint and optional UI/format checks in the existing `check-lint` row. The 14 serial core test-type graphs likewise split into five `check-test-types-core-*` stripes (two overlapped graphs per stripe), leaving the extensions/root/scripts type tail in the `check-test-types` row; targets without stripe support keep the whole lane in that row. The compact Node planner keeps separate Blacksmith and standard 4-core hosted hint tables, targets 90/95 seconds of serial group work for the hosted large/small source runner classes, and applies a 1.6x median scaling fallback only to unmeasured groups. Failed and timeout-and-retry samples are excluded from refreshes. Groups above 150 predicted seconds split into file-weighted hosted stripes before packing; whole-config groups with registered file listers (agent support, gateway methods, runtime config, isolated unit fast) split the same way. The subprocess-heavy tooling family uses seven balanced stripes projecting to roughly 115 seconds per stripe. The resulting 70 push descriptors and 79 pull-request fallback descriptors stay within the 96-row hosted cap, and every nondist lane is at or below 149 predicted test seconds; the serial TUI PTY dist descriptor keeps its indivisible measured wall. In `github` mode compact jobs run hosted; hybrid attempt 1 uses each row's 4-vCPU or 8-vCPU Blacksmith label, while hybrid retries use hosted runners. The corresponding all-Blacksmith plans contain 25 and 34 total descriptors. Control UI E2E expands from the unchanged all-Blacksmith shape of three Vitest shards plus one browser-extension shard to eleven Vitest shards plus one browser-extension shard for the `github` and `hybrid` planner profiles. QA Smoke similarly expands from four parts to six. Hybrid attempt 1 runs those expanded matrices on Blacksmith; `github` mode and hybrid retries use hosted runners. QA's planner reserves the final part's observed roughly two-minute Matrix rider before greedily assigning primary scenarios, keeping that separate run from becoming the tail. Windows expands from the single Blacksmith job to three hosted jobs with disjoint, project-aligned explicit test lists only in `github` mode and on dispatches, whose runner expression forces hosted `windows-2025` in every mode. This partitions the complete Windows-specific inventory without applying Vitest `--shard` to project-local single-file selections, which Vitest rejects. Hybrid push and pull-request runs keep the single `blacksmith-8vcpu-windows-2025` lane because that runner class admits about two concurrent jobs, so a three-part split queues its tail behind a finished part; a hybrid retry reruns that single lane on hosted `windows-2025`, slower but bounded. Expect slower individual builds on standard 4-core hosted runners. Blacksmith's runner-registration budget is irrelevant for hosted jobs, but GitHub-hosted concurrency limits apply. +Hosted paths use the same setup exercised by manual dispatches and fork pull requests. Blacksmith-only Docker and sticky-disk steps are skipped, dependency setup uses the ordinary Actions pnpm-store cache, and low-memory Android builds use separate Gradle processes. Hybrid attempt-1 Node and plateau lanes deliberately keep this backend-neutral Actions-cache profile when they run on Blacksmith because preflight remains hosted. The exact workspace dependency cache intentionally stays off when preflight is hosted: GitHub can roll the runner image and Node patch between preflight and fanout in one workflow, while the safe exact key then misses; the ordinary store archive is only slightly smaller and pnpm's measured relink is already single-digit seconds. Vitest transform and Node compile caches still use the upstream Actions cache API, which Blacksmith proxies; their Linux-only `runner.os != 'Windows'` conditions do not exclude Blacksmith labels, and the planner still elects one semantic transform-cache writer. Core oxlint splits into five deterministic hosted stripes, with extension/scripts lint and optional UI and format checks in the existing `check-lint` row. The 14 serial core test-type graphs likewise split into five `check-test-types-core-*` stripes (two overlapped graphs per stripe), leaving the extensions/root/scripts type tail in the `check-test-types` row; targets without stripe support keep the whole lane in that row. + +The compact Node planner keeps separate Blacksmith and standard 4-core hosted timing ownership. The `github` profile targets 90/95 seconds of serial group work for the hosted large/small source runner classes and applies a 1.6x median scaling fallback only to unmeasured groups. Hybrid keeps the expanded topology and hosted-derived splitting for groups above the unchanged 150-second ceiling, so hosted retries do not inherit an indivisible hosted wall pole. Its attempt-1 packing scales the Blacksmith hints by 0.87: the unscaled all-Blacksmith push plan totals 4,723 predicted seconds, while complete Blacksmith runs `31945998653` and `31949756966` measured 3,742.046 and 3,756.674 seconds of shard work (79.230% and 79.540%). The 0.87 factor is 9.379% above the higher observed ratio. With the direct outlier hints below, 0.85 yields 45 nondist jobs and 0.86 is the first two-decimal factor yielding 46; 0.87 stays one point above that rounding cliff while preserving the 46-job plan. Direct sampled hints cover the doctor and cron-service outliers, while a 140-second floor and singleton bin protect the observed `agentic-gateway-core-3` tail. During rebalance, native hybrid groups use scaled Blacksmith stripe hints; only synthesized hosted stripes retain their divided parent weight. Failed and timeout-and-retry samples are excluded from refreshes. Whole-config groups with registered file listers (agent support, gateway methods, runtime config, isolated unit fast) split into file-weighted hosted stripes. The subprocess-heavy tooling family uses seven balanced stripes projecting to roughly 115 hosted seconds per stripe. + +The `github` profile contains 70 push descriptors and 79 pull-request fallback descriptors, with every nondist lane at or below 149 predicted test seconds; the serial TUI PTY dist descriptor keeps its indivisible measured wall. Hybrid contains 47 push descriptors (46 nondist jobs) and 55 pull-request fallback descriptors (53 nondist jobs), with a 140-second predicted maximum. In `github` mode compact jobs run hosted; hybrid attempt 1 uses each row's 4-vCPU or 8-vCPU Blacksmith label, while hybrid retries use hosted runners. The corresponding all-Blacksmith plans contain 25 and 34 total descriptors. Control UI E2E expands from the unchanged all-Blacksmith shape of three Vitest shards plus one browser-extension shard to eleven Vitest shards plus one browser-extension shard for the `github` and `hybrid` planner profiles. QA Smoke similarly expands from four parts to six. Hybrid attempt 1 runs those expanded matrices on Blacksmith; `github` mode and hybrid retries use hosted runners. QA's planner reserves the final part's observed roughly two-minute Matrix rider before greedily assigning primary scenarios, keeping that separate run from becoming the tail. Windows expands from the single Blacksmith job to three hosted jobs with disjoint, project-aligned explicit test lists only in `github` mode and on dispatches, whose runner expression forces hosted `windows-2025` in every mode. This partitions the complete Windows-specific inventory without applying Vitest `--shard` to project-local single-file selections, which Vitest rejects. Hybrid push and pull-request runs keep the single `blacksmith-8vcpu-windows-2025` lane because that runner class admits about two concurrent jobs, so a three-part split queues its tail behind a finished part; a hybrid retry reruns that single lane on hosted `windows-2025`, slower but bounded. Expect slower individual builds on standard 4-core hosted runners. Blacksmith's runner-registration budget is irrelevant for hosted jobs, but GitHub-hosted concurrency limits apply. Restore all-Blacksmith routing after an outage by deleting the variable: @@ -255,7 +259,7 @@ target below about 60% of the live bucket. With the current 10,000-registration bucket, that means a 6,000-registration operating target, leaving headroom for concurrent repositories, retries, and burst overlap. -The changed-target PR plan reduces the common Node test burst from 29 Blacksmith registrations to one. Broad-risk PRs keep the 32-registration metadata-complete compact fallback; canonical pushes use 24 nondist compact registrations. The `github` and `hybrid` planner profile can emit up to 96 compact rows. In `github` mode those rows are hosted; in hybrid they consume at most 96 Blacksmith registrations on attempt 1 and move to hosted capacity on retries. With roughly 25 other Blacksmith lanes, one full run registers about 121 runners; four admitted runs in a five-minute window register about 484, far below the 6,000-registration operating target. +The changed-target PR plan reduces the common Node test burst from 29 Blacksmith registrations to one. Broad-risk all-Blacksmith PRs keep the 32-registration metadata-complete compact fallback; canonical all-Blacksmith pushes use 24 nondist compact registrations. Hybrid currently uses 53 nondist fallback rows and 46 nondist push rows. The `github` and `hybrid` planner profiles remain capped at 96 compact rows; `github` rows are hosted, while hybrid rows consume Blacksmith registrations on attempt 1 and move to hosted capacity on retries. Even the 96-row cap plus roughly 25 other Blacksmith lanes is about 121 registrations per full run, or 484 for four admitted runs in a five-minute window, far below the 6,000-registration operating target. Canonical-repo CI keeps Blacksmith as the default runner path for pushes and first-attempt same-repo pull-request runs when the backend is unset or `blacksmith`. Hybrid keeps the heavy set plus the named critical-path plateau lanes on Blacksmith for attempt 1; other light lanes and every rerun Blacksmith lane use GitHub-hosted capacity. Pull-request retries of both UI E2E jobs use GitHub-hosted Ubuntu in every mode; push retries remain on their normal backend unless hybrid fallback applies. All `workflow_dispatch` runs, including `release_gate`, and non-canonical repository runs use GitHub-hosted runners. The [`github` backend](#runner-backend-modes) provides a manual repository-wide fallback; canonical runs do not probe Blacksmith queue health or mutate the variable automatically. diff --git a/scripts/lib/ci-node-test-plan.mts b/scripts/lib/ci-node-test-plan.mts index 8afcd5d2505a..5839c6d7fc1d 100644 --- a/scripts/lib/ci-node-test-plan.mts +++ b/scripts/lib/ci-node-test-plan.mts @@ -165,19 +165,18 @@ const COMPACT_EMBEDDED_GROUP_NAMES = [ const MAX_BUNDLED_NODE_TEST_PATTERNS = 64; // Compact bundles trade a little serial work for fewer ephemeral runner registrations. // Keep runner classes and subprocess isolation intact while bounding each combined job. -// The group hints below are loaded-fleet CI walls. Three-way striping plus a -// Blacksmith keeps the proven 200s/276s admission caps and 28-worker ceiling. -// Standard 4-core GitHub runners use direct hosted wall hints below. These -// budgets target roughly 160 seconds of predicted body work, leaving the -// measured ~70-second setup overhead inside a roughly 230-second lane. +// Default Blacksmith plans pack the Blacksmith base hints with 200s/276s +// admission caps. GitHub-hosted plans use direct hosted hints with 90s/95s +// packing caps. Hybrid keeps the expanded topology but packs its attempt-1 +// Blacksmith rows with the refit Blacksmith estimates below. const COMPACT_LARGE_NODE_TEST_JOB_SECONDS = 200; const COMPACT_SMALL_NODE_TEST_JOB_SECONDS = 276; const COMPACT_GITHUB_LARGE_NODE_TEST_JOB_SECONDS = 90; const COMPACT_GITHUB_SMALL_NODE_TEST_JOB_SECONDS = 95; const COMPACT_GITHUB_GROUP_SECONDS_SCALE = 1.6; -// Wall-time ceiling per hosted lane: with ~70s of fixed setup, a 150-second -// body keeps every Node lane inside a ~220-second job. The former 210-second -// ceiling let single fat groups own ~280-second critical paths. +const COMPACT_HYBRID_GROUP_SECONDS_SCALE = 0.87; +// Split groups above this hosted prediction before packing. Hybrid reuses the +// hosted-derived splits so retries cannot reunite an oversized hosted group. const COMPACT_GITHUB_MAX_PREDICTED_SECONDS = 150; const COMPACT_GITHUB_NODE_TEST_JOB_CAP = 96; const COMPACT_NODE_TEST_JOB_GROUPS = 10; @@ -530,6 +529,14 @@ const COMPACT_GITHUB_GROUP_SECONDS_HINTS = new Map([ ["core-unit-support", 32], ]); +// Hybrid-specific Blacksmith observations from 31949756966, plus the +// gateway-core-3 139.5s spike in 31938297538 that must stay singleton. +const COMPACT_HYBRID_GROUP_SECONDS_HINTS = new Map([ + ["agentic-commands-doctor", 64], + ["agentic-gateway-core-3", 140], + ["core-runtime-cron-service", 80], +]); + // Advisory per-file wall-clock hints (seconds) for stripe balancing, measured // from single-file local runs (M4 Max) and static import-graph size. Packing // only: a stale entry skews stripe balance but never correctness. Unlisted @@ -631,16 +638,33 @@ function estimateDefaultCompactGroupSeconds(group: NodeTestShardGroup): number { return DEFAULT_WHOLE_GROUP_SECONDS; } -function usesGithubRunnerProfile(runnerBackend: string | undefined): boolean { +function usesExpandedRunnerProfile(runnerBackend: string | undefined): boolean { return runnerBackend === "github" || runnerBackend === "hybrid"; } +function estimateHybridCompactGroupSeconds(group: NodeTestShardGroup, seconds: number): number { + // The 4,723s Blacksmith push hint sum measured 3,742.046s/3,756.674s + // (79.230%/79.540%) in runs 31945998653/31949756966. A 0.87 scale keeps + // 9.379% headroom above the higher ratio. With direct outlier hints, it sits + // one point above the 0.86 packing cliff. + return ( + COMPACT_HYBRID_GROUP_SECONDS_HINTS.get(group.shard_name) ?? + Math.round(seconds * COMPACT_HYBRID_GROUP_SECONDS_SCALE) + ); +} + function estimateCompactGroupSeconds( group: NodeTestShardGroup, runnerBackend: string | undefined, ): number { const defaultSeconds = estimateDefaultCompactGroupSeconds(group); - if (!usesGithubRunnerProfile(runnerBackend)) { + // Hybrid attempt 1 runs on Blacksmith. It keeps the expanded topology for + // hosted retries, but its packing weights must describe the runner that + // normally executes the plan. + if (runnerBackend === "hybrid") { + return estimateHybridCompactGroupSeconds(group, defaultSeconds); + } + if (runnerBackend !== "github") { return defaultSeconds; } return ( @@ -653,13 +677,15 @@ function estimateCompactStripeSeconds( group: NodeTestShardGroup, runnerBackend: string | undefined, ): number { - if (usesGithubRunnerProfile(runnerBackend)) { + if (runnerBackend === "github") { return estimateCompactGroupSeconds(group, runnerBackend); } - return ( + const blacksmithSeconds = COMPACT_LARGE_GROUP_STRIPE_SECONDS_HINTS.get(group.shard_name) ?? - estimateDefaultCompactGroupSeconds(group) - ); + estimateDefaultCompactGroupSeconds(group); + return runnerBackend === "hybrid" + ? estimateHybridCompactGroupSeconds(group, blacksmithSeconds) + : blacksmithSeconds; } // Equal-weight sibling stripes can otherwise land in one bin and recreate the @@ -2111,20 +2137,24 @@ const WHOLE_CONFIG_SPLIT_FILE_LISTERS = new Map string[]>([ function splitOversizedGithubCompactGroup( group: NodeTestShardGroup, + runnerBackend: string | undefined, ): Array<{ group: NodeTestShardGroup; seconds: number }> { - const seconds = estimateCompactGroupSeconds(group, "github"); - if (seconds <= COMPACT_GITHUB_MAX_PREDICTED_SECONDS) { - return [{ group, seconds }]; + // Hybrid retries run hosted, so retain hosted-derived striping even though + // Blacksmith timings own its attempt-1 packing weights. + const githubSeconds = estimateCompactGroupSeconds(group, "github"); + const profileSeconds = estimateCompactGroupSeconds(group, runnerBackend); + if (githubSeconds <= COMPACT_GITHUB_MAX_PREDICTED_SECONDS) { + return [{ group, seconds: profileSeconds }]; } const includePatterns = group.includePatterns ?? WHOLE_CONFIG_SPLIT_FILE_LISTERS.get(group.shard_name)?.(); if (!includePatterns || includePatterns.length === 0) { - return [{ group, seconds }]; + return [{ group, seconds: profileSeconds }]; } - const stripeCount = Math.ceil(seconds / COMPACT_GITHUB_MAX_PREDICTED_SECONDS); - const splitSeconds = Math.ceil(seconds / stripeCount); + const stripeCount = Math.ceil(githubSeconds / COMPACT_GITHUB_MAX_PREDICTED_SECONDS); + const splitSeconds = Math.ceil(profileSeconds / stripeCount); return createStripedBatches(includePatterns, stripeCount, stripeFileWeight).map( (patterns, index) => ({ group: { @@ -2145,7 +2175,7 @@ function createCompactNodeTestShardBundles( (shard) => compactMode !== "push" || !COMPACT_PUSH_EXCLUDED_SHARDS.has(shard.shardName), ); const groupsByRunner = new Map(); - const hostedSplitSeconds = new Map(); + const synthesizedSplitSeconds = new Map(); for (const shard of shards) { const runner = resolveCiNodeTestRunner(shard); @@ -2159,13 +2189,15 @@ function createCompactNodeTestShardBundles( runner, shard_name: shard.shardName, }); - const plannedGroups = usesGithubRunnerProfile(options.runnerBackend) - ? splitOversizedGithubCompactGroup(group) + const plannedGroups = usesExpandedRunnerProfile(options.runnerBackend) + ? splitOversizedGithubCompactGroup(group, options.runnerBackend) : [{ group, seconds: estimateCompactGroupSeconds(group, options.runnerBackend) }]; for (const planned of plannedGroups) { groups.push(planned.group); - if (usesGithubRunnerProfile(options.runnerBackend)) { - hostedSplitSeconds.set(planned.group.shard_name, planned.seconds); + // Synthesized hosted stripes need their divided parent weight. Native + // groups must reach the runner-specific stripe estimator during rebalance. + if (planned.group.shard_name !== group.shard_name) { + synthesizedSplitSeconds.set(planned.group.shard_name, planned.seconds); } } groupsByRunner.set(key, groups); @@ -2173,10 +2205,10 @@ function createCompactNodeTestShardBundles( const compactJobs: CompactNodeTestShard[] = []; const estimateGroupSeconds = (group: NodeTestShardGroup) => - hostedSplitSeconds.get(group.shard_name) ?? + synthesizedSplitSeconds.get(group.shard_name) ?? estimateCompactGroupSeconds(group, options.runnerBackend); const estimateStripeSeconds = (group: NodeTestShardGroup) => - hostedSplitSeconds.get(group.shard_name) ?? + synthesizedSplitSeconds.get(group.shard_name) ?? estimateCompactStripeSeconds(group, options.runnerBackend); for (const groups of groupsByRunner.values()) { // First-fit decreasing sets the existing registration count from the @@ -2192,7 +2224,7 @@ function createCompactNodeTestShardBundles( const exclusive = isExclusiveCompactGroup(group); const secondsCap = exclusive ? COMPACT_EXCLUSIVE_JOB_SECONDS - : usesGithubRunnerProfile(options.runnerBackend) + : usesExpandedRunnerProfile(options.runnerBackend) ? group.runner.includes("-8vcpu-") ? COMPACT_GITHUB_LARGE_NODE_TEST_JOB_SECONDS : COMPACT_GITHUB_SMALL_NODE_TEST_JOB_SECONDS @@ -2280,7 +2312,7 @@ function createCompactNodeTestShardBundles( } if ( - usesGithubRunnerProfile(options.runnerBackend) && + usesExpandedRunnerProfile(options.runnerBackend) && compactJobs.length > COMPACT_GITHUB_NODE_TEST_JOB_CAP ) { throw new Error( diff --git a/test/scripts/ci-node-test-plan.test.ts b/test/scripts/ci-node-test-plan.test.ts index f7e4b28e4328..743a20602716 100644 --- a/test/scripts/ci-node-test-plan.test.ts +++ b/test/scripts/ci-node-test-plan.test.ts @@ -313,20 +313,82 @@ describe("scripts/lib/ci-node-test-plan.mts", () => { "core-tooling-isolated", ]); - // Pushes retain three lanes of headroom under the workflow's 28-worker cap. - expect(compact).toHaveLength(25); - expect(pullRequestCompact).toHaveLength(34); - expect(githubCompact).toHaveLength(70); - expect(githubPullRequestCompact).toHaveLength(79); - expect(hybridCompact).toEqual(githubCompact); - expect(hybridPullRequestCompact).toEqual(githubPullRequestCompact); + for (const profile of [ + { + name: "Blacksmith", + pullRequest: pullRequestCompact, + pullRequestJobs: 34, + pullRequestMax: 204, + push: compact, + pushJobs: 25, + pushMax: 204, + }, + { + name: "GitHub-hosted", + pullRequest: githubPullRequestCompact, + pullRequestJobs: 79, + pullRequestMax: 186, + push: githubCompact, + pushJobs: 70, + pushMax: 149, + }, + { + name: "hybrid", + pullRequest: hybridPullRequestCompact, + pullRequestJobs: 55, + pullRequestMax: 140, + push: hybridCompact, + pushJobs: 47, + pushMax: 140, + }, + ]) { + expect(profile.push, `${profile.name} push jobs`).toHaveLength(profile.pushJobs); + expect(profile.pullRequest, `${profile.name} pull-request jobs`).toHaveLength( + profile.pullRequestJobs, + ); + expect( + Math.max(...profile.push.map((shard) => shard.predictedSeconds ?? Infinity)), + `${profile.name} push max`, + ).toBe(profile.pushMax); + expect( + Math.max(...profile.pullRequest.map((shard) => shard.predictedSeconds ?? Infinity)), + `${profile.name} pull-request max`, + ).toBe(profile.pullRequestMax); + } + expect(hybridCompact.filter((shard) => !shard.requiresDist)).toHaveLength(46); + expect(githubCompact.length - hybridCompact.length).toBeGreaterThanOrEqual(20); expect(githubPullRequestCompact.length).toBeLessThanOrEqual(96); - // Nondist hosted lanes stay under the 150-second body ceiling; the serial - // TUI PTY dist descriptor keeps its indivisible measured wall. - expect(Math.max(...githubCompact.map((shard) => shard.predictedSeconds ?? Infinity))).toBe(149); - expect( - Math.max(...githubPullRequestCompact.map((shard) => shard.predictedSeconds ?? Infinity)), - ).toBe(186); + // Nondist expanded-profile lanes stay under the 150-second body ceiling; + // the hosted PR's serial TUI PTY descriptor remains indivisible. + for (const plan of [ + githubCompact, + githubPullRequestCompact, + hybridCompact, + hybridPullRequestCompact, + ]) { + expect( + plan + .filter((shard) => !shard.requiresDist) + .every((shard) => (shard.predictedSeconds ?? Infinity) <= 150), + ).toBe(true); + } + // Historical checks-node-compact-large-2 was this gateway-core group. Its + // 139.5s Blacksmith spike keeps a dedicated floor and singleton bin even + // though compact check numbers change when the matrix shrinks. + const hybridLargeTail = hybridCompact.find((shard) => + shard.groups.some((group) => group.shard_name === "agentic-gateway-core-3"), + ); + expect(hybridLargeTail?.groups.map((group) => group.shard_name)).toEqual([ + "agentic-gateway-core-3", + ]); + expect(hybridLargeTail?.predictedSeconds).toBe(140); + const hybridJobOf = (name: string) => + hybridCompact.findIndex((shard) => shard.groups.some((group) => group.shard_name === name)); + // Synthesized hosted stripes retain divided admission weights, while + // native hybrid groups use Blacksmith stripe hints during rebalance. + expect(hybridJobOf("agentic-agents-core-runtime-hosted-1")).not.toBe( + hybridJobOf("agentic-agents-core-tools"), + ); expect(compact.every((shard) => Array.isArray(shard.groups))).toBe(true); expect(compact.every((shard) => shard.groups.length <= 10)).toBe(true); expect(compact.some((shard) => shard.requiresDist)).toBe(true); diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index 9c0e7c963560..59e0c8e7ac73 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -5920,50 +5920,30 @@ printf '%s\n' "\${CURL_SUCCESS_IP:-203.0.113.7}" }), ); - const push = runCiManifestFixture({ - bundledPlanner: true, - eventName: "push", - }); - expect(push.status, push.output).toBe(0); - expect( - JSON.parse( - expectDefined( - push.outputs.checks_node_core_nondist_matrix, - "push node core nondist matrix output", - ), - ).include, - ).toContainEqual( - expect.objectContaining({ - check_name: "bundled-node-plan", - env: { - OPENCLAW_CI_TEST_COMPACT_MODE: "push", - OPENCLAW_CI_TEST_RUNNER_BACKEND: "", - }, - }), - ); - - const githubPush = runCiManifestFixture({ - bundledPlanner: true, - eventName: "push", - runnerBackend: "github", - }); - expect(githubPush.status, githubPush.output).toBe(0); - expect( - JSON.parse( - expectDefined( - githubPush.outputs.checks_node_core_nondist_matrix, - "GitHub-hosted push node core nondist matrix output", - ), - ).include, - ).toContainEqual( - expect.objectContaining({ - check_name: "bundled-node-plan", - env: { - OPENCLAW_CI_TEST_COMPACT_MODE: "push", - OPENCLAW_CI_TEST_RUNNER_BACKEND: "github", - }, - }), - ); + for (const runnerBackend of [undefined, "github", "hybrid"] as const) { + const push = runCiManifestFixture({ + bundledPlanner: true, + eventName: "push", + runnerBackend, + }); + expect(push.status, push.output).toBe(0); + expect( + JSON.parse( + expectDefined( + push.outputs.checks_node_core_nondist_matrix, + `${runnerBackend ?? "default"} push node core nondist matrix output`, + ), + ).include, + ).toContainEqual( + expect.objectContaining({ + check_name: "bundled-node-plan", + env: { + OPENCLAW_CI_TEST_COMPACT_MODE: "push", + OPENCLAW_CI_TEST_RUNNER_BACKEND: runnerBackend ?? "", + }, + }), + ); + } const changedPullRequest = runCiManifestFixture({ bundledPlanner: true,