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

346 lines
9.6 KiB
JavaScript

/**
* Run orchestrator for the test console: executes selected plan tasks as
* child processes, streams status transitions to subscribers, and persists
* every byte of output under the console state dir.
*
* Each task runs as its own `run-all-tests.mjs --filter='^<label>$'`
* invocation rather than one big sweep. That keeps full parity with CI lane
* semantics (lane env, Postgres auto-provision, empty-suite skip logic live
* in run-all-tests, not here) while giving the console per-task exit codes,
* per-task logs, targeted re-runs, and cancellation. Discovery costs ~0.2s
* per spawn — noise against real suite runtimes.
*
* Result classification: run-all-tests prints `[eliza-test] PASS/SKIP/FAIL
* <label>` lines. A self-skipping task exits 3 (the vacuous-green floor with
* --min-tasks=1), so classification parses the log tail first and only falls
* back to the exit code — exit 0 alone is not proof a test ran.
*/
import { spawn } from "node:child_process";
import { EventEmitter } from "node:events";
import fs from "node:fs";
import path from "node:path";
import { REPO_ROOT } from "./registry.mjs";
import {
newRunDir,
recordTaskStatus,
runLogPath,
saveRunManifest,
} from "./store.mjs";
function escapeRegex(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function taskSlug(label) {
return label.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 180);
}
const CLOUD_LABEL = "cloud#test";
export class RunManager extends EventEmitter {
constructor() {
super();
this.current = null;
}
isRunning() {
return Boolean(this.current && !this.current.finished);
}
/**
* Start a run over the given plan tasks. `lane` is "pr" (deterministic,
* keyless) or "live" (post-merge semantics). Extra env carries saved
* credentials + opt-in gates; it is injected only into child processes,
* never into this server's own process.env.
*/
startRun({ tasks, lane = "pr", extraEnv = {}, concurrency = 3 }) {
if (this.isRunning()) throw new Error("a run is already in progress");
const runId = `${new Date().toISOString().replace(/[:.]/g, "-")}-${lane}`;
newRunDir(runId);
const entries = tasks.map((task) => ({
label: task.label,
relativeDir: task.relativeDir,
scriptName: task.scriptName,
parallelSafe: Boolean(task.parallelSafe) && lane === "pr",
status: "queued",
exitCode: null,
durationMs: null,
startedAt: null,
finishedAt: null,
log: path.relative(REPO_ROOT, runLogPath(runId, taskSlug(task.label))),
}));
const run = {
runId,
lane,
startedAt: new Date().toISOString(),
finishedAt: null,
cancelled: false,
finished: false,
tasks: entries,
children: new Set(),
};
this.current = run;
this.persist();
this.emit("event", {
type: "run-started",
runId,
lane,
total: entries.length,
});
void this.drive(run, { extraEnv, concurrency }).catch((error) => {
this.emit("event", {
type: "run-error",
runId,
message: String(error?.message ?? error),
});
});
return runId;
}
async drive(run, { extraEnv, concurrency }) {
const queue = [...run.tasks];
const parallel = queue.filter(
(t) => t.parallelSafe && t.label !== CLOUD_LABEL,
);
const serial = queue.filter(
(t) => !t.parallelSafe || t.label === CLOUD_LABEL,
);
const workers = Array.from(
{ length: Math.max(1, Math.min(concurrency, parallel.length || 1)) },
async () => {
while (parallel.length > 0 && !run.cancelled) {
const entry = parallel.shift();
await this.executeTask(run, entry, extraEnv);
}
},
);
await Promise.all(workers);
for (const entry of serial) {
if (run.cancelled) break;
await this.executeTask(run, entry, extraEnv);
}
for (const entry of run.tasks) {
if (entry.status === "queued") entry.status = "cancelled";
}
run.finished = true;
run.finishedAt = new Date().toISOString();
this.persist();
this.emit("event", {
type: "run-finished",
runId: run.runId,
cancelled: run.cancelled,
counts: countStatuses(run.tasks),
});
}
buildCommand(entry, lane) {
if (entry.label === CLOUD_LABEL) {
return { cmd: "bun", args: ["run", "test:cloud"] };
}
const args = [
"packages/scripts/run-all-tests.mjs",
`--filter=^${escapeRegex(entry.label)}$`,
"--min-tasks=1",
"--no-cloud",
];
// Non-`test` scripts (test:e2e, test:live, …) need --all so the extra
// script lanes are collected; plain `test` stays scoped to --only=test.
args.push(entry.scriptName === "test" ? "--only=test" : "--all");
return { cmd: process.execPath, args };
}
laneEnv(lane) {
if (lane === "live") {
return {
TEST_LANE: "post-merge",
ELIZA_LIVE_TEST: "1",
ELIZA_REAL_APIS: "1",
};
}
return {
TEST_LANE: "pr",
ELIZA_LIVE_TEST: "0",
SCENARIO_USE_LLM_PROXY: "1",
};
}
executeTask(run, entry, extraEnv) {
return new Promise((resolvePromise) => {
const logFile = path.join(REPO_ROOT, entry.log);
const logStream = fs.createWriteStream(logFile);
const { cmd, args } = this.buildCommand(entry, run.lane);
entry.status = "running";
entry.startedAt = new Date().toISOString();
this.persist();
this.emit("event", {
type: "task",
runId: run.runId,
label: entry.label,
status: "running",
});
const started = Date.now();
// Own process group so cancel can kill the whole spawn tree (vitest
// workers, browsers, emulators) rather than just the wrapper.
const child = spawn(cmd, args, {
cwd: REPO_ROOT,
env: {
...process.env,
...this.laneEnv(run.lane),
...extraEnv,
FORCE_COLOR: "0",
},
detached: true,
stdio: ["ignore", "pipe", "pipe"],
});
run.children.add(child);
let tail = "";
const capture = (chunk) => {
logStream.write(chunk);
tail = (tail + chunk.toString()).slice(-16_384);
this.emit("event", {
type: "log",
runId: run.runId,
label: entry.label,
chunk: chunk.toString(),
});
};
child.stdout.on("data", capture);
child.stderr.on("data", capture);
child.once("close", (code, signal) => {
run.children.delete(child);
logStream.end();
entry.exitCode = code;
entry.durationMs = Date.now() - started;
entry.finishedAt = new Date().toISOString();
entry.status = classifyResult({
label: entry.label,
code,
signal,
tail,
cancelled: run.cancelled,
});
recordTaskStatus(entry.label, {
status: entry.status,
runId: run.runId,
at: entry.finishedAt,
durationMs: entry.durationMs,
});
this.persist();
this.emit("event", {
type: "task",
runId: run.runId,
label: entry.label,
status: entry.status,
exitCode: code,
durationMs: entry.durationMs,
});
resolvePromise();
});
});
}
cancel() {
const run = this.current;
if (!run || run.finished) return false;
run.cancelled = true;
for (const child of run.children) {
try {
process.kill(-child.pid, "SIGTERM");
} catch {
// error-policy:J6 best-effort teardown — the group may already be gone.
}
}
this.emit("event", { type: "run-cancelling", runId: run.runId });
return true;
}
persist() {
const run = this.current;
if (!run) return;
saveRunManifest(run.runId, {
runId: run.runId,
lane: run.lane,
startedAt: run.startedAt,
finishedAt: run.finishedAt,
cancelled: run.cancelled,
counts: countStatuses(run.tasks),
tasks: run.tasks.map(
({
label,
relativeDir,
scriptName,
status,
exitCode,
durationMs,
startedAt,
finishedAt,
log,
}) => ({
label,
relativeDir,
scriptName,
status,
exitCode,
durationMs,
startedAt,
finishedAt,
log,
}),
),
});
}
snapshot() {
const run = this.current;
if (!run) return null;
return {
runId: run.runId,
lane: run.lane,
startedAt: run.startedAt,
finishedAt: run.finishedAt,
cancelled: run.cancelled,
finished: run.finished,
counts: countStatuses(run.tasks),
tasks: run.tasks.map(({ label, status, exitCode, durationMs }) => ({
label,
status,
exitCode,
durationMs,
})),
};
}
}
export function classifyResult({ label, code, signal, tail, cancelled }) {
if (cancelled) return "cancelled";
if (signal) return "failed";
const labelPattern = escapeRegex(label);
if (new RegExp(`\\[eliza-test\\] FAIL ${labelPattern}`).test(tail))
return "failed";
if (new RegExp(`\\[eliza-test\\] PASS ${labelPattern}`).test(tail))
return "passed";
if (new RegExp(`\\[eliza-test\\] SKIP ${labelPattern}`).test(tail))
return "skipped";
// Exit 3 is run-all-tests' vacuous-green floor: the only task collected
// self-skipped. Anything else nonzero without a status line is a failure.
if (code === 3) return "skipped";
return code === 0 ? "passed" : "failed";
}
export function countStatuses(tasks) {
const counts = {};
for (const task of tasks)
counts[task.status] = (counts[task.status] ?? 0) + 1;
return counts;
}