Files
wehub-resource-sync 426e9eeabd
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
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) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

360 lines
9.3 KiB
TypeScript

/**
* Live e2e driving the SelfControl blocker against a spawned dev stack. Gated on
* ELIZA_LIVE_TEST.
*/
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import { setTimeout as sleep } from "node:timers/promises";
import { afterAll, beforeAll, expect, it } from "vitest";
import { describeIf } from "../../../packages/test/helpers/conditional-tests.ts";
import { req } from "../../../packages/test/helpers/http";
const LIVE_TESTS_ENABLED = process.env.ELIZA_LIVE_TEST === "1";
const REPO_ROOT = path.resolve(import.meta.dirname, "..", "..", "..", "..");
type StartedDevStack = {
apiPort: number;
close: () => Promise<void>;
getLogTail: () => string;
hostsFilePath: string;
uiPort: number;
};
async function getFreePort(): Promise<number> {
return await new Promise((resolve, reject) => {
const server = net.createServer();
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
const address = server.address();
if (!address || typeof address === "string") {
server.close();
reject(new Error("Could not allocate a loopback port"));
return;
}
server.close((error) => {
if (error) reject(error);
else resolve(address.port);
});
});
});
}
async function waitForChildExit(
child: ChildProcessWithoutNullStreams,
timeoutMs: number,
): Promise<boolean> {
if (child.exitCode != null) {
return true;
}
return await new Promise((resolve) => {
const timeout = setTimeout(() => {
cleanup();
resolve(false);
}, timeoutMs);
const handleExit = () => {
cleanup();
resolve(true);
};
const cleanup = () => {
clearTimeout(timeout);
child.off("exit", handleExit);
child.off("close", handleExit);
};
child.once("exit", handleExit);
child.once("close", handleExit);
});
}
async function waitForJsonPredicate<T>(
url: string,
predicate: (value: T) => boolean,
timeoutMs = 180_000,
): Promise<T> {
const deadline = Date.now() + timeoutMs;
let lastError: unknown = null;
while (Date.now() < deadline) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Request failed (${response.status}): ${url}`);
}
const data = (await response.json()) as T;
if (predicate(data)) {
return data;
}
} catch (error) {
lastError = error;
}
await sleep(1_000);
}
throw lastError instanceof Error
? lastError
: new Error(`Timed out waiting for ${url}`);
}
async function waitForTextPredicate(
url: string,
predicate: (text: string) => boolean,
timeoutMs = 180_000,
): Promise<string> {
const deadline = Date.now() + timeoutMs;
let lastError: unknown = null;
while (Date.now() < deadline) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Request failed (${response.status}): ${url}`);
}
const text = await response.text();
if (predicate(text)) {
return text;
}
} catch (error) {
lastError = error;
}
await sleep(1_000);
}
throw lastError instanceof Error
? lastError
: new Error(`Timed out waiting for ${url}`);
}
async function waitForHostsBlock(
hostsFilePath: string,
websites: string[],
timeoutMs = 60_000,
): Promise<string> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const hosts = await readFile(hostsFilePath, "utf8");
if (websites.every((website) => hosts.includes(website))) {
return hosts;
}
await sleep(1_000);
}
throw new Error(
`Timed out waiting for hosts file block: ${websites.join(", ")}`,
);
}
async function removeTempRoot(tempRoot: string): Promise<void> {
const deadline = Date.now() + 30_000;
let lastError: unknown = null;
while (Date.now() < deadline) {
try {
await rm(tempRoot, { recursive: true, force: true });
return;
} catch (error) {
lastError = error;
const code =
typeof error === "object" && error && "code" in error
? String((error as NodeJS.ErrnoException).code ?? "")
: "";
if (!["EBUSY", "ENOTEMPTY", "EPERM"].includes(code)) {
throw error;
}
}
await sleep(250);
}
throw lastError instanceof Error
? lastError
: new Error(`Timed out removing temp directory: ${tempRoot}`);
}
async function startDevStack(): Promise<StartedDevStack> {
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "eliza-dev-stack-"));
const stateDir = path.join(tempRoot, "state");
const configPath = path.join(tempRoot, "eliza.json");
const hostsFilePath = path.join(tempRoot, "hosts");
const apiPort = await getFreePort();
const uiPort = await getFreePort();
const logs: string[] = [];
await mkdir(stateDir, { recursive: true });
await writeFile(hostsFilePath, "127.0.0.1 localhost\n", "utf8");
await writeFile(
configPath,
`${JSON.stringify(
{
logging: { level: "info" },
plugins: {
allow: ["selfcontrol"],
},
},
null,
2,
)}\n`,
"utf8",
);
const child = spawn("bun", ["run", "dev"], {
cwd: REPO_ROOT,
env: {
...process.env,
CI: "1",
ELIZA_CONFIG_PATH: configPath,
ELIZA_DEV_ONCHAIN: "0",
ELIZA_DISABLE_LOCAL_EMBEDDINGS: "1",
ELIZA_API_PORT: String(apiPort),
ELIZA_UI_PORT: String(uiPort),
ELIZA_PORT: String(uiPort),
ELIZA_STATE_DIR: stateDir,
ALLOW_NO_DATABASE: "",
SELFCONTROL_HOSTS_FILE_PATH: hostsFilePath,
WEBSITE_BLOCKER_HOSTS_FILE_PATH: hostsFilePath,
},
stdio: ["pipe", "pipe", "pipe"],
});
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk: string) => logs.push(chunk));
child.stderr.on("data", (chunk: string) => logs.push(chunk));
try {
await waitForJsonPredicate<{ ready?: boolean; runtime?: string }>(
`http://127.0.0.1:${apiPort}/api/health`,
(value) => value.ready === true && value.runtime === "ok",
);
await waitForTextPredicate(
`http://127.0.0.1:${uiPort}`,
(text) =>
text.includes('<div id="root">') || text.includes("<!doctype html>"),
);
} catch (error) {
const logTail = logs.join("").slice(-12_000);
if (child.exitCode == null) {
child.kill("SIGKILL");
await waitForChildExit(child, 5_000);
}
await removeTempRoot(tempRoot);
throw new Error(
`Dev stack failed to start: ${error instanceof Error ? error.message : String(error)}\n${logTail}`,
);
}
return {
apiPort,
getLogTail: () => logs.join("").slice(-12_000),
hostsFilePath,
uiPort,
close: async () => {
if (child.exitCode == null) {
child.kill("SIGTERM");
const exited = await waitForChildExit(child, 10_000);
if (!exited && child.exitCode == null) {
child.kill("SIGKILL");
await waitForChildExit(child, 5_000);
}
}
await removeTempRoot(tempRoot);
},
};
}
describeIf(LIVE_TESTS_ENABLED)("Live: website blocker dev launcher", () => {
let stack: StartedDevStack | undefined;
beforeAll(async () => {
stack = await startDevStack();
}, 240_000);
afterAll(async () => {
if (stack) {
await stack.close();
}
});
it("boots bun run dev with website blocker APIs and UI available", async () => {
const uiResponse = await fetch(`http://127.0.0.1:${stack.uiPort}`);
expect(uiResponse.ok).toBe(true);
const permissionResponse = await req(
stack.apiPort,
"GET",
"/api/permissions/website-blocking",
);
expect(permissionResponse.status).toBe(200);
expect(permissionResponse.data).toMatchObject({
id: "website-blocking",
status: "granted",
canRequest: false,
});
const cleanupResponse = await req(
stack.apiPort,
"DELETE",
"/api/website-blocker",
);
expect(cleanupResponse.status).toBe(200);
try {
const startResponse = await req(
stack.apiPort,
"PUT",
"/api/website-blocker",
{
websites: ["x.com", "twitter.com"],
durationMinutes: 5,
},
);
expect(startResponse.status).toBe(200);
expect(startResponse.data).toMatchObject({
success: true,
request: {
websites: ["x.com", "twitter.com"],
durationMinutes: 5,
},
});
const hosts = await waitForHostsBlock(stack.hostsFilePath, [
"x.com",
"twitter.com",
]);
expect(hosts).toContain("0.0.0.0 x.com");
expect(hosts).toContain("0.0.0.0 twitter.com");
expect(hosts).not.toContain("0.0.0.0 api.x.com");
expect(hosts).not.toContain("0.0.0.0 api.twitter.com");
} finally {
const stopResponse = await req(
stack.apiPort,
"DELETE",
"/api/website-blocker",
);
expect(stopResponse.status).toBe(200);
expect(stopResponse.data).toMatchObject({
success: true,
status: {
active: false,
websites: [],
},
});
expect(await readFile(stack.hostsFilePath, "utf8")).toBe(
"127.0.0.1 localhost\n",
);
}
}, 240_000);
});