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

385 lines
15 KiB
JavaScript

#!/usr/bin/env node
// Package `test` entry — wraps what used to be a bare `bun test --isolate`.
//
// WHY (#15785): on the Windows CI shard (`windows-ci.yml` app-and-cli lane,
// pinned to Bun canary) the PGlite-backed tenant-db placement-claimer suite
// intermittently wedges in a beforeEach/afterEach hook and then takes the
// WHOLE `bun test` process down with a native crash:
//
// (fail) tenant DB durable placement claimer > (unnamed) [6147.35ms]
// ^ a beforeEach/afterEach hook timed out for this test.
// panic(main thread): Illegal instruction at address 0x7FF6B271CDB0
// oh no: Bun has crashed. This indicates a bug in Bun, not your code.
// error: script "test" exited with code 3
//
// That is a Bun/PGlite (WASM) bug, not a test bug — the byte-identical suite
// passed 11h earlier. Workflow files cannot carry the mitigation (see the
// issue), so it lives here in the test entry:
//
// - non-win32 (default): exactly `bun test --isolate [args]` — unchanged.
// - win32 (or ELIZA_WIN_PGLITE_QUARANTINE=1):
// pass 1 `bun test --isolate` over everything EXCEPT the quarantined
// PGlite tenant-db suites (repeated --path-ignore-patterns).
// pass 2 the quarantined suites in their own child `bun test` process,
// retried a bounded number of times ONLY when the child died
// with a native-crash signature; full crash output is captured
// to a file for the upstream Bun report
// (scripts/bun-pglite-crash-upstream-report.md).
//
// Integrity guarantees (#13620 — no vacuous green):
// - every quarantined suite still RUNS on every platform; this is not a skip
// list, and a missing quarantined file fails the run loudly.
// - a reported test failure (assertion/hook fail with a completed run) is
// NEVER retried — it fails the run immediately.
// - retries are bounded; a persistent crash still fails the run.
// - the main pass keeps plain fail-fast semantics: any non-zero exit fails
// the run (no crash-retry outside the quarantined suites).
//
// Extra CLI args are forwarded verbatim to BOTH passes (flags like --timeout
// or --conditions compose fine; a positional file filter will run matching
// files in both passes — harmless, but scope filters manually if that grates).
import { spawn } from "node:child_process";
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
buildMainPassArgs,
buildQuarantinePassArgs,
classifyBunTestExit,
DEFAULT_QUARANTINED_SUITES,
extractCrashExcerpt,
resolveAttemptTimeoutMs,
resolveMaxAttempts,
resolveQuarantineMode,
shouldRetryQuarantinedSuites,
} from "./run-bun-tests-helpers.mjs";
const here = path.dirname(fileURLToPath(import.meta.url));
const packageDir = path.resolve(here, "..");
const repoRoot = path.resolve(packageDir, "../../..");
const passthroughArgs = process.argv.slice(2);
const UPSTREAM_TEMPLATE = "packages/cloud/shared/scripts/bun-pglite-crash-upstream-report.md";
// Tail cap for captured child output: the panic banner sits at the very end of
// the stream, so a bounded tail always contains it while keeping memory sane.
const OUTPUT_TAIL_CAP_BYTES = 16 * 1024 * 1024;
/**
* Spawn seam: `ELIZA_BUN_TEST_BIN` (+ JSON-array `ELIZA_BUN_TEST_BIN_ARGS`)
* lets the wrapper's own e2e test substitute a scripted stand-in for bun.
* The default spawns `bun` through a shell on win32 — same as the repo's
* test-cloud-run.mjs precedent — so a `.cmd`-shimmed bun still resolves.
*/
function resolveBunCommand(env) {
const bin = env.ELIZA_BUN_TEST_BIN;
if (bin) {
let prefixArgs = [];
if (env.ELIZA_BUN_TEST_BIN_ARGS) {
prefixArgs = JSON.parse(env.ELIZA_BUN_TEST_BIN_ARGS);
if (!Array.isArray(prefixArgs) || prefixArgs.some((a) => typeof a !== "string")) {
throw new Error("[run-bun-tests] ELIZA_BUN_TEST_BIN_ARGS must be a JSON array of strings");
}
}
return { bin, prefixArgs, useShell: false };
}
return { bin: "bun", prefixArgs: [], useShell: process.platform === "win32" };
}
// With shell:true node joins args into one cmd.exe command line without
// quoting. CI passes no extra args; guard local invocations against args that
// cmd.exe would misparse instead of silently mangling them.
const SHELL_SAFE_ARG = /^[A-Za-z0-9_\-./\\=:*?,[\]@+]+$/;
function assertShellSafe(args) {
const offender = args.find((arg) => !SHELL_SAFE_ARG.test(arg));
if (offender !== undefined) {
console.error(
`[run-bun-tests] argument ${JSON.stringify(offender)} is not safe to pass through the win32 shell spawn; ` +
"quote-free args only (no spaces or cmd metacharacters).",
);
process.exit(1);
}
}
function appendCapped(buffer, chunk) {
const combined = buffer + chunk;
return combined.length > OUTPUT_TAIL_CAP_BYTES
? combined.slice(combined.length - OUTPUT_TAIL_CAP_BYTES)
: combined;
}
function killTree(child) {
if (process.platform === "win32" && typeof child.pid === "number") {
// taskkill /t reaches bun even when the spawn went through a cmd.exe shell.
const killer = spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
stdio: "ignore",
});
killer.on("error", () => child.kill("SIGKILL"));
} else {
child.kill("SIGKILL");
}
}
/**
* Run one `bun test` child. Output streams through to the parent stdio live
* and a bounded tail is captured for classification/crash reports.
*
* `inherit` (optional) hands the parent stdio straight to the child (no
* capture) — used by the quarantine-off path for exact legacy behavior.
* `onOutput` (optional) sees every raw chunk (main-pass exclusion scan).
* `timeoutMs` (optional) arms a wall-clock watchdog; on expiry the child tree
* is killed and the result carries `watchdogFired: true`.
*/
function runBunTest(testArgs, { inherit, onOutput, timeoutMs } = {}) {
const { bin, prefixArgs, useShell } = resolveBunCommand(process.env);
const argv = [...prefixArgs, "test", ...testArgs];
if (useShell) assertShellSafe([bin, ...argv]);
return new Promise((resolve, reject) => {
const stdio = inherit ? "inherit" : ["ignore", "pipe", "pipe"];
// In shell mode, pass ONE pre-joined command line (every token was just
// validated quote-free) — spawn(cmd, args, {shell:true}) concatenates
// unescaped anyway and node 24 warns about it (DEP0190).
const child = useShell
? spawn([bin, ...argv].join(" "), {
cwd: packageDir,
env: process.env,
stdio,
shell: true,
})
: spawn(bin, argv, {
cwd: packageDir,
env: process.env,
stdio,
});
let output = "";
let watchdogFired = false;
let watchdog;
if (timeoutMs !== undefined) {
watchdog = setTimeout(() => {
watchdogFired = true;
console.error(
`[run-bun-tests] watchdog: child exceeded ${timeoutMs}ms wall clock (wedged process — the #15785 crash wedged for ~64 minutes); killing process tree pid=${child.pid}`,
);
killTree(child);
}, timeoutMs);
watchdog.unref?.();
}
const consume = (stream, sink) => {
stream.on("data", (chunk) => {
const text = chunk.toString();
sink.write(chunk);
output = appendCapped(output, text);
onOutput?.(text);
});
};
if (!inherit) {
consume(child.stdout, process.stdout);
consume(child.stderr, process.stderr);
}
child.on("error", (error) => {
if (watchdog) clearTimeout(watchdog);
reject(error);
});
child.on("close", (status, signal) => {
if (watchdog) clearTimeout(watchdog);
resolve({ status, signal, output, watchdogFired });
});
});
}
function resolveQuarantinedSuites(env) {
const raw = env.ELIZA_PGLITE_QUARANTINE_SUITES;
if (!raw) return DEFAULT_QUARANTINED_SUITES;
// Test seam (the wrapper e2e test points it at fixture suites). Note this
// only changes WHICH suites get the isolated-retry treatment — every listed
// suite still runs and must pass, so it cannot be used to skip anything.
const parsed = JSON.parse(raw);
if (
!Array.isArray(parsed) ||
parsed.length === 0 ||
parsed.some((entry) => typeof entry !== "string")
) {
throw new Error(
"[run-bun-tests] ELIZA_PGLITE_QUARANTINE_SUITES must be a non-empty JSON array of strings",
);
}
return parsed;
}
function writeCrashCapture({ attempt, maxAttempts, args, result, reason }) {
const dir = process.env.ELIZA_PGLITE_CRASH_DIR ?? path.join(repoRoot, ".tmp", "bun-pglite-crash");
mkdirSync(dir, { recursive: true });
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const file = path.join(dir, `tenant-db-pglite-crash-${stamp}-attempt${attempt}.log`);
const header = [
"# Bun native crash capture — elizaOS/eliza issue #15785",
`# date: ${new Date().toISOString()}`,
`# platform: ${process.platform} ${process.arch}`,
`# command: bun test ${args.join(" ")}`,
`# cwd: ${packageDir}`,
`# attempt: ${attempt}/${maxAttempts}`,
`# exit: status=${result.status ?? "null"} signal=${result.signal ?? "none"} watchdogFired=${result.watchdogFired}`,
`# classification: ${reason}`,
`# upstream report template: ${UPSTREAM_TEMPLATE}`,
"",
].join("\n");
writeFileSync(file, header + result.output);
return file;
}
async function main() {
const quarantineOn = resolveQuarantineMode({
platform: process.platform,
env: process.env,
});
if (!quarantineOn) {
// Behavior-identical to the previous `"test": "bun test --isolate"`.
const result = await runBunTest(["--isolate", ...passthroughArgs], {
inherit: true,
});
if (result.signal) {
console.error(`[run-bun-tests] bun test terminated by signal ${result.signal}`);
process.exit(1);
}
process.exit(result.status ?? 1);
}
const quarantinedSuites = resolveQuarantinedSuites(process.env);
const maxAttempts = resolveMaxAttempts(process.env);
const attemptTimeoutMs = resolveAttemptTimeoutMs(process.env);
// Fail loud on a stale quarantine list (e.g. a renamed suite) — otherwise
// the quarantined pass would silently run nothing (#13620).
const missing = quarantinedSuites.filter((suite) => !existsSync(path.join(packageDir, suite)));
if (missing.length > 0) {
console.error(
`[run-bun-tests] quarantined suite(s) not found on disk:\n ${missing.join("\n ")}\n` +
"Update DEFAULT_QUARANTINED_SUITES in scripts/run-bun-tests-helpers.mjs to match the layout.",
);
process.exit(1);
}
console.log(
`[run-bun-tests] win32 PGlite quarantine active (#15785): ${quarantinedSuites.length} suite(s) run isolated with native-crash retry (max ${maxAttempts} attempts, ${attemptTimeoutMs}ms watchdog):\n` +
quarantinedSuites.map((suite) => ` - ${suite}`).join("\n"),
);
// ---- pass 1: everything except the quarantined suites ------------------
const mainArgs = buildMainPassArgs(quarantinedSuites, passthroughArgs);
console.log(`[run-bun-tests] main pass: bun test ${mainArgs.join(" ")}`);
const quarantinedBasenames = quarantinedSuites.map((suite) => path.posix.basename(suite));
let exclusionLeak = false;
let scanCarry = "";
const mainResult = await runBunTest(mainArgs, {
onOutput: (text) => {
const window = scanCarry + text;
if (quarantinedBasenames.some((name) => window.includes(name))) {
exclusionLeak = true;
}
scanCarry = window.slice(-512);
},
});
const mainOk = mainResult.status === 0 && !mainResult.signal;
if (!mainOk) {
console.error(
`[run-bun-tests] main pass FAILED (status=${mainResult.status ?? "null"}, signal=${mainResult.signal ?? "none"}) — this is outside the quarantined suites and is NOT retried.`,
);
}
if (exclusionLeak) {
console.warn(
"[run-bun-tests] WARNING: a quarantined suite name appeared in main-pass output — --path-ignore-patterns may no longer exclude it (bun behavior change?). The suite still runs isolated below; failures stay loud either way.",
);
}
// ---- pass 2: the quarantined suites, isolated, crash-retried -----------
const quarantineArgs = buildQuarantinePassArgs(quarantinedSuites, passthroughArgs);
let quarantineOk = false;
let quarantineStatus = 1;
const crashCaptures = [];
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
console.log(
`[run-bun-tests] quarantined PGlite pass (attempt ${attempt}/${maxAttempts}): bun test ${quarantineArgs.join(" ")}`,
);
const result = await runBunTest(quarantineArgs, { timeoutMs: attemptTimeoutMs });
const classification = result.watchdogFired
? {
kind: "native-crash",
reason: `watchdog kill after ${attemptTimeoutMs}ms wall clock (wedged process, #15785 signature)`,
}
: classifyBunTestExit(result);
if (classification.kind === "pass") {
quarantineOk = true;
quarantineStatus = 0;
if (attempt > 1) {
console.warn(
`[run-bun-tests] quarantined suites PASSED on attempt ${attempt}/${maxAttempts} after ${attempt - 1} native-crash attempt(s) — this is the #15785 Bun-canary/PGlite flake, not a test bug.\n` +
`[run-bun-tests] report it upstream with the capture(s) below (template: ${UPSTREAM_TEMPLATE}):\n` +
crashCaptures.map((file) => ` - ${file}`).join("\n"),
);
}
break;
}
if (classification.kind === "test-failure") {
quarantineStatus = result.status ?? 1;
console.error(
`[run-bun-tests] quarantined suites reported a GENUINE test failure (${classification.reason}) — failing immediately, native-crash retry does not apply.`,
);
break;
}
// native crash
const captureFile = writeCrashCapture({
attempt,
maxAttempts,
args: quarantineArgs,
result,
reason: classification.reason,
});
crashCaptures.push(captureFile);
console.error(
`[run-bun-tests] NATIVE CRASH in quarantined PGlite pass (attempt ${attempt}/${maxAttempts}): ${classification.reason}\n` +
`[run-bun-tests] full output captured to: ${captureFile}\n` +
`[run-bun-tests] crash excerpt:\n${extractCrashExcerpt(result.output)}`,
);
if (shouldRetryQuarantinedSuites(classification, attempt, maxAttempts)) {
console.warn(
`[run-bun-tests] retrying quarantined suites (native-crash signature only; a reported test failure would NOT be retried)…`,
);
continue;
}
quarantineStatus = result.status ?? 1;
if (quarantineStatus === 0) quarantineStatus = 1;
console.error(
`[run-bun-tests] quarantined suites crashed natively on all ${maxAttempts} attempt(s) — failing the run. Report upstream with the captures (template: ${UPSTREAM_TEMPLATE}):\n` +
crashCaptures.map((file) => ` - ${file}`).join("\n"),
);
break;
}
if (mainOk && quarantineOk) {
process.exit(0);
}
if (!mainOk) {
const status = mainResult.status ?? 1;
process.exit(status === 0 ? 1 : status);
}
process.exit(quarantineStatus === 0 ? 1 : quarantineStatus);
}
main().catch((error) => {
console.error("[run-bun-tests] fatal:", error);
process.exit(1);
});