Files
elizaos--eliza/packages/scripts/cloud/admin/run-api-worker-e2e.mjs
T
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

311 lines
9.0 KiB
JavaScript

// Drives cloud admin cloud admin run api worker e2e automation with explicit environment and CI invariants.
import { spawn, spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import { createConnection } from "node:net";
import { homedir } from "node:os";
import { delimiter, resolve } from "node:path";
import { config } from "dotenv";
for (const envPath of [
resolve(".env"),
resolve(".env.local"),
resolve(".env.test"),
]) {
config({ path: envPath });
}
const port = Number.parseInt(process.env.TEST_API_PORT || "8787", 10);
const baseUrl = process.env.TEST_API_BASE_URL || `http://127.0.0.1:${port}`;
const startupTimeoutMs = 120_000;
const pollIntervalMs = 500;
const testAuthSecret =
process.env.PLAYWRIGHT_TEST_AUTH_SECRET || "playwright-local-auth-secret";
const repoRoot = resolve(import.meta.dirname, "../../../..");
const rmRecursiveScript = resolve(
repoRoot,
"packages",
"scripts",
"rm-path-recursive.mjs",
);
const pglitePort = Number.parseInt(process.env.TEST_PGLITE_PORT || "55432", 10);
const pgliteHost = process.env.PGLITE_HOST || "127.0.0.1";
const pgliteDataDir =
process.env.TEST_PGLITE_DATA_DIR || ".eliza/.pgdata-worker-e2e";
const pgliteMaxConnections =
process.env.TEST_PGLITE_MAX_CONNECTIONS ||
process.env.PGLITE_MAX_CONNECTIONS ||
"16";
const defaultE2eEnv = {
CRON_SECRET: process.env.CRON_SECRET || "test-cron-secret",
INTERNAL_SECRET: process.env.INTERNAL_SECRET || "test-internal-secret",
AGENT_TEST_BOOTSTRAP_ADMIN: process.env.AGENT_TEST_BOOTSTRAP_ADMIN || "true",
PLAYWRIGHT_TEST_AUTH: process.env.PLAYWRIGHT_TEST_AUTH || "true",
PLAYWRIGHT_TEST_AUTH_SECRET: testAuthSecret,
};
function bunExecutable() {
if (process.env.BUN && existsSync(process.env.BUN)) return process.env.BUN;
// Windows: HOME is unset, bun installs to `%USERPROFILE%\.bun\bin\bun.exe`
// and PATH uses `;` as delimiter; POSIX: `~/.bun/bin/bun` + `:` delimiter.
const isWindows = process.platform === "win32";
const bunBasename = isWindows ? "bun.exe" : "bun";
const userHome =
process.env.HOME || process.env.USERPROFILE || homedir() || "";
const homeBun = resolve(userHome, ".bun", "bin", bunBasename);
if (existsSync(homeBun)) return homeBun;
const pathBun = process.env.PATH?.split(delimiter)
.map((entry) => resolve(entry, bunBasename))
.find((candidate) => existsSync(candidate));
if (pathBun) return pathBun;
if (process.env.npm_execpath?.includes("bun"))
return process.env.npm_execpath;
return "bun";
}
function parsePGliteDataDir(url) {
if (!url?.startsWith("pglite://")) return null;
const dataDir = url.slice("pglite://".length);
if (!dataDir || dataDir === "memory") return null;
return dataDir;
}
function rmRecursive(targetPath) {
const result = spawnSync(process.execPath, [rmRecursiveScript, targetPath], {
cwd: repoRoot,
stdio: "inherit",
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(
`[worker-e2e] recursive cleanup failed for ${targetPath} with exit code ${result.status ?? "unknown"}`,
);
}
}
async function tcpOk(host, port) {
return new Promise((resolve) => {
const socket = createConnection({ host, port });
socket.setTimeout(1_000);
socket.once("connect", () => {
socket.end();
resolve(true);
});
socket.once("timeout", () => {
socket.destroy();
resolve(false);
});
socket.once("error", () => {
socket.destroy();
resolve(false);
});
});
}
async function healthOk() {
try {
const res = await fetch(`${baseUrl}/api/health`, {
signal: AbortSignal.timeout(5_000),
});
return res.ok;
} catch {
return false;
}
}
async function waitForTcp(child, host, port) {
const startedAt = Date.now();
while (Date.now() - startedAt < startupTimeoutMs) {
if (await tcpOk(host, port)) return;
if (child.exitCode !== null) {
throw new Error(`PGlite TCP server exited with code ${child.exitCode}`);
}
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
}
throw new Error(
`PGlite TCP server did not become reachable at ${host}:${port}`,
);
}
async function waitForHealth(child) {
const startedAt = Date.now();
while (Date.now() - startedAt < startupTimeoutMs) {
if (await healthOk()) return;
if (child.exitCode !== null) {
throw new Error(`Worker dev server exited with code ${child.exitCode}`);
}
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
}
throw new Error(
`Worker dev server did not become healthy at ${baseUrl}/api/health`,
);
}
async function main() {
const bun = bunExecutable();
let child = null;
let pgliteChild = null;
const testEnv = {
...process.env,
...defaultE2eEnv,
};
if (!testEnv.TEST_DATABASE_URL && testEnv.DATABASE_URL) {
console.warn(
"[worker-e2e] Ignoring DATABASE_URL for Worker e2e. Set TEST_DATABASE_URL to run against an explicit test database.",
);
delete testEnv.DATABASE_URL;
}
const configuredDbUrl = testEnv.TEST_DATABASE_URL || "";
const usingPGliteTcpBridge =
!configuredDbUrl || configuredDbUrl.startsWith("pglite://");
let workerExit = null;
let result;
try {
if (usingPGliteTcpBridge) {
const pgliteDatabaseUrl = `postgresql://postgres@${pgliteHost}:${pglitePort}/postgres`;
const dataDir = parsePGliteDataDir(configuredDbUrl) || pgliteDataDir;
const shouldResetDefaultPGlite =
!configuredDbUrl &&
testEnv.TEST_PGLITE_PERSIST !== "1" &&
Boolean(dataDir);
testEnv.DATABASE_URL = pgliteDatabaseUrl;
testEnv.TEST_DATABASE_URL = pgliteDatabaseUrl;
const pgliteAlreadyRunning = await tcpOk(pgliteHost, pglitePort);
if (shouldResetDefaultPGlite) {
if (pgliteAlreadyRunning) {
throw new Error(
`Default Worker e2e PGlite server is already running at ${pgliteHost}:${pglitePort}; stop it before running isolated tests, or set TEST_PGLITE_PERSIST=1 to reuse it.`,
);
}
rmRecursive(resolve(dataDir));
}
if (!pgliteAlreadyRunning) {
pgliteChild = spawn(
bun,
["run", "packages/scripts/cloud/admin/dev/pglite-server.ts"],
{
stdio: ["ignore", "inherit", "inherit"],
env: {
...testEnv,
PGLITE_HOST: pgliteHost,
PGLITE_PORT: String(pglitePort),
PGLITE_MAX_CONNECTIONS: pgliteMaxConnections,
...(dataDir ? { PGLITE_DATA_DIR: dataDir } : {}),
},
},
);
await waitForTcp(pgliteChild, pgliteHost, pglitePort);
await new Promise((resolve) => setTimeout(resolve, 1_000));
}
} else {
testEnv.DATABASE_URL = configuredDbUrl;
testEnv.TEST_DATABASE_URL = configuredDbUrl;
}
const migrateResult = spawnSync(bun, ["run", "db:cloud:migrate"], {
stdio: ["ignore", "inherit", "inherit"],
env: testEnv,
});
if (migrateResult.error) {
throw migrateResult.error;
}
if (migrateResult.status !== 0) {
throw new Error(
`db:cloud:migrate exited with code ${migrateResult.status}`,
);
}
const workerEnv = {
...testEnv,
NODE_ENV: "development",
TEST_API_BASE_URL: baseUrl,
TEST_BASE_URL: baseUrl,
TEST_REUSE_SERVER: "1",
};
if (!(await healthOk())) {
const syncResult = spawnSync(
bun,
["run", "packages/scripts/cloud/admin/sync-api-dev-vars.ts"],
{
stdio: "inherit",
env: workerEnv,
},
);
if (syncResult.error) {
throw syncResult.error;
}
if (syncResult.status !== 0) {
throw new Error(
`sync-api-dev-vars exited with code ${syncResult.status}`,
);
}
child = spawn(
bun,
[
"run",
"--cwd",
"packages/cloud/api",
"wrangler",
"dev",
"--ip",
"127.0.0.1",
"--port",
String(port),
"--local",
"--persist-to",
`.wrangler/state-e2e-${port}`,
"--live-reload=false",
"--show-interactive-dev-session=false",
],
{
stdio: ["pipe", "inherit", "inherit"],
env: workerEnv,
},
);
child.on("exit", (code, signal) => {
workerExit = { code, signal };
});
await waitForHealth(child);
}
result = spawnSync(
bun,
["run", "--cwd", "packages/cloud/api", "test:e2e"],
{
stdio: "inherit",
env: workerEnv,
},
);
} finally {
if (child) {
child.kill("SIGTERM");
}
if (pgliteChild) {
pgliteChild.kill("SIGTERM");
}
}
if (result?.error) {
throw result.error;
}
if (result?.status !== 0 && workerExit) {
console.error(
`[worker-e2e] Wrangler exited during test run: code=${workerExit.code} signal=${workerExit.signal}`,
);
}
process.exit(result?.status ?? 1);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});