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
406 lines
13 KiB
TypeScript
406 lines
13 KiB
TypeScript
// Exercises tests test task pool.test automation behavior with deterministic script fixtures.
|
|
import { describe, expect, test } from "bun:test";
|
|
import { spawnSync } from "node:child_process";
|
|
import { readFileSync } from "node:fs";
|
|
|
|
import {
|
|
isParallelSafeTask,
|
|
normalizeConcurrency,
|
|
parseShardSpec,
|
|
partitionTasks,
|
|
runPool,
|
|
SERIALIZE_PACKAGES,
|
|
taskBelongsToShard,
|
|
} from "../lib/test-task-pool.mjs";
|
|
|
|
describe("isParallelSafeTask", () => {
|
|
test("plain `test` script in the pr lane is parallel-safe", () => {
|
|
expect(
|
|
isParallelSafeTask({
|
|
scriptName: "test",
|
|
lane: "pr",
|
|
packageName: "@elizaos/core",
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("extra-script lanes (e2e/integration/...) are never parallel-safe", () => {
|
|
for (const scriptName of [
|
|
"test:e2e",
|
|
"test:integration",
|
|
"test:playwright",
|
|
"test:ui",
|
|
"test:live",
|
|
]) {
|
|
expect(
|
|
isParallelSafeTask({
|
|
scriptName,
|
|
lane: "pr",
|
|
packageName: "@elizaos/core",
|
|
}),
|
|
).toBe(false);
|
|
}
|
|
});
|
|
|
|
test("any lane other than pr forces serial (real-API / shared DB)", () => {
|
|
expect(
|
|
isParallelSafeTask({
|
|
scriptName: "test",
|
|
lane: "post-merge",
|
|
packageName: "@elizaos/core",
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
test("denylisted packages stay serial even for their `test` script", () => {
|
|
for (const packageName of SERIALIZE_PACKAGES) {
|
|
expect(
|
|
isParallelSafeTask({ scriptName: "test", lane: "pr", packageName }),
|
|
).toBe(false);
|
|
}
|
|
});
|
|
|
|
test("denylist matches the packages the root test:plugins sweep pulls out", () => {
|
|
expect(SERIALIZE_PACKAGES.has("@elizaos/plugin-personal-assistant")).toBe(
|
|
true,
|
|
);
|
|
expect(SERIALIZE_PACKAGES.has("@elizaos/plugin-agent-orchestrator")).toBe(
|
|
true,
|
|
);
|
|
expect(SERIALIZE_PACKAGES.has("@elizaos/plugin-sql")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("partitionTasks", () => {
|
|
test("splits into parallel/serial buckets preserving order", () => {
|
|
const tasks = [
|
|
{ packageName: "@elizaos/core", scriptName: "test" },
|
|
{ packageName: "@elizaos/core", scriptName: "test:e2e" },
|
|
{ packageName: "@elizaos/plugin-sql", scriptName: "test" },
|
|
{ packageName: "@elizaos/agent", scriptName: "test" },
|
|
];
|
|
const { parallel, serial } = partitionTasks(tasks, "pr");
|
|
expect(parallel.map((t) => t.packageName)).toEqual([
|
|
"@elizaos/core",
|
|
"@elizaos/agent",
|
|
]);
|
|
expect(serial.map((t) => `${t.packageName}#${t.scriptName}`)).toEqual([
|
|
"@elizaos/core#test:e2e",
|
|
"@elizaos/plugin-sql#test",
|
|
]);
|
|
});
|
|
|
|
test("post-merge lane puts everything in the serial bucket", () => {
|
|
const tasks = [
|
|
{ packageName: "@elizaos/core", scriptName: "test" },
|
|
{ packageName: "@elizaos/agent", scriptName: "test" },
|
|
];
|
|
const { parallel, serial } = partitionTasks(tasks, "post-merge");
|
|
expect(parallel).toHaveLength(0);
|
|
expect(serial).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
describe("runPool", () => {
|
|
test("preserves result order regardless of completion order", async () => {
|
|
const results = await runPool(
|
|
[30, 10, 20, 0, 5],
|
|
async (ms, i) => {
|
|
await new Promise((r) => setTimeout(r, ms));
|
|
return i;
|
|
},
|
|
3,
|
|
);
|
|
expect(results.map((r) => (r.ok ? r.value : null))).toEqual([
|
|
0, 1, 2, 3, 4,
|
|
]);
|
|
});
|
|
|
|
test("never runs more than `concurrency` workers at once", async () => {
|
|
let inFlight = 0;
|
|
let maxInFlight = 0;
|
|
await runPool(
|
|
Array.from({ length: 20 }, (_, i) => i),
|
|
async () => {
|
|
inFlight++;
|
|
maxInFlight = Math.max(maxInFlight, inFlight);
|
|
await new Promise((r) => setTimeout(r, 5));
|
|
inFlight--;
|
|
},
|
|
4,
|
|
);
|
|
expect(maxInFlight).toBeLessThanOrEqual(4);
|
|
expect(maxInFlight).toBeGreaterThan(1);
|
|
});
|
|
|
|
test("captures thrown errors as { ok: false } without aborting siblings", async () => {
|
|
const results = await runPool(
|
|
[1, 2, 3, 4],
|
|
async (n) => {
|
|
if (n % 2 === 0) {
|
|
throw new Error(`boom ${n}`);
|
|
}
|
|
return n;
|
|
},
|
|
2,
|
|
);
|
|
expect(results[0]).toEqual({ ok: true, value: 1 });
|
|
expect(results[1].ok).toBe(false);
|
|
expect(results[2]).toEqual({ ok: true, value: 3 });
|
|
expect(results[3].ok).toBe(false);
|
|
// All four ran — a thrown worker does not cancel the rest of the pool.
|
|
expect(results.every((r) => r !== undefined)).toBe(true);
|
|
});
|
|
|
|
test("empty input resolves to an empty array", async () => {
|
|
expect(await runPool([], async () => 1, 4)).toEqual([]);
|
|
});
|
|
|
|
test("concurrency is clamped to at least 1 and at most item count", async () => {
|
|
const results = await runPool([1, 2], async (n) => n, 99);
|
|
expect(results.map((r) => r.value)).toEqual([1, 2]);
|
|
});
|
|
});
|
|
|
|
describe("process supervision boundary", () => {
|
|
test("documents why dev supervisors and the test pool stay split", () => {
|
|
const doc = readFileSync(
|
|
new URL("../process-supervision.md", import.meta.url),
|
|
"utf8",
|
|
);
|
|
|
|
expect(doc).toContain("keep the seams separate");
|
|
expect(doc).toContain("api-supervisor.mjs");
|
|
expect(doc).toContain("dev-all.mjs");
|
|
expect(doc).toContain("run-all-tests.mjs");
|
|
expect(doc).toContain("long-lived single-child supervisor");
|
|
expect(doc).toContain("bounded batch runner");
|
|
expect(doc).toContain("digest.readUInt32BE(0)");
|
|
});
|
|
});
|
|
|
|
describe("normalizeConcurrency", () => {
|
|
test("defaults to 1 (fully serial) for empty/invalid input", () => {
|
|
for (const value of [undefined, null, "", "abc", "0", "-3", 0, -1]) {
|
|
expect(normalizeConcurrency(value)).toBe(1);
|
|
}
|
|
});
|
|
|
|
test("parses positive integers from string or number", () => {
|
|
expect(normalizeConcurrency("4")).toBe(4);
|
|
expect(normalizeConcurrency(8)).toBe(8);
|
|
expect(normalizeConcurrency("3.9")).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe("parseShardSpec", () => {
|
|
test("parses a valid N/M spec", () => {
|
|
expect(parseShardSpec("2/4")).toEqual({ index: 2, total: 4 });
|
|
expect(parseShardSpec("1/1")).toEqual({ index: 1, total: 1 });
|
|
expect(parseShardSpec("4/4")).toEqual({ index: 4, total: 4 });
|
|
});
|
|
|
|
test("returns null for absent specs", () => {
|
|
expect(parseShardSpec("")).toBeNull();
|
|
expect(parseShardSpec(undefined)).toBeNull();
|
|
expect(parseShardSpec(null)).toBeNull();
|
|
});
|
|
|
|
test("returns null for malformed or out-of-range specs", () => {
|
|
for (const bad of [
|
|
"3", // missing /M
|
|
"a/b", // non-numeric
|
|
"0/4", // index < 1
|
|
"5/4", // index > total
|
|
"2/0", // total <= 0
|
|
"-1/4", // negative index
|
|
"1/2/3", // too many parts
|
|
"/4", // empty index
|
|
"2/", // empty total
|
|
]) {
|
|
expect(parseShardSpec(bad)).toBeNull();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("taskBelongsToShard", () => {
|
|
test("everything belongs when there is no shard config", () => {
|
|
expect(taskBelongsToShard("packages/core", null)).toBe(true);
|
|
});
|
|
|
|
test("membership is deterministic for the same key + config", () => {
|
|
const cfg = { index: 2, total: 5 };
|
|
const a = taskBelongsToShard("packages/app-core", cfg);
|
|
const b = taskBelongsToShard("packages/app-core", cfg);
|
|
expect(a).toBe(b);
|
|
});
|
|
|
|
test("a single shard (M=1) owns every task", () => {
|
|
const cfg = { index: 1, total: 1 };
|
|
for (const key of [
|
|
"packages/core",
|
|
"plugins/plugin-openai",
|
|
"packages/ui",
|
|
]) {
|
|
expect(taskBelongsToShard(key, cfg)).toBe(true);
|
|
}
|
|
});
|
|
|
|
test("every task lands in exactly one shard, across many M (partition invariant)", () => {
|
|
const keys = Array.from({ length: 300 }, (_, i) => `packages/pkg-${i}`);
|
|
for (const total of [2, 3, 4, 5, 8, 13]) {
|
|
for (const key of keys) {
|
|
const owners = [];
|
|
for (let index = 1; index <= total; index++) {
|
|
if (taskBelongsToShard(key, { index, total })) {
|
|
owners.push(index);
|
|
}
|
|
}
|
|
// Each key is claimed by precisely one shard — no gaps, no overlaps.
|
|
expect(owners).toHaveLength(1);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("shards are reasonably balanced (no shard is wildly over/under-loaded)", () => {
|
|
const total = 4;
|
|
const keys = Array.from({ length: 800 }, (_, i) => `plugins/plugin-${i}`);
|
|
const counts = new Array(total).fill(0);
|
|
for (const key of keys) {
|
|
for (let index = 1; index <= total; index++) {
|
|
if (taskBelongsToShard(key, { index, total })) {
|
|
counts[index - 1]++;
|
|
}
|
|
}
|
|
}
|
|
expect(counts.reduce((a, b) => a + b, 0)).toBe(keys.length);
|
|
// Even split would be 200/shard; allow generous slack for hash variance.
|
|
for (const count of counts) {
|
|
expect(count).toBeGreaterThan(120);
|
|
expect(count).toBeLessThan(280);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("CI plugin sharding contract", () => {
|
|
const rootPackageJson = JSON.parse(
|
|
readFileSync(new URL("../../../package.json", import.meta.url), "utf8"),
|
|
);
|
|
const testWorkflow = readFileSync(
|
|
new URL("../../../.github/workflows/test.yml", import.meta.url),
|
|
"utf8",
|
|
);
|
|
const qualityWorkflow = readFileSync(
|
|
new URL("../../../.github/workflows/quality.yml", import.meta.url),
|
|
"utf8",
|
|
);
|
|
|
|
test("root test:plugins uses the shard-aware cross-package runner", () => {
|
|
const script = rootPackageJson.scripts["test:plugins"];
|
|
expect(script).toContain("run-all-tests.mjs");
|
|
expect(script).toContain("TEST_PACKAGE_FILTER='\\(plugins/'");
|
|
expect(script).toContain("TEST_SCRIPT_FILTER='^test$'");
|
|
expect(script).toContain("--only=test");
|
|
expect(script).toContain("--no-cloud");
|
|
expect(script).toContain("--concurrency=3");
|
|
});
|
|
|
|
test("Tests workflow shards plugin tests and keeps a stable aggregate check", () => {
|
|
expect(testWorkflow).toMatch(
|
|
/plugin-tests:\s+name:\s+Plugin Tests \(\$\{\{ matrix\.shard \}\}\/4\)[\s\S]*?strategy:[\s\S]*?fail-fast:\s+false[\s\S]*?matrix:[\s\S]*?shard:\s+\[1,\s*2,\s*3,\s*4\]/,
|
|
);
|
|
expect(testWorkflow).toMatch(/TEST_SHARD:\s+\$\{\{ matrix\.shard \}\}\/4/);
|
|
expect(testWorkflow).toMatch(
|
|
/plugin-tests-status:\s+name:\s+Plugin Tests[\s\S]*?needs:[\s\S]*?-\s+plugin-tests/,
|
|
);
|
|
expect(testWorkflow).toMatch(
|
|
/ci-ok:[\s\S]*?needs:[\s\S]*?-\s+plugin-tests-status/,
|
|
);
|
|
});
|
|
|
|
test("Quality workflow gates develop PRs with static scans and lint", () => {
|
|
expect(qualityWorkflow).toMatch(
|
|
/develop-static-gate:[\s\S]*?Prompt secret scan[\s\S]*?check:secrets[\s\S]*?UI determinism self-test[\s\S]*?audit:ui-determinism:self-test[\s\S]*?UI determinism gate[\s\S]*?audit:ui-determinism/,
|
|
);
|
|
expect(qualityWorkflow).toMatch(
|
|
/develop-lint-gate:[\s\S]*?install-command: bun install[\s\S]*?Run lint[\s\S]*?bun run lint/,
|
|
);
|
|
expect(qualityWorkflow).not.toMatch(
|
|
/develop-static-gate:[\s\S]*?Run lint[\s\S]*?bun run lint[\s\S]*?develop-lint-gate:/,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("run-all-tests plan mode", () => {
|
|
const runnerPath = new URL("../run-all-tests.mjs", import.meta.url);
|
|
|
|
function runPlan(args: string[], env: Record<string, string> = {}) {
|
|
return spawnSync(process.execPath, [runnerPath.pathname, ...args], {
|
|
cwd: new URL("../../..", import.meta.url).pathname,
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
TEST_LANE: "pr",
|
|
TEST_PACKAGE_FILTER: "",
|
|
TEST_SCRIPT_FILTER: "",
|
|
TEST_SHARD: "",
|
|
TEST_START_AT: "",
|
|
...env,
|
|
},
|
|
});
|
|
}
|
|
|
|
test("prints a JSON inventory without preparing services or starting package tests", () => {
|
|
const result = runPlan(
|
|
[
|
|
"--plan=json",
|
|
"--only=test",
|
|
"--no-cloud",
|
|
"--filter=^@elizaos/core \\(packages/core\\)#test$",
|
|
],
|
|
// If plan mode regresses and prepares PostgreSQL or spawns package
|
|
// scripts, this stripped PATH makes the side effect visible.
|
|
{ PATH: "" },
|
|
);
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.stderr).toBe("");
|
|
expect(result.stdout).not.toContain("[eliza-test] START");
|
|
expect(result.stdout).not.toContain("PostgreSQL");
|
|
const plan = JSON.parse(result.stdout);
|
|
expect(plan.summary).toMatchObject({
|
|
lane: "pr",
|
|
only: "test",
|
|
noCloud: true,
|
|
taskCount: 1,
|
|
cloudStep: false,
|
|
});
|
|
expect(plan.tasks).toEqual([
|
|
{
|
|
packageName: "@elizaos/core",
|
|
relativeDir: "packages/core",
|
|
scriptName: "test",
|
|
label: "@elizaos/core (packages/core)#test",
|
|
parallelSafe: true,
|
|
},
|
|
]);
|
|
expect(plan.cloudStep).toBeNull();
|
|
});
|
|
|
|
test("bare --plan prints text and keeps the cloud step visible", () => {
|
|
const result = runPlan([
|
|
"--plan",
|
|
"--only=test",
|
|
"--filter=^@elizaos/core \\(packages/core\\)#test$",
|
|
]);
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(result.stdout).toContain("[eliza-test] PLAN lane=pr only=test");
|
|
expect(result.stdout).toContain("[eliza-test] PLAN cloud-step=yes");
|
|
expect(result.stdout).toContain(
|
|
"[eliza-test] PLAN parallel @elizaos/core (packages/core)#test",
|
|
);
|
|
expect(result.stdout).not.toContain("[eliza-test] START");
|
|
});
|
|
});
|