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
197 lines
6.7 KiB
JavaScript
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;
|
|
}
|