ci: split remaining slow CI lanes

This commit is contained in:
Peter Steinberger
2026-04-20 17:26:42 +01:00
parent b3963e847e
commit b225d31179
6 changed files with 856 additions and 109 deletions

View File

@@ -10,8 +10,8 @@ permissions:
contents: read
concurrency:
group: ${{ github.event_name == 'pull_request' && format('{0}-{1}', github.workflow, github.event.pull_request.number) || format('{0}-{1}', github.workflow, github.run_id) }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
group: ${{ github.event_name == 'pull_request' && format('{0}-{1}', github.workflow, github.event.pull_request.number) || (github.repository == 'openclaw/openclaw' && format('{0}-{1}', github.workflow, github.ref) || format('{0}-{1}-{2}', github.workflow, github.ref, github.sha)) }}
cancel-in-progress: true
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
@@ -23,7 +23,7 @@ jobs:
permissions:
contents: read
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: blacksmith-16vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 20
outputs:
docs_only: ${{ steps.manifest.outputs.docs_only }}
@@ -39,10 +39,14 @@ jobs:
run_build_artifacts: ${{ steps.manifest.outputs.run_build_artifacts }}
run_checks_fast: ${{ steps.manifest.outputs.run_checks_fast }}
checks_fast_core_matrix: ${{ steps.manifest.outputs.checks_fast_core_matrix }}
channel_contracts_matrix: ${{ steps.manifest.outputs.channel_contracts_matrix }}
checks_node_extensions_matrix: ${{ steps.manifest.outputs.checks_node_extensions_matrix }}
run_checks: ${{ steps.manifest.outputs.run_checks }}
checks_matrix: ${{ steps.manifest.outputs.checks_matrix }}
checks_node_core_test_matrix: ${{ steps.manifest.outputs.checks_node_core_test_matrix }}
run_checks_node_core_nondist: ${{ steps.manifest.outputs.run_checks_node_core_nondist }}
checks_node_core_nondist_matrix: ${{ steps.manifest.outputs.checks_node_core_nondist_matrix }}
run_checks_node_core_dist: ${{ steps.manifest.outputs.run_checks_node_core_dist }}
checks_node_core_dist_matrix: ${{ steps.manifest.outputs.checks_node_core_dist_matrix }}
run_extension_fast: ${{ steps.manifest.outputs.run_extension_fast }}
extension_fast_matrix: ${{ steps.manifest.outputs.extension_fast_matrix }}
run_check: ${{ steps.manifest.outputs.run_check }}
@@ -134,12 +138,16 @@ jobs:
OPENCLAW_CI_RUN_CONTROL_UI_I18N: ${{ steps.changed_scope.outputs.run_control_ui_i18n || 'false' }}
OPENCLAW_CI_HAS_CHANGED_EXTENSIONS: ${{ steps.changed_extensions.outputs.has_changed_extensions || 'false' }}
OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX: ${{ steps.changed_extensions.outputs.changed_extensions_matrix || '{"include":[]}' }}
OPENCLAW_CI_REPOSITORY: ${{ github.repository }}
run: |
node --input-type=module <<'EOF'
import { appendFileSync } from "node:fs";
import {
createNodeTestShards,
} from "./scripts/lib/ci-node-test-plan.mjs";
import {
createChannelContractTestShards,
} from "./scripts/lib/channel-contract-test-plan.mjs";
import {
createExtensionTestShards,
DEFAULT_EXTENSION_TEST_SHARD_COUNT,
@@ -165,12 +173,16 @@ jobs:
const outputPath = process.env.GITHUB_OUTPUT;
const eventName = process.env.GITHUB_EVENT_NAME ?? "pull_request";
const isPush = eventName === "push";
const isCanonicalRepository = process.env.OPENCLAW_CI_REPOSITORY === "openclaw/openclaw";
const docsOnly = parseBoolean(process.env.OPENCLAW_CI_DOCS_ONLY);
const docsChanged = parseBoolean(process.env.OPENCLAW_CI_DOCS_CHANGED);
const runNode = parseBoolean(process.env.OPENCLAW_CI_RUN_NODE) && !docsOnly;
const runMacos = parseBoolean(process.env.OPENCLAW_CI_RUN_MACOS) && !docsOnly;
const runAndroid = parseBoolean(process.env.OPENCLAW_CI_RUN_ANDROID) && !docsOnly;
const runWindows = parseBoolean(process.env.OPENCLAW_CI_RUN_WINDOWS) && !docsOnly;
const runMacos =
parseBoolean(process.env.OPENCLAW_CI_RUN_MACOS) && !docsOnly && isCanonicalRepository;
const runAndroid =
parseBoolean(process.env.OPENCLAW_CI_RUN_ANDROID) && !docsOnly && isCanonicalRepository;
const runWindows =
parseBoolean(process.env.OPENCLAW_CI_RUN_WINDOWS) && !docsOnly && isCanonicalRepository;
const runSkillsPython = parseBoolean(process.env.OPENCLAW_CI_RUN_SKILLS_PYTHON) && !docsOnly;
const runControlUiI18n =
parseBoolean(process.env.OPENCLAW_CI_RUN_CONTROL_UI_I18N) && !docsOnly;
@@ -179,10 +191,13 @@ jobs:
const changedExtensionsMatrix = hasChangedExtensions
? parseJson(process.env.OPENCLAW_CI_CHANGED_EXTENSIONS_MATRIX, { include: [] })
: { include: [] };
const extensionTestShardCount = isCanonicalRepository
? DEFAULT_EXTENSION_TEST_SHARD_COUNT
: Math.max(DEFAULT_EXTENSION_TEST_SHARD_COUNT, 36);
const extensionShardMatrix = createMatrix(
runNode
? createExtensionTestShards({
shardCount: DEFAULT_EXTENSION_TEST_SHARD_COUNT,
shardCount: extensionTestShardCount,
}).map((shard) => ({
check_name: shard.checkName,
extensions_csv: shard.extensionIds.join(","),
@@ -191,6 +206,19 @@ jobs:
}))
: [],
);
const nodeTestShards = runNode
? createNodeTestShards().map((shard) => ({
check_name: shard.checkName,
runtime: "node",
task: "test-shard",
shard_name: shard.shardName,
configs: shard.configs,
includePatterns: shard.includePatterns,
requires_dist: shard.requiresDist,
}))
: [];
const nodeTestNonDistShards = nodeTestShards.filter((shard) => !shard.requires_dist);
const nodeTestDistShards = nodeTestShards.filter((shard) => shard.requires_dist);
const manifest = {
docs_only: docsOnly,
@@ -209,13 +237,14 @@ jobs:
? [
{ check_name: "checks-fast-bundled", runtime: "node", task: "bundled" },
{
check_name: "checks-fast-contracts-protocol",
check_name: "checks-fast-contracts-plugins",
runtime: "node",
task: "contracts",
task: "contracts-plugins",
},
]
: [],
),
channel_contracts_matrix: createMatrix(runNode ? createChannelContractTestShards() : []),
checks_node_extensions_matrix: extensionShardMatrix,
run_checks: runNode,
checks_matrix: createMatrix(
@@ -236,18 +265,10 @@ jobs:
]
: [],
),
checks_node_core_test_matrix: createMatrix(
runNode
? createNodeTestShards().map((shard) => ({
check_name: shard.checkName,
runtime: "node",
task: "test-shard",
shard_name: shard.shardName,
configs: shard.configs,
requires_dist: shard.requiresDist,
}))
: [],
),
run_checks_node_core_nondist: nodeTestNonDistShards.length > 0,
checks_node_core_nondist_matrix: createMatrix(nodeTestNonDistShards),
run_checks_node_core_dist: nodeTestDistShards.length > 0,
checks_node_core_dist_matrix: createMatrix(nodeTestDistShards),
run_extension_fast: hasChangedExtensions,
extension_fast_matrix: createMatrix(
hasChangedExtensions
@@ -302,7 +323,7 @@ jobs:
permissions:
contents: read
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: blacksmith-16vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 20
env:
PRE_COMMIT_CACHE_KEY_SUFFIX: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.sha }}
@@ -402,7 +423,7 @@ jobs:
contents: read
needs: [preflight]
if: needs.preflight.outputs.run_build_artifacts == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 20
steps:
- name: Checkout
@@ -499,7 +520,7 @@ jobs:
name: ${{ matrix.check_name }}
needs: [preflight]
if: needs.preflight.outputs.run_checks_fast == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 60
strategy:
fail-fast: false
@@ -569,8 +590,11 @@ jobs:
bundled)
pnpm test:bundled
;;
contracts)
pnpm test:contracts
contracts-channels)
pnpm test:contracts:channels
;;
contracts-plugins)
pnpm test:contracts:plugins
;;
*)
echo "Unsupported checks-fast task: $TASK" >&2
@@ -578,13 +602,115 @@ jobs:
;;
esac
checks-fast-channel-contracts-shard:
permissions:
contents: read
name: ${{ matrix.checkName }}
needs: [preflight]
if: needs.preflight.outputs.run_checks_fast == 'true'
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.preflight.outputs.channel_contracts_matrix) }}
steps:
- name: Checkout
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
}
checkout_attempt() {
local attempt="$1"
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
echo "checkout attempt ${attempt}/2 succeeded"
}
for attempt in 1 2; do
if checkout_attempt "$attempt"; then
exit 0
fi
echo "checkout attempt ${attempt}/2 failed"
sleep $((attempt * 5))
done
echo "checkout failed after 2 attempts" >&2
exit 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Run channel contract shard
env:
OPENCLAW_CONTRACT_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }}
shell: bash
run: |
set -euo pipefail
include_file="$RUNNER_TEMP/channel-contract-include.json"
INCLUDE_FILE="$include_file" node --input-type=module <<'EOF'
import { writeFileSync } from "node:fs";
const includePatterns = JSON.parse(process.env.OPENCLAW_CONTRACT_INCLUDE_PATTERNS_JSON ?? "[]");
if (!Array.isArray(includePatterns) || includePatterns.length === 0) {
console.error("Missing channel contract include patterns");
process.exit(1);
}
writeFileSync(process.env.INCLUDE_FILE, JSON.stringify(includePatterns), "utf8");
EOF
OPENCLAW_VITEST_INCLUDE_FILE="$include_file" pnpm test:contracts:channels
checks-fast-channel-contracts:
permissions:
contents: read
name: checks-fast-contracts-channels
needs: [preflight, checks-fast-channel-contracts-shard]
if: always() && needs.preflight.outputs.run_checks_fast == 'true'
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 5
steps:
- name: Verify channel contract shards
env:
SHARD_RESULT: ${{ needs.checks-fast-channel-contracts-shard.result }}
run: |
if [ "$SHARD_RESULT" != "success" ]; then
echo "Channel contract shards failed: $SHARD_RESULT" >&2
exit 1
fi
checks-fast-protocol:
permissions:
contents: read
name: "checks-fast-protocol"
needs: [preflight]
if: needs.preflight.outputs.run_checks_fast == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 30
steps:
- name: Checkout
@@ -649,7 +775,7 @@ jobs:
name: ${{ matrix.check_name }}
needs: [preflight]
if: needs.preflight.outputs.run_checks_fast == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 60
strategy:
fail-fast: false
@@ -719,7 +845,7 @@ jobs:
name: checks-node-extensions
needs: [preflight, checks-node-extensions-shard]
if: always() && needs.preflight.outputs.run_checks_fast == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 5
steps:
- name: Verify extension shards
@@ -737,7 +863,7 @@ jobs:
name: ${{ matrix.check_name }}
needs: [preflight, build-artifacts]
if: always() && needs.preflight.outputs.run_checks == 'true' && needs.build-artifacts.result == 'success'
runs-on: blacksmith-32vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 60
strategy:
fail-fast: false
@@ -809,17 +935,14 @@ jobs:
env:
TASK: ${{ matrix.task }}
run: |
{
echo "OPENCLAW_VITEST_MAX_WORKERS=2"
if [ "$TASK" = "test" ]; then
echo "OPENCLAW_TEST_PROJECTS_LEAF_SHARDS=1"
echo "OPENCLAW_TEST_PROJECTS_PARALLEL=4"
echo "OPENCLAW_TEST_SKIP_FULL_EXTENSIONS_SHARD=1"
fi
if [ "$TASK" = "channels" ]; then
echo "OPENCLAW_VITEST_MAX_WORKERS=1"
fi
} >> "$GITHUB_ENV"
echo "OPENCLAW_VITEST_MAX_WORKERS=2" >> "$GITHUB_ENV"
if [ "$TASK" = "test" ]; then
echo "OPENCLAW_TEST_PROJECTS_LEAF_SHARDS=1" >> "$GITHUB_ENV"
echo "OPENCLAW_TEST_SKIP_FULL_EXTENSIONS_SHARD=1" >> "$GITHUB_ENV"
fi
if [ "$TASK" = "channels" ]; then
echo "OPENCLAW_VITEST_MAX_WORKERS=1" >> "$GITHUB_ENV"
fi
- name: Download dist artifact
if: matrix.task == 'test'
@@ -863,17 +986,141 @@ jobs:
;;
esac
checks-node-core-test-shard:
checks-node-core-test-nondist-shard:
permissions:
contents: read
name: ${{ matrix.check_name }}
needs: [preflight]
if: needs.preflight.outputs.run_checks_node_core_nondist == 'true'
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_nondist_matrix) }}
steps:
- name: Checkout
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
}
checkout_attempt() {
local attempt="$1"
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
echo "checkout attempt ${attempt}/2 succeeded"
}
for attempt in 1 2; do
if checkout_attempt "$attempt"; then
exit 0
fi
echo "checkout attempt ${attempt}/2 failed"
sleep $((attempt * 5))
done
echo "checkout failed after 2 attempts" >&2
exit 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
node-version: "${{ matrix.node_version || '24.x' }}"
cache-key-suffix: "${{ matrix.cache_key_suffix || 'node24' }}"
install-bun: "false"
- name: Configure Node test resources
run: echo "OPENCLAW_VITEST_MAX_WORKERS=2" >> "$GITHUB_ENV"
- name: Run Node test shard
env:
NODE_OPTIONS: --max-old-space-size=6144
OPENCLAW_NODE_TEST_CONFIGS_JSON: ${{ toJson(matrix.configs) }}
OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }}
shell: bash
run: |
set -euo pipefail
node --input-type=module <<'EOF'
import { spawnSync } from "node:child_process";
import { writeFileSync } from "node:fs";
import { join } from "node:path";
import { resolveVitestCliEntry, resolveVitestNodeArgs } from "./scripts/run-vitest.mjs";
const configs = JSON.parse(process.env.OPENCLAW_NODE_TEST_CONFIGS_JSON ?? "[]");
if (!Array.isArray(configs) || configs.length === 0) {
console.error("Missing node test shard configs");
process.exit(1);
}
const includePatterns = JSON.parse(process.env.OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON ?? "null");
const childEnv = { ...process.env };
if (Array.isArray(includePatterns) && includePatterns.length > 0) {
const includeFile = join(
process.env.RUNNER_TEMP ?? ".",
`node-test-include-${process.env.GITHUB_JOB ?? "local"}-${Date.now()}.json`,
);
writeFileSync(includeFile, JSON.stringify(includePatterns), "utf8");
childEnv.OPENCLAW_VITEST_INCLUDE_FILE = includeFile;
}
for (const config of configs) {
console.error(`[test] starting ${config}`);
const result = spawnSync(
"pnpm",
[
"exec",
"node",
...resolveVitestNodeArgs(process.env),
resolveVitestCliEntry(),
"run",
"--config",
config,
],
{
env: childEnv,
stdio: "inherit",
},
);
if ((result.status ?? 1) !== 0) {
process.exit(result.status ?? 1);
}
}
EOF
checks-node-core-test-dist-shard:
permissions:
contents: read
name: ${{ matrix.check_name }}
needs: [preflight, build-artifacts]
if: always() && needs.preflight.outputs.run_checks == 'true' && needs.build-artifacts.result == 'success'
runs-on: blacksmith-16vcpu-ubuntu-2404
if: always() && needs.preflight.outputs.run_checks_node_core_dist == 'true' && needs.build-artifacts.result == 'success'
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_test_matrix) }}
matrix: ${{ fromJson(needs.preflight.outputs.checks_node_core_dist_matrix) }}
steps:
- name: Checkout
shell: bash
@@ -935,14 +1182,13 @@ jobs:
- name: Restore dist cache
id: dist-cache
if: matrix.requires_dist == true
uses: actions/cache@v5
with:
path: dist/
key: ${{ runner.os }}-dist-build-${{ github.sha }}
- name: Download dist artifact
if: matrix.requires_dist == true && steps.dist-cache.outputs.cache-hit != 'true'
if: steps.dist-cache.outputs.cache-hit != 'true'
uses: actions/download-artifact@v8
with:
name: dist-build
@@ -958,11 +1204,14 @@ jobs:
env:
NODE_OPTIONS: --max-old-space-size=6144
OPENCLAW_NODE_TEST_CONFIGS_JSON: ${{ toJson(matrix.configs) }}
OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }}
shell: bash
run: |
set -euo pipefail
node --input-type=module <<'EOF'
import { spawnSync } from "node:child_process";
import { writeFileSync } from "node:fs";
import { join } from "node:path";
import { resolveVitestCliEntry, resolveVitestNodeArgs } from "./scripts/run-vitest.mjs";
const configs = JSON.parse(process.env.OPENCLAW_NODE_TEST_CONFIGS_JSON ?? "[]");
@@ -970,6 +1219,16 @@ jobs:
console.error("Missing node test shard configs");
process.exit(1);
}
const includePatterns = JSON.parse(process.env.OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON ?? "null");
const childEnv = { ...process.env };
if (Array.isArray(includePatterns) && includePatterns.length > 0) {
const includeFile = join(
process.env.RUNNER_TEMP ?? ".",
`node-test-include-${process.env.GITHUB_JOB ?? "local"}-${Date.now()}.json`,
);
writeFileSync(includeFile, JSON.stringify(includePatterns), "utf8");
childEnv.OPENCLAW_VITEST_INCLUDE_FILE = includeFile;
}
for (const config of configs) {
console.error(`[test] starting ${config}`);
@@ -985,7 +1244,7 @@ jobs:
config,
],
{
env: process.env,
env: childEnv,
stdio: "inherit",
},
);
@@ -999,17 +1258,24 @@ jobs:
permissions:
contents: read
name: checks-node-core
needs: [preflight, checks-node-core-test-shard]
needs: [preflight, checks-node-core-test-nondist-shard, checks-node-core-test-dist-shard]
if: always() && needs.preflight.outputs.run_checks == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 5
steps:
- name: Verify node test shards
env:
SHARD_RESULT: ${{ needs.checks-node-core-test-shard.result }}
DIST_SHARD_RESULT: ${{ needs.checks-node-core-test-dist-shard.result }}
NONDIST_SHARD_RESULT: ${{ needs.checks-node-core-test-nondist-shard.result }}
RUN_DIST_SHARDS: ${{ needs.preflight.outputs.run_checks_node_core_dist }}
RUN_NONDIST_SHARDS: ${{ needs.preflight.outputs.run_checks_node_core_nondist }}
run: |
if [ "$SHARD_RESULT" != "success" ]; then
echo "Node test shards failed: $SHARD_RESULT" >&2
if [ "$RUN_NONDIST_SHARDS" = "true" ] && [ "$NONDIST_SHARD_RESULT" != "success" ]; then
echo "Node non-dist test shards failed: $NONDIST_SHARD_RESULT" >&2
exit 1
fi
if [ "$RUN_DIST_SHARDS" = "true" ] && [ "$DIST_SHARD_RESULT" != "success" ]; then
echo "Node dist test shards failed: $DIST_SHARD_RESULT" >&2
exit 1
fi
@@ -1019,7 +1285,7 @@ jobs:
name: "extension-fast"
needs: [preflight]
if: needs.preflight.outputs.run_extension_fast == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 60
strategy:
fail-fast: false
@@ -1083,15 +1349,31 @@ jobs:
OPENCLAW_CHANGED_EXTENSION: ${{ matrix.extension }}
run: pnpm test:extension "$OPENCLAW_CHANGED_EXTENSION"
# Types, lint, and format check.
check:
# Types, lint, and format check shards.
check-shard:
permissions:
contents: read
name: "check"
name: ${{ matrix.check_name }}
needs: [preflight]
if: always() && needs.preflight.outputs.run_check == 'true'
runs-on: blacksmith-32vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- check_name: check-preflight-guards
task: preflight-guards
- check_name: check-prod-types
task: prod-types
- check_name: check-lint
task: lint
- check_name: check-policy-guards
task: policy-guards
- check_name: check-test-types
task: test-types
- check_name: check-strict-smoke
task: strict-smoke
steps:
- name: Checkout
shell: bash
@@ -1146,18 +1428,60 @@ jobs:
with:
install-bun: "false"
- name: Check types and lint and oxfmt
- name: Run check shard
env:
OPENCLAW_LOCAL_CHECK: "0"
run: pnpm check
TASK: ${{ matrix.task }}
shell: bash
run: |
set -euo pipefail
case "$TASK" in
preflight-guards)
pnpm check:no-conflict-markers
pnpm tool-display:check
pnpm check:host-env-policy:swift
;;
prod-types)
pnpm tsgo:prod
;;
lint)
pnpm lint
;;
policy-guards)
pnpm lint:webhook:no-low-level-body-read
pnpm lint:auth:no-pairing-store-group
pnpm lint:auth:pairing-account-scope
pnpm check:import-cycles
;;
test-types)
pnpm check:test-types
;;
strict-smoke)
pnpm build:strict-smoke
;;
*)
echo "Unsupported check task: $TASK" >&2
exit 1
;;
esac
- name: Check test types
check:
permissions:
contents: read
name: "check"
needs: [preflight, check-shard]
if: always() && needs.preflight.outputs.run_check == 'true'
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 5
steps:
- name: Verify check shards
env:
OPENCLAW_LOCAL_CHECK: "0"
run: pnpm check:test-types
- name: Strict TS build smoke
run: pnpm build:strict-smoke
SHARD_RESULT: ${{ needs.check-shard.result }}
run: |
if [ "$SHARD_RESULT" != "success" ]; then
echo "Check shards failed: $SHARD_RESULT" >&2
exit 1
fi
check-additional-shard:
permissions:
@@ -1165,7 +1489,7 @@ jobs:
name: ${{ matrix.check_name }}
needs: [preflight]
if: always() && needs.preflight.outputs.run_check_additional == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 20
strategy:
fail-fast: false
@@ -1173,10 +1497,18 @@ jobs:
include:
- check_name: check-additional-boundaries
group: boundaries
- check_name: check-additional-extension-surfaces
group: extension-surfaces
- check_name: check-additional-runtime-topology
group: runtime-topology
- check_name: check-additional-extension-channels
group: extension-channels
- check_name: check-additional-extension-bundled
group: extension-bundled
- check_name: check-additional-extension-package-boundary-compile
group: extension-package-boundary-compile
- check_name: check-additional-extension-package-boundary-canary
group: extension-package-boundary-canary
- check_name: check-additional-runtime-topology-gateway
group: runtime-topology-gateway
- check_name: check-additional-runtime-topology-architecture
group: runtime-topology-architecture
steps:
- name: Checkout
shell: bash
@@ -1276,16 +1608,25 @@ jobs:
run_check "extension-relative-outside-package-boundary" pnpm run lint:extensions:no-relative-outside-package
run_check "lint:ui:no-raw-window-open" pnpm lint:ui:no-raw-window-open
;;
extension-surfaces)
extension-channels)
run_check "lint:extensions:channels" pnpm run lint:extensions:channels
run_check "lint:extensions:bundled" pnpm run lint:extensions:bundled
run_check "test:extensions:package-boundary" pnpm run test:extensions:package-boundary
;;
runtime-topology)
extension-bundled)
run_check "lint:extensions:bundled" pnpm run lint:extensions:bundled
;;
extension-package-boundary-compile)
run_check "test:extensions:package-boundary:compile" pnpm run test:extensions:package-boundary:compile
;;
extension-package-boundary-canary)
run_check "test:extensions:package-boundary:canary" pnpm run test:extensions:package-boundary:canary
;;
runtime-topology-gateway)
if [ "$RUN_CONTROL_UI_I18N" = "true" ]; then
run_check "ui:i18n:check" pnpm ui:i18n:check
fi
run_check "gateway-watch-regression" pnpm test:gateway:watch-regression
;;
runtime-topology-architecture)
run_check "check:architecture" pnpm check:architecture
;;
*)
@@ -1297,7 +1638,7 @@ jobs:
exit "$failures"
- name: Upload gateway watch regression artifacts
if: always() && matrix.group == 'runtime-topology'
if: always() && matrix.group == 'runtime-topology-gateway'
uses: actions/upload-artifact@v7
with:
name: gateway-watch-regression
@@ -1310,7 +1651,7 @@ jobs:
name: "check-additional"
needs: [preflight, check-additional-shard]
if: always() && needs.preflight.outputs.run_check_additional == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 5
steps:
- name: Verify additional check shards
@@ -1328,7 +1669,7 @@ jobs:
name: "build-smoke"
needs: [preflight, build-artifacts]
if: always() && needs.preflight.outputs.run_build_smoke == 'true' && (github.event_name != 'push' || needs.build-artifacts.result == 'success')
runs-on: blacksmith-16vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 20
steps:
- name: Checkout
@@ -1424,7 +1765,7 @@ jobs:
contents: read
needs: [preflight]
if: needs.preflight.outputs.run_check_docs == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 20
steps:
- name: Checkout
@@ -1488,7 +1829,7 @@ jobs:
contents: read
needs: [preflight]
if: needs.preflight.outputs.run_skills_python_job == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 20
steps:
- name: Checkout
@@ -1519,7 +1860,7 @@ jobs:
name: ${{ matrix.check_name }}
needs: [preflight, build-artifacts]
if: always() && needs.preflight.outputs.run_checks_windows == 'true' && needs.build-artifacts.result == 'success'
runs-on: blacksmith-32vcpu-windows-2025
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-32vcpu-windows-2025' || 'windows-2025' }}
timeout-minutes: 60
env:
NODE_OPTIONS: --max-old-space-size=6144
@@ -1581,9 +1922,6 @@ jobs:
- name: Capture node path
run: |
node_bin="$(dirname "$(node -p 'process.execPath')")"
if command -v cygpath >/dev/null 2>&1; then
node_bin="$(cygpath -u "$node_bin")"
fi
echo "NODE_BIN=$node_bin" >> "$GITHUB_ENV"
- name: Install dependencies
@@ -1636,7 +1974,7 @@ jobs:
name: ${{ matrix.check_name }}
needs: [preflight, build-artifacts]
if: always() && needs.preflight.outputs.run_macos_node == 'true' && needs.build-artifacts.result == 'success'
runs-on: blacksmith-6vcpu-macos-latest
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-6vcpu-macos-latest' || 'macos-latest' }}
timeout-minutes: 20
strategy:
fail-fast: false
@@ -1715,7 +2053,7 @@ jobs:
name: "macos-swift"
needs: [preflight]
if: needs.preflight.outputs.run_macos_swift == 'true'
runs-on: blacksmith-12vcpu-macos-latest
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-latest' || 'macos-latest' }}
timeout-minutes: 20
steps:
- name: Checkout
@@ -1826,7 +2164,7 @@ jobs:
name: ${{ matrix.check_name }}
needs: [preflight]
if: needs.preflight.outputs.run_android_job == 'true'
runs-on: blacksmith-16vcpu-ubuntu-2404
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
timeout-minutes: 20
strategy:
fail-fast: false

View File

@@ -12,25 +12,26 @@ The CI runs on every push to `main` and every pull request. It uses smart scopin
## Job Overview
| Job | Purpose | When it runs |
| ------------------------ | ------------------------------------------------------------------------------------------- | ----------------------------------- |
| `preflight` | Detect docs-only changes, changed scopes, changed extensions, and build the CI manifest | Always on non-draft pushes and PRs |
| `security-fast` | Private key detection, workflow audit via `zizmor`, production dependency audit | Always on non-draft pushes and PRs |
| `build-artifacts` | Build `dist/` and the Control UI once, upload reusable artifacts for downstream jobs | Node-relevant changes |
| `checks-fast-core` | Fast Linux correctness lanes such as bundled/plugin-contract/protocol checks | Node-relevant changes |
| `checks-node-extensions` | Full bundled-plugin test shards across the extension suite | Node-relevant changes |
| `checks-node-core-test` | Core Node test shards, excluding channel, bundled, contract, and extension lanes | Node-relevant changes |
| `extension-fast` | Focused tests for only the changed bundled plugins | When extension changes are detected |
| `check` | Main local gate in CI: `pnpm check`, `pnpm check:test-types`, and `pnpm build:strict-smoke` | Node-relevant changes |
| `check-additional` | Architecture, boundary, import-cycle guards plus the gateway watch regression harness | Node-relevant changes |
| `build-smoke` | Built-CLI smoke tests and startup-memory smoke | Node-relevant changes |
| `checks` | Remaining Linux Node lanes: channel tests and push-only Node 22 compatibility | Node-relevant changes |
| `check-docs` | Docs formatting, lint, and broken-link checks | Docs changed |
| `skills-python` | Ruff + pytest for Python-backed skills | Python-skill-relevant changes |
| `checks-windows` | Windows-specific test lanes | Windows-relevant changes |
| `macos-node` | macOS TypeScript test lane using the shared built artifacts | macOS-relevant changes |
| `macos-swift` | Swift lint, build, and tests for the macOS app | macOS-relevant changes |
| `android` | Android build and test matrix | Android-relevant changes |
| Job | Purpose | When it runs |
| -------------------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------- |
| `preflight` | Detect docs-only changes, changed scopes, changed extensions, and build the CI manifest | Always on non-draft pushes and PRs |
| `security-fast` | Private key detection, workflow audit via `zizmor`, production dependency audit | Always on non-draft pushes and PRs |
| `build-artifacts` | Build `dist/` and the Control UI once, upload reusable artifacts for downstream jobs | Node-relevant changes |
| `checks-fast-core` | Fast Linux correctness lanes such as bundled/plugin-contract/protocol checks | Node-relevant changes |
| `checks-fast-contracts-channels` | Sharded channel contract checks with a stable aggregate check result | Node-relevant changes |
| `checks-node-extensions` | Full bundled-plugin test shards across the extension suite | Node-relevant changes |
| `checks-node-core-test` | Core Node test shards, excluding channel, bundled, contract, and extension lanes | Node-relevant changes |
| `extension-fast` | Focused tests for only the changed bundled plugins | When extension changes are detected |
| `check` | Sharded main local gate equivalent: prod types, lint, guards, test types, and strict smoke | Node-relevant changes |
| `check-additional` | Architecture, boundary, extension-surface guards, package-boundary, and gateway-watch shards | Node-relevant changes |
| `build-smoke` | Built-CLI smoke tests and startup-memory smoke | Node-relevant changes |
| `checks` | Remaining Linux Node lanes: channel tests and push-only Node 22 compatibility | Node-relevant changes |
| `check-docs` | Docs formatting, lint, and broken-link checks | Docs changed |
| `skills-python` | Ruff + pytest for Python-backed skills | Python-skill-relevant changes |
| `checks-windows` | Windows-specific test lanes | Windows-relevant changes |
| `macos-node` | macOS TypeScript test lane using the shared built artifacts | macOS-relevant changes |
| `macos-swift` | Swift lint, build, and tests for the macOS app | macOS-relevant changes |
| `android` | Android build and test matrix | Android-relevant changes |
## Fail-Fast Order
@@ -39,7 +40,7 @@ Jobs are ordered so cheap checks fail before expensive ones run:
1. `preflight` decides which lanes exist at all. The `docs-scope` and `changed-scope` logic are steps inside this job, not standalone jobs.
2. `security-fast`, `check`, `check-additional`, `check-docs`, and `skills-python` fail quickly without waiting on the heavier artifact and platform matrix jobs.
3. `build-artifacts` overlaps with the fast Linux lanes so downstream consumers can start as soon as the shared build is ready.
4. Heavier platform and runtime lanes fan out after that: `checks-fast-core`, `checks-node-extensions`, `checks-node-core-test`, `extension-fast`, `checks`, `checks-windows`, `macos-node`, `macos-swift`, and `android`.
4. Heavier platform and runtime lanes fan out after that: `checks-fast-core`, `checks-fast-contracts-channels`, `checks-node-extensions`, `checks-node-core-test`, `extension-fast`, `checks`, `checks-windows`, `macos-node`, `macos-swift`, and `android`.
Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests in `src/scripts/ci-changed-scope.test.ts`.
The separate `install-smoke` workflow reuses the same scope script through its own `preflight` job. It computes `run_install_smoke` from the narrower changed-smoke signal, so Docker/install smoke only runs for install, packaging, and container-relevant changes.
@@ -48,6 +49,8 @@ Local changed-lane logic lives in `scripts/changed-lanes.mjs` and is executed by
On pushes, the `checks` matrix adds the push-only `compat-node22` lane. On pull requests, that lane is skipped and the matrix stays focused on the normal test/channel lanes.
The slowest Node test families are split into include-file shards so each job stays small: channel contracts split registry/core/extension coverage into focused shards, and auto-reply reply tests split each large prefix group into two include-pattern shards. `check-additional` also separates package-boundary compile/canary work from runtime topology gateway/architecture work.
## Runners
| Runner | Jobs |

View File

@@ -0,0 +1,54 @@
import { existsSync, readdirSync } from "node:fs";
import { join, relative } from "node:path";
function listContractTestFiles(rootDir = "src/channels/plugins/contracts") {
if (!existsSync(rootDir)) {
return [];
}
return readdirSync(rootDir, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith(".test.ts"))
.map((entry) => join(rootDir, entry.name).replaceAll("\\", "/"))
.toSorted((a, b) => a.localeCompare(b));
}
export function createChannelContractTestShards() {
const rootDir = "src/channels/plugins/contracts";
const groups = {
"checks-fast-contracts-channels-registry-a": [],
"checks-fast-contracts-channels-registry-b": [],
"checks-fast-contracts-channels-core-a": [],
"checks-fast-contracts-channels-core-b": [],
"checks-fast-contracts-channels-extensions": [],
};
const pushBalanced = (firstKey, secondKey, file) => {
const target = groups[firstKey].length <= groups[secondKey].length ? firstKey : secondKey;
groups[target].push(file);
};
for (const file of listContractTestFiles(rootDir)) {
const name = relative(rootDir, file).replaceAll("\\", "/");
if (name.startsWith("plugins-core-extension.")) {
groups["checks-fast-contracts-channels-extensions"].push(file);
} else if (name.startsWith("plugins-core.") || name.startsWith("plugin.")) {
pushBalanced(
"checks-fast-contracts-channels-core-a",
"checks-fast-contracts-channels-core-b",
file,
);
} else {
pushBalanced(
"checks-fast-contracts-channels-registry-a",
"checks-fast-contracts-channels-registry-b",
file,
);
}
}
return Object.entries(groups).map(([checkName, includePatterns]) => ({
checkName,
includePatterns,
task: "contracts-channels",
runtime: "node",
}));
}

View File

@@ -1,3 +1,5 @@
import { existsSync, readdirSync } from "node:fs";
import { join, relative } from "node:path";
import { fullSuiteVitestShards } from "../../test/vitest/vitest.test-shards.mjs";
const EXCLUDED_FULL_SUITE_SHARDS = new Set([
@@ -7,7 +9,137 @@ const EXCLUDED_FULL_SUITE_SHARDS = new Set([
]);
const EXCLUDED_PROJECT_CONFIGS = new Set(["test/vitest/vitest.channels.config.ts"]);
function listTestFiles(rootDir) {
if (!existsSync(rootDir)) {
return [];
}
const files = [];
const visit = (dir) => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
if (entry.isDirectory()) {
visit(path);
continue;
}
if (entry.isFile() && entry.name.endsWith(".test.ts")) {
files.push(path.replaceAll("\\", "/"));
}
}
};
visit(rootDir);
return files.toSorted((a, b) => a.localeCompare(b));
}
function createAutoReplyReplySplitShards() {
const files = listTestFiles("src/auto-reply/reply");
const groups = {
"auto-reply-reply-agent-runner": [],
"auto-reply-reply-commands": [],
"auto-reply-reply-dispatch": [],
"auto-reply-reply-state-routing": [],
};
for (const file of files) {
const name = relative("src/auto-reply/reply", file).replaceAll("\\", "/");
if (
name.startsWith("agent-runner") ||
name.startsWith("acp-") ||
name === "abort.test.ts" ||
name === "bash-command.stop.test.ts" ||
name.startsWith("block-")
) {
groups["auto-reply-reply-agent-runner"].push(file);
} else if (name.startsWith("commands")) {
groups["auto-reply-reply-commands"].push(file);
} else if (
name.startsWith("directive-") ||
name.startsWith("dispatch") ||
name.startsWith("followup-") ||
name.startsWith("get-reply")
) {
groups["auto-reply-reply-dispatch"].push(file);
} else {
groups["auto-reply-reply-state-routing"].push(file);
}
}
return Object.entries(groups).flatMap(([groupName, includePatterns]) => {
const midpoint = Math.ceil(includePatterns.length / 2);
return [
{
shardName: `${groupName}-a`,
configs: ["test/vitest/vitest.auto-reply-reply.config.ts"],
includePatterns: includePatterns.slice(0, midpoint),
requiresDist: false,
},
{
shardName: `${groupName}-b`,
configs: ["test/vitest/vitest.auto-reply-reply.config.ts"],
includePatterns: includePatterns.slice(midpoint),
requiresDist: false,
},
].filter((shard) => shard.includePatterns.length > 0);
});
}
const SPLIT_NODE_SHARDS = new Map([
[
"core-runtime",
[
{
shardName: "core-runtime-infra",
configs: [
"test/vitest/vitest.infra.config.ts",
"test/vitest/vitest.hooks.config.ts",
"test/vitest/vitest.runtime-config.config.ts",
"test/vitest/vitest.secrets.config.ts",
"test/vitest/vitest.logging.config.ts",
"test/vitest/vitest.process.config.ts",
],
requiresDist: true,
},
{
shardName: "core-runtime-media-ui",
configs: [
"test/vitest/vitest.media.config.ts",
"test/vitest/vitest.media-understanding.config.ts",
"test/vitest/vitest.tui.config.ts",
"test/vitest/vitest.ui.config.ts",
"test/vitest/vitest.wizard.config.ts",
],
requiresDist: true,
},
{
shardName: "core-runtime-shared",
configs: [
"test/vitest/vitest.acp.config.ts",
"test/vitest/vitest.cron.config.ts",
"test/vitest/vitest.shared-core.config.ts",
"test/vitest/vitest.tasks.config.ts",
"test/vitest/vitest.utils.config.ts",
],
requiresDist: true,
},
],
],
[
"auto-reply",
[
{
shardName: "auto-reply-core",
configs: ["test/vitest/vitest.auto-reply-core.config.ts"],
requiresDist: false,
},
{
shardName: "auto-reply-top-level",
configs: ["test/vitest/vitest.auto-reply-top-level.config.ts"],
requiresDist: false,
},
...createAutoReplyReplySplitShards(),
],
],
[
"agentic",
[
@@ -77,6 +209,7 @@ export function createNodeTestShards() {
checkName: formatNodeTestShardCheckName(splitShard.shardName),
shardName: splitShard.shardName,
configs: splitConfigs,
...(splitShard.includePatterns ? { includePatterns: splitShard.includePatterns } : {}),
requiresDist: splitShard.requiresDist,
},
];

View File

@@ -0,0 +1,62 @@
import { existsSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { createChannelContractTestShards } from "../../scripts/lib/channel-contract-test-plan.mjs";
function listContractTests(rootDir = "src/channels/plugins/contracts"): string[] {
if (!existsSync(rootDir)) {
return [];
}
return readdirSync(rootDir, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith(".test.ts"))
.map((entry) => join(rootDir, entry.name).replaceAll("\\", "/"))
.toSorted((a, b) => a.localeCompare(b));
}
describe("scripts/lib/channel-contract-test-plan.mjs", () => {
it("splits channel contracts into focused shards", () => {
expect(
createChannelContractTestShards().map((shard) => ({
checkName: shard.checkName,
runtime: shard.runtime,
task: shard.task,
})),
).toEqual([
{
checkName: "checks-fast-contracts-channels-registry-a",
runtime: "node",
task: "contracts-channels",
},
{
checkName: "checks-fast-contracts-channels-registry-b",
runtime: "node",
task: "contracts-channels",
},
{
checkName: "checks-fast-contracts-channels-core-a",
runtime: "node",
task: "contracts-channels",
},
{
checkName: "checks-fast-contracts-channels-core-b",
runtime: "node",
task: "contracts-channels",
},
{
checkName: "checks-fast-contracts-channels-extensions",
runtime: "node",
task: "contracts-channels",
},
]);
});
it("covers every channel contract test exactly once", () => {
const actual = createChannelContractTestShards()
.flatMap((shard) => shard.includePatterns)
.toSorted((a, b) => a.localeCompare(b));
expect(actual).toEqual(listContractTests());
expect(new Set(actual).size).toBe(actual.length);
});
});

View File

@@ -1,6 +1,29 @@
import { existsSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { createNodeTestShards } from "../../scripts/lib/ci-node-test-plan.mjs";
function listTestFiles(rootDir: string): string[] {
if (!existsSync(rootDir)) {
return [];
}
const files: string[] = [];
const visit = (dir: string) => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
if (entry.isDirectory()) {
visit(path);
} else if (entry.isFile() && entry.name.endsWith(".test.ts")) {
files.push(path.replaceAll("\\", "/"));
}
}
};
visit(rootDir);
return files.toSorted((a, b) => a.localeCompare(b));
}
describe("scripts/lib/ci-node-test-plan.mjs", () => {
it("names the node shard checks as core test lanes", () => {
const shards = createNodeTestShards();
@@ -32,11 +55,60 @@ describe("scripts/lib/ci-node-test-plan.mjs", () => {
expect(requiresDistShardNames).toEqual([
"core-support-boundary",
"core-runtime",
"core-runtime-infra",
"core-runtime-media-ui",
"core-runtime-shared",
"agentic-agents-plugins",
]);
});
it("splits core runtime configs into smaller dist-dependent shards", () => {
const runtimeShards = createNodeTestShards()
.filter((shard) => shard.shardName.startsWith("core-runtime-"))
.map((shard) => ({
configs: shard.configs,
requiresDist: shard.requiresDist,
shardName: shard.shardName,
}));
expect(runtimeShards).toEqual([
{
configs: [
"test/vitest/vitest.infra.config.ts",
"test/vitest/vitest.hooks.config.ts",
"test/vitest/vitest.runtime-config.config.ts",
"test/vitest/vitest.secrets.config.ts",
"test/vitest/vitest.logging.config.ts",
"test/vitest/vitest.process.config.ts",
],
requiresDist: true,
shardName: "core-runtime-infra",
},
{
configs: [
"test/vitest/vitest.media.config.ts",
"test/vitest/vitest.media-understanding.config.ts",
"test/vitest/vitest.tui.config.ts",
"test/vitest/vitest.ui.config.ts",
"test/vitest/vitest.wizard.config.ts",
],
requiresDist: true,
shardName: "core-runtime-media-ui",
},
{
configs: [
"test/vitest/vitest.acp.config.ts",
"test/vitest/vitest.cron.config.ts",
"test/vitest/vitest.shared-core.config.ts",
"test/vitest/vitest.tasks.config.ts",
"test/vitest/vitest.utils.config.ts",
],
requiresDist: true,
shardName: "core-runtime-shared",
},
]);
});
it("splits the agentic lane into control-plane, commands, and agent/plugin shards", () => {
const shards = createNodeTestShards();
const controlPlaneShard = shards.find((shard) => shard.shardName === "agentic-control-plane");
@@ -77,4 +149,89 @@ describe("scripts/lib/ci-node-test-plan.mjs", () => {
requiresDist: true,
});
});
it("splits auto-reply into independent core, top-level, and reply subtree shards", () => {
const shards = createNodeTestShards();
const autoReplyShards = shards
.filter((shard) => shard.shardName.startsWith("auto-reply"))
.map((shard) => ({
checkName: shard.checkName,
configs: shard.configs,
requiresDist: shard.requiresDist,
shardName: shard.shardName,
}));
expect(autoReplyShards).toEqual([
{
checkName: "checks-node-auto-reply-core",
configs: ["test/vitest/vitest.auto-reply-core.config.ts"],
requiresDist: false,
shardName: "auto-reply-core",
},
{
checkName: "checks-node-auto-reply-top-level",
configs: ["test/vitest/vitest.auto-reply-top-level.config.ts"],
requiresDist: false,
shardName: "auto-reply-top-level",
},
{
checkName: "checks-node-auto-reply-reply-agent-runner-a",
configs: ["test/vitest/vitest.auto-reply-reply.config.ts"],
requiresDist: false,
shardName: "auto-reply-reply-agent-runner-a",
},
{
checkName: "checks-node-auto-reply-reply-agent-runner-b",
configs: ["test/vitest/vitest.auto-reply-reply.config.ts"],
requiresDist: false,
shardName: "auto-reply-reply-agent-runner-b",
},
{
checkName: "checks-node-auto-reply-reply-commands-a",
configs: ["test/vitest/vitest.auto-reply-reply.config.ts"],
requiresDist: false,
shardName: "auto-reply-reply-commands-a",
},
{
checkName: "checks-node-auto-reply-reply-commands-b",
configs: ["test/vitest/vitest.auto-reply-reply.config.ts"],
requiresDist: false,
shardName: "auto-reply-reply-commands-b",
},
{
checkName: "checks-node-auto-reply-reply-dispatch-a",
configs: ["test/vitest/vitest.auto-reply-reply.config.ts"],
requiresDist: false,
shardName: "auto-reply-reply-dispatch-a",
},
{
checkName: "checks-node-auto-reply-reply-dispatch-b",
configs: ["test/vitest/vitest.auto-reply-reply.config.ts"],
requiresDist: false,
shardName: "auto-reply-reply-dispatch-b",
},
{
checkName: "checks-node-auto-reply-reply-state-routing-a",
configs: ["test/vitest/vitest.auto-reply-reply.config.ts"],
requiresDist: false,
shardName: "auto-reply-reply-state-routing-a",
},
{
checkName: "checks-node-auto-reply-reply-state-routing-b",
configs: ["test/vitest/vitest.auto-reply-reply.config.ts"],
requiresDist: false,
shardName: "auto-reply-reply-state-routing-b",
},
]);
});
it("covers every auto-reply reply test exactly once across split shards", () => {
const actual = createNodeTestShards()
.filter((shard) => shard.shardName.startsWith("auto-reply-reply-"))
.flatMap((shard) => shard.includePatterns ?? [])
.toSorted((a, b) => a.localeCompare(b));
expect(actual).toEqual(listTestFiles("src/auto-reply/reply"));
expect(new Set(actual).size).toBe(actual.length);
});
});