Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

197 lines
6.7 KiB
JavaScript

/**
* test-task-pool.mjs
*
* Concurrency primitives for the cross-package test runner
* (`run-all-tests.mjs`). That script executes its main loop on import, so the
* parallelizable logic lives here where it can be unit-tested in isolation.
*
* The runner has historically been strictly serial: one package's test script
* had to exit before the next could start. The only cross-package parallelism
* came from turbo `--concurrency` inside specific aggregate scripts and from CI
* sharding. This module lets the runner opt into a bounded worker pool over the
* package set that is proven safe to run concurrently.
*
* Safety model (mirrors the existing, proven `test:plugins` policy in the root
* package.json, which already runs ~140 plugin packages at `--concurrency 3`):
*
* - Only the plain `test` script in the secret-free PR lane is parallel-safe.
* There, vitest forks bind ephemeral ports and `*.real`/`*.real.e2e` files
* are excluded (VITEST_EXCLUDE_REAL), so there is no shared-Postgres or
* fixed-port contention between packages.
* - The EXTRA_SCRIPT_NAMES lanes (test:integration / test:e2e /
* test:playwright / test:ui / test:live) bind fixed ports and/or touch the
* shared `eliza_test` database, so they stay serialized.
* - A small denylist of packages must stay serial even for their `test`
* script — the same two the root `test:plugins` already pulls out of the
* concurrent sweep, plus `plugin-sql` (it drives the shared Postgres /
* PGlite database with `fileParallelism: false`).
* - Any lane other than `pr` (i.e. the post-merge real-API lane) runs fully
* serial: real providers + the shared database make concurrency unsafe.
*
* It also owns the deterministic `TEST_SHARD=N/M` membership logic so CI
* matrices can split the package set across runner boxes (an orthogonal,
* cross-machine form of the same parallelism).
*/
import crypto from "node:crypto";
import { resolveTestSerialPackages } from "./script-metadata.mjs";
/**
* Packages whose `test` script must not run concurrently with others, even in
* the PR lane. Membership is declared per-package via `elizaos.scripts.testSerial`
* (a shared DB harness or fixed-port contention makes concurrency unsafe) and
* resolved here through the discovery seam — no plugin names live in this file.
*/
export const SERIALIZE_PACKAGES = resolveTestSerialPackages();
/**
* Whether a discovered test task may run concurrently with other tasks.
*
* @param {{ scriptName: string, lane: string, packageName?: string }} task
* @returns {boolean}
*/
export function isParallelSafeTask({ scriptName, lane, packageName }) {
if (lane !== "pr") {
return false;
}
if (scriptName !== "test") {
return false;
}
if (packageName && SERIALIZE_PACKAGES.has(packageName)) {
return false;
}
return true;
}
/**
* Split tasks into the concurrent bucket and the serialized bucket, preserving
* the original relative order within each bucket.
*
* @template {{ scriptName: string, packageName?: string }} T
* @param {T[]} tasks
* @param {string} lane
* @returns {{ parallel: T[], serial: T[] }}
*/
export function partitionTasks(tasks, lane) {
const parallel = [];
const serial = [];
for (const task of tasks) {
if (isParallelSafeTask({ ...task, lane })) {
parallel.push(task);
} else {
serial.push(task);
}
}
return { parallel, serial };
}
/**
* Run `items` through `worker` with at most `concurrency` invocations in flight
* at once. Resolves to a results array in the SAME ORDER as `items` so callers
* can pair each result with its input. Never rejects: a worker that throws
* yields `{ ok: false, error }` in that slot, success yields `{ ok: true,
* value }`. This lets the caller run every task to completion and report all
* failures at the end rather than aborting on the first one.
*
* @template T, R
* @param {T[]} items
* @param {(item: T, index: number) => Promise<R>} worker
* @param {number} concurrency
* @returns {Promise<Array<{ ok: true, value: R } | { ok: false, error: unknown }>>}
*/
export async function runPool(items, worker, concurrency) {
const results = new Array(items.length);
if (items.length === 0) {
return results;
}
const workers = Math.max(
1,
Math.min(Math.trunc(concurrency) || 1, items.length),
);
let next = 0;
async function drain() {
while (true) {
const index = next++;
if (index >= items.length) {
return;
}
try {
results[index] = { ok: true, value: await worker(items[index], index) };
} catch (error) {
results[index] = { ok: false, error };
}
}
}
await Promise.all(Array.from({ length: workers }, () => drain()));
return results;
}
/**
* Normalise a requested concurrency value (CLI flag or env var) into a positive
* integer, defaulting to 1 (fully serial — the historical behaviour).
*
* @param {string | number | undefined | null} value
* @returns {number}
*/
export function normalizeConcurrency(value) {
if (value === undefined || value === null || value === "") {
return 1;
}
const parsed =
typeof value === "number" ? value : Number.parseInt(String(value), 10);
if (!Number.isFinite(parsed) || parsed < 1) {
return 1;
}
return Math.trunc(parsed);
}
/**
* Parse a `TEST_SHARD` spec ("N/M", 1-indexed) into `{ index, total }`, or
* `null` when absent or malformed. Pure (no warnings) so callers decide how to
* surface an invalid spec.
*
* @param {string | undefined | null} spec
* @returns {{ index: number, total: number } | null}
*/
export function parseShardSpec(spec) {
if (!spec) {
return null;
}
const parts = String(spec).split("/");
if (parts.length !== 2) {
return null;
}
const index = Number.parseInt(parts[0], 10);
const total = Number.parseInt(parts[1], 10);
if (
!Number.isInteger(index) ||
!Number.isInteger(total) ||
total <= 0 ||
index < 1 ||
index > total
) {
return null;
}
return { index, total };
}
/**
* Stable shard membership: SHA-1 of the task key (the task's relative package
* dir) → bucket → assign to shard N (1-indexed) of M. Hashing the relative dir
* rather than the full label keeps a package's `test` and `test:e2e` tasks in
* the same shard, amortising Postgres + mock startup across the package's full
* task set. Returns true when there is no shard config (run everything).
*
* @param {string} taskKey
* @param {{ index: number, total: number } | null} shardCfg
* @returns {boolean}
*/
export function taskBelongsToShard(taskKey, shardCfg) {
if (!shardCfg) {
return true;
}
const digest = crypto.createHash("sha1").update(taskKey).digest();
const bucket = digest.readUInt32BE(0) % shardCfg.total;
return bucket === shardCfg.index - 1;
}