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
360 lines
9.3 KiB
TypeScript
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);
|
|
});
|