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
339 lines
13 KiB
JavaScript
339 lines
13 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
// Cross-platform replacement for the previous `test:cloud` shell pipeline,
|
|
// which used `printf '...\n'` (broken under bun's embedded shell on Windows
|
|
// — outputs literal `n` instead of newlines) and required POSIX-shell
|
|
// `$OLDPWD` semantics.
|
|
//
|
|
// The pure helpers below (walkTests, chunkByBudget, formatBatchFiles,
|
|
// writeSyncAll) are exported so test-cloud-run.test.mjs can exercise them
|
|
// directly; the batch-orchestration side effects (spawning `bun test`,
|
|
// writing bunfig.toml) only run when this file is invoked as the entry
|
|
// script, guarded by the `main()` call at the bottom.
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
readdirSync,
|
|
statSync,
|
|
writeFileSync,
|
|
writeSync,
|
|
} from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { shouldNormalizeBunStatus99 } from "./test-cloud-run-helpers.mjs";
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = path.resolve(here, "../..");
|
|
const stagingDir = path.join(repoRoot, ".tmp", "cloud-unit-bun");
|
|
|
|
// `test/` is the api e2e harness (own `test:e2e` lane + a live server); the
|
|
// rest is build output / vendored deps that carry no unit lane.
|
|
export const EXCLUDED_API_DIRS = new Set([
|
|
"test",
|
|
"node_modules",
|
|
"dist",
|
|
".turbo",
|
|
]);
|
|
// Vendored deps and build output under any non-api root: never a unit target.
|
|
export const EXCLUDED_DIRS = new Set(["node_modules", "dist", ".turbo"]);
|
|
|
|
export function walkTests(dir, excluded) {
|
|
const out = [];
|
|
for (const entry of readdirSync(dir)) {
|
|
if (excluded.has(entry)) continue;
|
|
const full = path.join(dir, entry);
|
|
if (statSync(full).isDirectory()) out.push(...walkTests(full, excluded));
|
|
else if (/\.(test|spec)\.tsx?$/.test(entry)) out.push(full);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// Batch size bounds per-process memory; the char cap keeps each argv under
|
|
// Windows' ~8 KiB cmd.exe command-line ceiling (the spawn goes through cmd.exe
|
|
// there — `shell: true` below — to resolve bun's `.cmd` shim). Whichever limit
|
|
// a file hits first closes the current batch.
|
|
export const MAX_FILES_PER_BATCH = 80;
|
|
export const MAX_ARGS_CHARS_WIN32 = 6000;
|
|
export const MAX_ARGS_CHARS_POSIX = 100000;
|
|
|
|
export function chunkByBudget(files, maxFilesPerBatch, maxArgsChars) {
|
|
const batches = [];
|
|
let current = [];
|
|
let chars = 0;
|
|
for (const file of files) {
|
|
const cost = file.length + 1;
|
|
if (
|
|
current.length > 0 &&
|
|
(current.length >= maxFilesPerBatch || chars + cost > maxArgsChars)
|
|
) {
|
|
batches.push(current);
|
|
current = [];
|
|
chars = 0;
|
|
}
|
|
current.push(file);
|
|
chars += cost;
|
|
}
|
|
if (current.length > 0) batches.push(current);
|
|
return batches;
|
|
}
|
|
|
|
export function formatBatchFiles(batch, root) {
|
|
return batch.map((file) => ` - ${path.relative(root, file)}`).join("\n");
|
|
}
|
|
|
|
// Write straight to the stdout/stderr file descriptors. `process.stdout.write`
|
|
// buffers asynchronously when the sink is a back-pressured pipe (the GitHub
|
|
// Actions log collector is exactly that): each per-batch `bun test` dump queues
|
|
// in Node's internal stream buffer, the synchronous `spawnSync` loop never
|
|
// yields to drain it, and the final `process.exit()` then discards every
|
|
// un-flushed byte. That silently swallowed the batch-10 failure diagnostic AND
|
|
// the earlier batches' summaries, surfacing as a bare `exited with code 1` with
|
|
// no reported failing test. `fs.writeSync` blocks until the bytes hit the fd,
|
|
// so nothing can be truncated by exit. Retry on EAGAIN for non-blocking fds.
|
|
export function writeSyncAll(fd, text) {
|
|
if (!text) return;
|
|
const buffer = Buffer.from(text, "utf8");
|
|
let offset = 0;
|
|
while (offset < buffer.length) {
|
|
try {
|
|
offset += writeSync(fd, buffer, offset, buffer.length - offset);
|
|
} catch (error) {
|
|
if (error && error.code === "EAGAIN") continue;
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
// The unit lane runs with NO database service (Cloud Tests → unit-tests calls
|
|
// cloud-setup-test-env without setup-db). DB-touching suites are built to fall
|
|
// back to in-process PGlite, but that fallback only engages when DATABASE_URL is
|
|
// empty or `pglite://` — and the Cloud Tests workflow sets a real
|
|
// `postgresql://…:5432` URL at the workflow level, which every job inherits.
|
|
// Left as-is, that ambient URL points every suite at a Postgres socket nothing
|
|
// is listening on: the isolated-PGlite guards disable themselves (their loud
|
|
// "pglite applied" assertions fail) and the raw-SQL suites hit ECONNREFUSED.
|
|
// Pin the unit lane to in-process PGlite so it is self-contained; suites that
|
|
// need a networked DB opt out via SKIP_DB_DEPENDENT.
|
|
export function buildTestEnv(baseEnv) {
|
|
return {
|
|
...baseEnv,
|
|
DATABASE_URL: "pglite://memory",
|
|
TEST_DATABASE_URL: "pglite://memory",
|
|
SKIP_DB_DEPENDENT: "1",
|
|
SKIP_SERVER_CHECK: "true",
|
|
};
|
|
}
|
|
|
|
// NOTE: keep in sync with the package layout. The #9917 reorg moved these from
|
|
// packages/cloud-shared -> packages/cloud/shared and packages/cloud-api ->
|
|
// packages/cloud/api; the stale paths made `bun test` target nonexistent dirs,
|
|
// so the cloud unit suite (incl. the IAC inference hot-path tests) silently ran
|
|
// nothing = false-green gate. cloud-tests.yml already triggers on
|
|
// `packages/scripts/cloud/**` and `packages/cloud/services/**`; the routing
|
|
// (model-routing resolver) and infra (IaC / static-config) packages carry
|
|
// pure, DB-free unit suites that ran on no PR lane until they were added here
|
|
// alongside the cloud-tests.yml `paths:` update.
|
|
export function computeTestRoots(root) {
|
|
return {
|
|
cloudSharedSrc: path.join(root, "packages", "cloud", "shared", "src"),
|
|
cloudApiRoot: path.join(root, "packages", "cloud", "api"),
|
|
cloudScriptsTests: path.join(root, "packages", "scripts", "cloud"),
|
|
cloudRoutingTests: path.join(root, "packages", "cloud", "routing", "src"),
|
|
cloudInfraTests: path.join(root, "packages", "cloud", "infra", "tests"),
|
|
cloudServicesRoot: path.join(root, "packages", "cloud", "services"),
|
|
};
|
|
}
|
|
|
|
// `bun test <nonexistent-dir>` exits 0 with no tests run, so a stale path
|
|
// (e.g. after a package move) would turn this gate into a silent false-green.
|
|
// Injectable `existsFn` lets the check run against a real or a fake filesystem.
|
|
export function findMissingRoots(testRoots, existsFn) {
|
|
return Object.entries(testRoots)
|
|
.filter(([, dir]) => !existsFn(dir))
|
|
.map(([name, dir]) => `${name} -> ${dir}`);
|
|
}
|
|
|
|
// The full unit set is ~700 files. bun's `--isolate` gives each file a fresh
|
|
// global but keeps ONE process, so JS heap plus external (pglite/WASM) memory
|
|
// accumulates monotonically across the whole run — RSS climbs past 7 GB. On the
|
|
// memory-bounded self-hosted runner that tips into GC-thrash/OOM, and because
|
|
// the drizzle-kit `pushSchema` introspect ("Pulling schema from database…")
|
|
// builds large full-schema JSON snapshots, the run consistently wedged there
|
|
// and the runner reclaimed the job (SIGTERM → exit 143). Splitting the file set
|
|
// into sequential fresh `bun test` processes (runBatches below) bounds peak
|
|
// memory to one batch: each process starts cold, runs its slice, and frees
|
|
// everything on exit before the next starts.
|
|
//
|
|
// `spawnBatch` is injected so tests can drive the failure-classification logic
|
|
// (status/signal handling, the pglite status-99 normalization, spawn errors)
|
|
// without shelling out to a real `bun test` run. Returns true if any batch's
|
|
// failure should fail the overall gate.
|
|
export function runBatches(
|
|
batches,
|
|
{ spawnBatch, stagingDir, env, repoRoot, writeOut, writeErr },
|
|
) {
|
|
let anyFailed = false;
|
|
for (let i = 0; i < batches.length; i++) {
|
|
const batch = batches[i];
|
|
writeOut(
|
|
`[test:cloud] batch ${i + 1}/${batches.length} — ${batch.length} files\n`,
|
|
);
|
|
const result = spawnBatch(batch, { cwd: stagingDir, env });
|
|
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
if (result.stdout) writeOut(result.stdout);
|
|
if (result.stderr) writeErr(result.stderr);
|
|
if (result.error) {
|
|
// spawnSync failure (e.g. ENOBUFS from maxBuffer, spawn ENOENT). Surface it
|
|
// and keep the exit deferred so the drained batch output is not truncated.
|
|
writeErr(
|
|
`[test:cloud] batch ${i + 1}/${batches.length} spawn error: ${
|
|
result.error.stack ?? String(result.error)
|
|
}\n`,
|
|
);
|
|
return true;
|
|
}
|
|
// Run every batch even after a failure so one broken suite doesn't mask the
|
|
// rest; aggregate into a single non-zero exit for the gate.
|
|
const status = result.status;
|
|
const signal = result.signal;
|
|
if ((status ?? 1) !== 0 || signal) {
|
|
if (shouldNormalizeBunStatus99({ status, signal, output })) {
|
|
writeErr(
|
|
`[test:cloud] batch ${i + 1}/${batches.length} exited with Bun status ${status} ` +
|
|
"after reporting no failed tests; treating as pass (known Bun/PGlite exitCode pollution).\n",
|
|
);
|
|
continue;
|
|
}
|
|
anyFailed = true;
|
|
writeErr(
|
|
`[test:cloud] batch ${i + 1}/${batches.length} exited non-zero ` +
|
|
`(status=${status ?? "null"}, signal=${signal ?? "none"})\n` +
|
|
`[test:cloud] files in failed batch:\n${formatBatchFiles(batch, repoRoot)}\n`,
|
|
);
|
|
}
|
|
}
|
|
return anyFailed;
|
|
}
|
|
|
|
function main() {
|
|
mkdirSync(stagingDir, { recursive: true });
|
|
|
|
writeFileSync(
|
|
path.join(stagingDir, "bunfig.toml"),
|
|
"[test]\ntimeout = 120000\ncoverage = false\n",
|
|
);
|
|
|
|
const env = buildTestEnv(process.env);
|
|
const testRoots = computeTestRoots(repoRoot);
|
|
const {
|
|
cloudSharedSrc,
|
|
cloudApiRoot,
|
|
cloudScriptsTests,
|
|
cloudRoutingTests,
|
|
cloudInfraTests,
|
|
cloudServicesRoot,
|
|
} = testRoots;
|
|
|
|
const missing = findMissingRoots(testRoots, existsSync);
|
|
if (missing.length > 0) {
|
|
console.error(
|
|
`[test:cloud] test root(s) not found — the gate would silently run no tests:\n ${missing.join("\n ")}\n` +
|
|
"Update packages/scripts/test-cloud-run.mjs to match the current package layout.",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Also sweeps colocated `<resource>/route.test.ts` unit tests that live
|
|
// OUTSIDE __tests__/ (billing, cron, credits, webhooks, …). Excludes `test/`
|
|
// (the api e2e harness: its own `test:e2e` lane + a live server) and build
|
|
// output.
|
|
const cloudApiUnitTests = walkTests(cloudApiRoot, EXCLUDED_API_DIRS).sort();
|
|
|
|
// If the api root exists but holds no colocated unit tests, the gate would
|
|
// silently run zero api tests — fail loud so a layout change can't quietly
|
|
// drop the lane.
|
|
if (cloudApiUnitTests.length === 0) {
|
|
console.error(
|
|
`[test:cloud] no colocated cloud/api unit tests found under ${cloudApiRoot} — ` +
|
|
"the gate would silently run zero api tests. Update packages/scripts/test-cloud-run.mjs " +
|
|
"to match the current package layout.",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const cloudServicesTests = walkTests(cloudServicesRoot, EXCLUDED_DIRS).sort();
|
|
|
|
// Same fail-loud guard as cloud/api: if a reorg moves the services suites,
|
|
// this gate must break instead of silently running zero services tests. The
|
|
// gateway suites also run in their dedicated workflows
|
|
// (cloud-gateway-discord/-webhook); they are self-contained bun:test files,
|
|
// so the duplicate coverage here is cheap and keeps this gate layout-proof.
|
|
if (cloudServicesTests.length === 0) {
|
|
console.error(
|
|
`[test:cloud] no cloud/services tests found under ${cloudServicesRoot} — ` +
|
|
"the gate would silently run zero services tests. Update packages/scripts/test-cloud-run.mjs " +
|
|
"to match the current package layout.",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const allTestFiles = [
|
|
...walkTests(cloudSharedSrc, EXCLUDED_DIRS),
|
|
...cloudApiUnitTests,
|
|
...walkTests(cloudScriptsTests, EXCLUDED_DIRS),
|
|
...walkTests(cloudRoutingTests, EXCLUDED_DIRS),
|
|
...walkTests(cloudInfraTests, EXCLUDED_DIRS),
|
|
...cloudServicesTests,
|
|
];
|
|
if (allTestFiles.length === 0) {
|
|
console.error(
|
|
"[test:cloud] enumerated zero test files across all roots — the gate would " +
|
|
"silently pass. Update packages/scripts/test-cloud-run.mjs to match the layout.",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const maxArgsChars =
|
|
process.platform === "win32" ? MAX_ARGS_CHARS_WIN32 : MAX_ARGS_CHARS_POSIX;
|
|
const batches = chunkByBudget(
|
|
allTestFiles,
|
|
MAX_FILES_PER_BATCH,
|
|
maxArgsChars,
|
|
);
|
|
|
|
const writeOut = (text) => writeSyncAll(1, text);
|
|
const writeErr = (text) => writeSyncAll(2, text);
|
|
|
|
const spawnBatch = (batch, { cwd, env: batchEnv }) =>
|
|
spawnSync("bun", ["test", ...batch, "--timeout", "120000", "--isolate"], {
|
|
cwd,
|
|
env: batchEnv,
|
|
encoding: "utf8",
|
|
maxBuffer: 128 * 1024 * 1024,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
shell: process.platform === "win32",
|
|
});
|
|
|
|
const anyFailed = runBatches(batches, {
|
|
spawnBatch,
|
|
stagingDir,
|
|
env,
|
|
repoRoot,
|
|
writeOut,
|
|
writeErr,
|
|
});
|
|
|
|
// Use process.exitCode + natural return instead of process.exit(): the latter
|
|
// tears the process down before any still-queued async stdout/stderr flushes,
|
|
// which is what erased the failure diagnostics above (#16062). All batch
|
|
// output already went out synchronously via writeSync, so the exit code
|
|
// alone remains here.
|
|
if (anyFailed) process.exitCode = 1;
|
|
}
|
|
|
|
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
main();
|
|
}
|