Files
elizaos--eliza/packages/core/test/live/research-task-thread-live.ts
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

411 lines
9.8 KiB
TypeScript

/** Runs live core runtime smoke coverage against real provider or orchestration surfaces. */
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import type { AgentRuntime } from "@elizaos/core";
import { createTestRuntime } from "../../src/testing/pglite-runtime.ts";
type CodexEvent =
| {
type: "item.completed";
item?: {
type?: string;
action?: {
type?: string;
url?: string;
query?: string;
queries?: string[];
};
};
}
| {
type: "item.started";
item?: {
type?: string;
action?: {
type?: string;
url?: string;
query?: string;
queries?: string[];
};
};
}
| {
type: "turn.completed";
}
| Record<string, unknown>;
type CommandResult = {
exitCode: number;
stdout: string;
stderr: string;
};
const RESEARCH_PROMPT =
"Use web search only. Find the official Playwright browser support page and the official Puppeteer supported browsers page. " +
"Return a concise Markdown report with sections Summary, Tradeoffs, and Sources. Include at least two source URLs and no code blocks.";
const KEEP_ARTIFACTS = process.env.ELIZA_KEEP_LIVE_ARTIFACTS === "1";
let runtime: AgentRuntime | undefined;
let cleanupRuntime: (() => Promise<void>) | undefined;
let reportDir: string | undefined;
function runCommand(
command: string,
args: string[],
options: {
cwd: string;
env?: NodeJS.ProcessEnv;
timeoutMs: number;
},
): Promise<CommandResult> {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd: options.cwd,
env: options.env ?? process.env,
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
let settled = false;
const timer = setTimeout(() => {
if (settled) {
return;
}
settled = true;
child.kill("SIGKILL");
reject(
new Error(
`${command} timed out after ${Math.round(options.timeoutMs / 1000)} seconds`,
),
);
}, options.timeoutMs);
child.stdout.on("data", (chunk: Buffer | string) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk: Buffer | string) => {
stderr += chunk.toString();
});
child.on("error", (error) => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
reject(error);
});
child.on("close", (code) => {
if (settled) {
return;
}
settled = true;
clearTimeout(timer);
resolve({
exitCode: code ?? -1,
stdout,
stderr,
});
});
});
}
function parseCodexEvents(stdout: string): CodexEvent[] {
return stdout
.split("\n")
.map((line) => line.trim())
.filter((line) => line.startsWith("{") && line.endsWith("}"))
.map((line) => {
try {
return JSON.parse(line) as CodexEvent;
} catch {
return null;
}
})
.filter((event): event is CodexEvent => event !== null);
}
async function cleanup(): Promise<void> {
try {
await cleanupRuntime?.();
} catch {}
if (reportDir) {
if (KEEP_ARTIFACTS) {
console.log(
"[research-task-thread-live] preserving artifacts",
JSON.stringify({ reportDir }),
);
} else {
fs.rmSync(reportDir, { recursive: true, force: true });
}
}
}
async function main(): Promise<void> {
({ runtime, cleanup: cleanupRuntime } = await createTestRuntime());
const { SwarmCoordinator } = await import(
"@elizaos/plugin-agent-orchestrator"
);
const coordinator = new SwarmCoordinator(runtime);
const thread = await coordinator.createTaskThread({
title: "Live Codex research report",
originalRequest: RESEARCH_PROMPT,
kind: "research",
metadata: {
source: "research-task-thread-live",
provider: "codex-subscription",
},
});
reportDir = fs.mkdtempSync(path.join(os.tmpdir(), "eliza-research-thread-"));
const transcriptPath = path.join(
reportDir,
"codex-research-transcript.jsonl",
);
const finalMessagePath = path.join(reportDir, "codex-final-message.md");
const reportPath = path.join(reportDir, "deep-research-report.md");
const sessionId = `research-codex-${Date.now()}`;
const startedAt = Date.now();
await coordinator.taskRegistry.registerSession({
threadId: thread.id,
sessionId,
framework: "codex",
providerSource: "subscription",
label: "codex-live-research",
originalTask: thread.originalRequest,
workdir: reportDir,
status: "active",
taskDelivered: true,
registeredAt: startedAt,
lastActivityAt: startedAt,
metadata: {
kind: "research",
mode: "codex-search-exec",
},
});
await coordinator.taskRegistry.appendEvent({
threadId: thread.id,
sessionId,
eventType: "research_started",
summary: "Started live Codex research run",
data: {
provider: "codex",
searchEnabled: true,
},
});
const codexEnv = { ...process.env };
delete codexEnv.OPENAI_API_KEY;
delete codexEnv.OPENAI_BASE_URL;
const commandResult = await runCommand(
"codex",
[
"-a",
"never",
"--search",
"-c",
'model_reasoning_effort="low"',
"exec",
"--skip-git-repo-check",
"--cd",
reportDir,
"--sandbox",
"workspace-write",
"--json",
"-o",
finalMessagePath,
RESEARCH_PROMPT,
],
{
cwd: reportDir,
env: codexEnv,
timeoutMs: 12 * 60_000,
},
);
fs.writeFileSync(transcriptPath, commandResult.stdout, "utf8");
assert.equal(
commandResult.exitCode,
0,
`Expected codex research run to exit 0, got ${commandResult.exitCode}\n${commandResult.stderr}`,
);
assert.ok(
fs.existsSync(finalMessagePath),
"Expected Codex research run to write a final message file",
);
const finalMessage = fs.readFileSync(finalMessagePath, "utf8").trim();
assert.ok(
finalMessage.length > 0,
"Expected Codex research run to return a non-empty report",
);
const events = parseCodexEvents(commandResult.stdout);
const searchEvents = events.filter(
(event) =>
(event.type === "item.completed" || event.type === "item.started") &&
event.item?.type === "web_search" &&
(event.item.action?.type === "search" ||
(typeof event.item.action?.query === "string" &&
event.item.action.query.length > 0) ||
(Array.isArray(event.item.action?.queries) &&
event.item.action.queries.length > 0) ||
(typeof event.item.action?.url === "string" &&
event.item.action.url.length > 0)),
);
assert.ok(
searchEvents.length > 0,
"Expected Codex research run to record at least one real web_search event",
);
assert.match(
finalMessage,
/https?:\/\//,
"Expected Codex research report to include source URLs",
);
const uniqueSearchQueries = [
...new Set(
searchEvents.flatMap((event) => {
const action = event.item?.action;
if (!action) {
return [];
}
const queries = Array.isArray(action.queries) ? action.queries : [];
return [
...(typeof action.query === "string" && action.query.length > 0
? [action.query]
: []),
...queries,
];
}),
),
];
const reportBody = [
"# Live Codex Research Report",
"",
finalMessage,
"",
"## Observed Search Queries",
...uniqueSearchQueries.map((query) => `- ${query}`),
].join("\n");
fs.writeFileSync(reportPath, reportBody, "utf8");
await coordinator.taskRegistry.recordTranscript({
threadId: thread.id,
sessionId,
direction: "stdout",
content: finalMessage,
});
await coordinator.taskRegistry.recordArtifact({
threadId: thread.id,
sessionId,
artifactType: "research_report",
title: "Live Codex research report",
path: reportPath,
mimeType: "text/markdown",
metadata: {
provider: "codex",
searchEventCount: searchEvents.length,
searchQueries: uniqueSearchQueries,
},
});
await coordinator.taskRegistry.recordArtifact({
threadId: thread.id,
sessionId,
artifactType: "transcript",
title: "Codex research transcript",
path: transcriptPath,
mimeType: "application/jsonl",
metadata: {
provider: "codex",
stderrLength: commandResult.stderr.length,
},
});
await coordinator.taskRegistry.updateSession(sessionId, {
status: "completed",
completionSummary: finalMessage.slice(0, 240),
lastActivityAt: Date.now(),
});
await coordinator.taskRegistry.updateThreadSummary(
thread.id,
finalMessage.slice(0, 400),
);
await coordinator.taskRegistry.appendEvent({
threadId: thread.id,
sessionId,
eventType: "research_completed",
summary: "Completed live Codex research run",
data: {
provider: "codex",
searchEventCount: searchEvents.length,
queryCount: uniqueSearchQueries.length,
},
});
const detail = await coordinator.getTaskThread(thread.id);
assert.ok(detail, "Expected task thread detail after research run");
assert.equal(detail.kind, "research");
assert.ok(
detail.sessions.some(
(session) =>
session.sessionId === sessionId && session.status === "completed",
),
"Expected the research run to persist as a completed session",
);
assert.ok(
detail.artifacts.some(
(artifact) =>
artifact.sessionId === sessionId &&
artifact.artifactType === "research_report" &&
artifact.path === reportPath,
),
"Expected the research report artifact to be attached to the task thread",
);
assert.ok(
detail.artifacts.some(
(artifact) =>
artifact.sessionId === sessionId &&
artifact.artifactType === "transcript" &&
artifact.path === transcriptPath,
),
"Expected the research transcript artifact to be attached to the task thread",
);
assert.ok(
detail.events.some((event) => event.eventType === "research_completed"),
"Expected the task thread to record research completion",
);
console.log(
"[research-task-thread-live] PASS",
JSON.stringify({
threadId: thread.id,
sessionId,
reportDir,
reportPath,
transcriptPath,
searchEventCount: searchEvents.length,
queryCount: uniqueSearchQueries.length,
}),
);
}
try {
await main();
await cleanup();
process.exit(0);
} catch (error) {
console.error("[research-task-thread-live] FAIL");
console.error(error);
await cleanup();
process.exit(1);
}