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
ci / test (push) Waiting to run
ci / lint-and-format (push) Waiting to run
ci / build (push) Waiting to run
ci / dev-startup (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
gitleaks / gitleaks (push) Waiting to run
Markdown Links / Relative Markdown Links (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
Quality (Extended) / Homepage Build (PR smoke) (push) Waiting to run
Quality (Extended) / Comment-only diff guard (push) Waiting to run
Quality (Extended) / Format + Type Safety Ratchet (push) Waiting to run
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Waiting to run
Quality (Extended) / Develop Gate (lint) (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

456 lines
13 KiB
TypeScript

/** Runs live core runtime smoke coverage against real provider or orchestration surfaces. */
import assert from "node:assert/strict";
import fs from "node:fs";
import { createServer, type Server } from "node:http";
import net from "node:net";
import path from "node:path";
import type { AgentRuntime } from "@elizaos/core";
import { createTestRuntime } from "../../src/testing/pglite-runtime.ts";
const {
AcpService,
cleanForChat,
listAgentsAction,
sendToAgentAction,
spawnAgentAction,
} = await import("@elizaos/plugin-agent-orchestrator");
type Framework = "claude" | "codex";
type Mode = "sequential" | "web";
const KEEP_ARTIFACTS = process.env.ELIZA_KEEP_LIVE_ARTIFACTS === "1";
async function createRuntime(settings: Record<string, unknown> = {}): Promise<{
runtime: AgentRuntime;
cleanup: () => Promise<void>;
}> {
const { runtime, cleanup } = await createTestRuntime({
characterName: "TaskAgentLiveSmoke",
});
const originalGetSetting = runtime.getSetting.bind(runtime);
runtime.getSetting = ((key: string) =>
settings[key] ??
originalGetSetting(key) ??
process.env[key]) as typeof runtime.getSetting;
return { runtime, cleanup };
}
function createMessage(content: Record<string, unknown> = {}) {
return {
id: `msg-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
userId: "live-user",
roomId: "live-room",
createdAt: Date.now(),
content,
};
}
async function wait(ms: number): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, ms));
}
async function fetchTextIfAvailable(url: string): Promise<string | null> {
try {
const response = await fetch(url);
if (!response.ok) {
return null;
}
return await response.text();
} catch {
// The local HTTP server is expected to refuse connections until the agent starts it.
return null;
}
}
async function waitFor(
check: () => Promise<boolean>,
timeoutMs: number,
intervalMs: number,
): Promise<void> {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
if (await check()) return;
await wait(intervalMs);
}
throw new Error(`Timed out after ${timeoutMs}ms`);
}
function ensureLiveBaseDir(): string {
const baseDir = path.join(process.cwd(), ".tmp-live");
fs.mkdirSync(baseDir, { recursive: true });
return baseDir;
}
function createWorkdir(agentType: Framework, label: string): string {
return fs.mkdtempSync(
path.join(ensureLiveBaseDir(), `agent-orchestrator-${agentType}-${label}-`),
);
}
async function getFreePort(): Promise<number> {
const server = net.createServer();
await new Promise<void>((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", () => resolve());
});
const address = server.address();
if (!address || typeof address === "string") {
server.close();
throw new Error("Failed to allocate an ephemeral port");
}
const port = address.port;
await new Promise<void>((resolve) => server.close(() => resolve()));
return port;
}
async function startReferenceServer(html: string): Promise<{
server: Server;
url: string;
}> {
const port = await getFreePort();
const server = createServer((_, res) => {
res.statusCode = 200;
res.setHeader("content-type", "text/html; charset=utf-8");
res.end(html);
});
await new Promise<void>((resolve, reject) => {
server.once("error", reject);
server.listen(port, "127.0.0.1", () => resolve());
});
return {
server,
url: `http://127.0.0.1:${port}/reference.html`,
};
}
function sawTaskCompletion(
events: Array<{ event: string; data: unknown }>,
startIndex: number,
): boolean {
return events
.slice(startIndex)
.some(
(entry) => entry.event === "task_complete" || entry.event === "completed",
);
}
async function waitForTrackedSession(
runtime: AgentRuntime,
sessionId: string,
expectedAgentType: Framework,
): Promise<void> {
let listResult:
| Awaited<ReturnType<typeof listAgentsAction.handler>>
| undefined;
await waitFor(
async () => {
listResult = await listAgentsAction.handler(
runtime,
createMessage({}) as never,
);
if (!listResult?.success) {
return false;
}
const sessions = Array.isArray(listResult.data?.sessions)
? listResult.data.sessions
: [];
const tasks = Array.isArray(listResult.data?.tasks)
? listResult.data.tasks
: [];
return (
sessions.some((entry) => entry.id === sessionId) &&
tasks.some(
(entry) =>
entry.sessionId === sessionId &&
entry.agentType === expectedAgentType,
)
);
},
45_000,
1_000,
);
assert.ok(listResult?.text.includes(sessionId));
assert.ok(listResult?.text.includes(expectedAgentType));
}
async function runSequentialSmoke(agentType: Framework): Promise<void> {
const workdir = createWorkdir(agentType, "reuse");
const { runtime, cleanup } = await createRuntime({ SERVER_PORT: "31337" });
// Start the ACP service and register it under its real serviceType so the
// TASKS actions (which resolve it via getAcpService -> runtime.getService
// ("ACP_SUBPROCESS_SERVICE")) spawn into the SAME instance this script reads
// output from. (Replaces the removed PTYService; same start+register pattern.)
const service = await AcpService.start(runtime);
runtime.services.set(AcpService.serviceType, [service]);
const events: Array<{ event: string; data: unknown }> = [];
const unsubscribe = service.onSessionEvent((_sessionId, event, data) => {
events.push({ event, data });
});
const firstFileName = `FIRST_${agentType.toUpperCase()}.txt`;
const secondFileName = `SECOND_${agentType.toUpperCase()}.txt`;
const firstFilePath = path.join(workdir, firstFileName);
const secondFilePath = path.join(workdir, secondFileName);
const firstSentinel = `LIVE_REUSE_${agentType.toUpperCase()}_FIRST_DONE`;
const secondSentinel = `LIVE_REUSE_${agentType.toUpperCase()}_SECOND_DONE`;
try {
const [preflight] = await service.checkAvailableAgents([agentType]);
assert.equal(preflight?.installed, true);
const spawnResult = await spawnAgentAction.handler(
runtime,
createMessage({
agentType,
workdir,
task:
`Create a file named ${firstFileName} in the current directory containing exactly "${agentType}-first". ` +
`Then print exactly "${firstSentinel}". Do not ask follow-up questions.`,
}) as never,
undefined,
{},
undefined,
);
assert.equal(spawnResult?.success, true);
assert.ok(spawnResult?.data?.sessionId);
const sessionId = String(spawnResult?.data?.sessionId);
await waitForTrackedSession(runtime, sessionId, agentType);
const firstTaskEventStart = events.length;
await waitFor(
async () => {
const sessionInfo = await service.getSession(sessionId);
if (!sessionInfo) {
throw new Error(
"session disappeared before completing the first task",
);
}
const recentLoginRequired = events.findLast(
(entry) => entry.event === "login_required",
);
if (recentLoginRequired) {
const details = recentLoginRequired.data as { instructions?: string };
throw new Error(
details.instructions || "framework authentication is required",
);
}
if (
sessionInfo.status === "stopped" ||
sessionInfo.status === "error"
) {
const output = await service.getSessionOutput(sessionId, 200);
throw new Error(
`session ended early with status ${sessionInfo.status}. Output: ${output.slice(-600)}`,
);
}
if (!fs.existsSync(firstFilePath)) return false;
const fileText = fs.readFileSync(firstFilePath, "utf8").trim();
if (fileText !== `${agentType}-first`) return false;
const output = cleanForChat(await service.getSessionOutput(sessionId));
return (
output.includes(firstSentinel) ||
sawTaskCompletion(events, firstTaskEventStart)
);
},
6 * 60 * 1000,
3000,
);
const secondTaskEventStart = events.length;
const sendResult = await sendToAgentAction.handler(
runtime,
createMessage({
sessionId,
task:
`Now create a second file named ${secondFileName} containing exactly "${agentType}-second". ` +
`Then print exactly "${secondSentinel}". Stay available for more work afterward and do not ask follow-up questions.`,
}) as never,
undefined,
{},
undefined,
);
assert.equal(sendResult?.success, true);
await waitFor(
async () => {
if (!fs.existsSync(secondFilePath)) return false;
const fileText = fs.readFileSync(secondFilePath, "utf8").trim();
if (fileText !== `${agentType}-second`) return false;
const output = cleanForChat(await service.getSessionOutput(sessionId));
return (
output.includes(secondSentinel) ||
sawTaskCompletion(events, secondTaskEventStart)
);
},
6 * 60 * 1000,
3000,
);
const finalList = await listAgentsAction.handler(
runtime,
createMessage({}) as never,
);
assert.equal(finalList?.success, true);
assert.ok(finalList?.text.includes(sessionId));
} finally {
unsubscribe();
await service.stop();
await cleanup();
if (!KEEP_ARTIFACTS) {
fs.rmSync(workdir, { recursive: true, force: true });
}
}
}
async function runWebSmoke(agentType: Framework): Promise<void> {
const workdir = createWorkdir(agentType, "web");
const { runtime, cleanup } = await createRuntime({ SERVER_PORT: "31337" });
const service = await AcpService.start(runtime);
runtime.services.set(AcpService.serviceType, [service]);
const events: Array<{ event: string; data: unknown }> = [];
const unsubscribe = service.onSessionEvent((_sessionId, event, data) => {
events.push({ event, data });
});
const agentPort = await getFreePort();
const serveSentinel = `LIVE_WEB_${agentType.toUpperCase()}_READY`;
const reference = await startReferenceServer(`<!doctype html>
<html>
<body>
<h1>Benchmark Ready</h1>
<p>Task agents stay reusable.</p>
<p>Codex and Claude Code should both handle research and serving tasks.</p>
</body>
</html>`);
try {
const [preflight] = await service.checkAvailableAgents([agentType]);
assert.equal(preflight?.installed, true);
const spawnResult = await spawnAgentAction.handler(
runtime,
createMessage({
agentType,
workdir,
task:
`Open the reference page at ${reference.url} and read it using your web or browser tools. ` +
`Create an index.html in the current directory that includes the exact phrases "Benchmark Ready" and "Task agents stay reusable." ` +
`Then start a local HTTP server in the background from the current directory with ` +
`"python3 -m http.server ${agentPort} >/tmp/${serveSentinel}.log 2>&1 & echo $! > server.pid", ` +
`print exactly "${serveSentinel}", and keep the server available until I stop you. ` +
`Do not ask follow-up questions.`,
}) as never,
undefined,
{},
undefined,
);
assert.equal(spawnResult?.success, true);
assert.ok(spawnResult?.data?.sessionId);
const sessionId = String(spawnResult?.data?.sessionId);
await waitForTrackedSession(runtime, sessionId, agentType);
const webTaskEventStart = events.length;
await waitFor(
async () => {
const sessionInfo = await service.getSession(sessionId);
if (!sessionInfo) {
throw new Error("session disappeared before completing the web task");
}
const recentLoginRequired = events.findLast(
(entry) => entry.event === "login_required",
);
if (recentLoginRequired) {
const details = recentLoginRequired.data as { instructions?: string };
throw new Error(
details.instructions || "framework authentication is required",
);
}
if (
sessionInfo.status === "stopped" ||
sessionInfo.status === "error"
) {
const output = await service.getSessionOutput(sessionId, 200);
throw new Error(
`web task ended early with status ${sessionInfo.status}. Output: ${output.slice(-600)}`,
);
}
const html = await fetchTextIfAvailable(
`http://127.0.0.1:${agentPort}/index.html`,
);
if (!html) return false;
return (
html.includes("Benchmark Ready") &&
html.includes("Task agents stay reusable.") &&
(cleanForChat(await service.getSessionOutput(sessionId)).includes(
serveSentinel,
) ||
sawTaskCompletion(events, webTaskEventStart))
);
},
6 * 60 * 1000,
3000,
);
const finalList = await listAgentsAction.handler(
runtime,
createMessage({}) as never,
);
assert.equal(finalList?.success, true);
assert.ok(finalList?.text.includes(sessionId));
} finally {
unsubscribe();
await new Promise<void>((resolve) =>
reference.server.close(() => resolve()),
);
await service.stop();
await cleanup();
if (!KEEP_ARTIFACTS) {
fs.rmSync(workdir, { recursive: true, force: true });
}
}
}
async function main(): Promise<void> {
const frameworkIndex = process.argv.indexOf("--framework");
const modeIndex = process.argv.indexOf("--mode");
const framework =
frameworkIndex !== -1
? (process.argv[frameworkIndex + 1] as Framework)
: null;
const mode = modeIndex !== -1 ? (process.argv[modeIndex + 1] as Mode) : null;
if (
(framework !== "claude" && framework !== "codex") ||
(mode !== "sequential" && mode !== "web")
) {
throw new Error(
"Usage: task-agent-live-smoke.ts --framework <claude|codex> --mode <sequential|web>",
);
}
if (mode === "sequential") {
await runSequentialSmoke(framework);
} else {
await runWebSmoke(framework);
}
console.log(
"[task-agent-live-smoke] PASS",
JSON.stringify({ framework, mode }),
);
}
try {
await main();
process.exit(0);
} catch (error) {
console.error("[task-agent-live-smoke] FAIL");
console.error(error);
process.exit(1);
}