Files
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

354 lines
12 KiB
JavaScript

#!/usr/bin/env node
/**
* Local test console server: one process that serves the console UI, holds
* the operator's saved credentials, and drives test runs with live status.
*
* Launched via `bun run test:console`. Binds 127.0.0.1 only — it holds raw
* API keys and can execute repo code, so it must never listen on a routable
* interface; there is deliberately no auth layer beyond that. The browser
* gets presence/suffix hints for saved secrets, never the values.
*
* Surface:
* GET / console UI (ui/index.html, self-contained)
* GET /api/state registry + connections + run snapshot + history
* GET /api/events SSE stream of run/task/log events
* POST /api/connections/:id save credential values DELETE — remove
* POST /api/connections/:id/verify live probe
* POST /api/gates toggle an opt-in gate
* POST /api/run start a run {mode, lane, labels?, concurrency?}
* POST /api/run/cancel cancel the active run
* GET /api/runs/:id/log?task=<label> persisted log text
* POST /api/cloud/login/start + GET /api/cloud/login/poll device-code login
* GET /oauth/google/start + /oauth/google/callback loopback OAuth
*/
import fs from "node:fs";
import http from "node:http";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { connectionById, connectionStatus } from "./lib/connections.mjs";
import {
completeGoogleFlow,
DEFAULT_CLOUD_BASE_URL,
pollCloudLogin,
refreshGoogleAccessToken,
startCloudLogin,
startGoogleFlow,
} from "./lib/oauth.mjs";
import { buildRegistry, discoverPlan } from "./lib/registry.mjs";
import { RunManager } from "./lib/runner.mjs";
import {
consoleDir,
credentialsToEnv,
listRuns,
loadCredentials,
loadHistory,
loadRun,
loadSettings,
removeConnection,
runLogPath,
saveSettings,
setConnection,
} from "./lib/store.mjs";
import { verifyConnection } from "./lib/verify.mjs";
const here = path.dirname(fileURLToPath(import.meta.url));
const PORT = Number(process.env.ELIZA_TEST_CONSOLE_PORT || 31338);
const HOST = "127.0.0.1";
const runManager = new RunManager();
const sseClients = new Set();
runManager.on("event", (event) => {
const frame = `data: ${JSON.stringify(event)}\n\n`;
for (const client of sseClients) client.write(frame);
});
function json(res, status, body) {
const text = JSON.stringify(body);
res.writeHead(status, {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(text),
});
res.end(text);
}
async function readBody(req) {
let data = "";
for await (const chunk of req) {
data += chunk;
if (data.length > 1_000_000) throw new Error("body too large");
}
return data ? JSON.parse(data) : {};
}
function currentState() {
const settings = loadSettings();
const registry = buildRegistry({
savedCredentials: loadCredentials(),
optInToggles: settings.optInToggles ?? {},
history: loadHistory(),
});
return {
consoleDir: consoleDir(),
registry,
settings: {
optInToggles: settings.optInToggles ?? {},
cloudBaseUrl: settings.cloudBaseUrl ?? DEFAULT_CLOUD_BASE_URL,
},
run: runManager.snapshot(),
runs: listRuns()
.slice(0, 25)
.map(({ runId, lane, startedAt, finishedAt, cancelled, counts }) => ({
runId,
lane,
startedAt,
finishedAt,
cancelled,
counts,
})),
};
}
function selectTasks({ mode, labels }) {
const plan = discoverPlan();
const history = loadHistory();
const all = [...plan.tasks];
if (plan.cloudStep) {
all.push({
label: plan.cloudStep.label,
relativeDir: "packages/cloud",
scriptName: "test",
parallelSafe: false,
});
}
if (mode === "selection") {
const wanted = new Set(labels ?? []);
return all.filter((t) => wanted.has(t.label));
}
if (mode === "failed") {
return all.filter((t) => history[t.label]?.status === "failed");
}
return all;
}
const routes = {
"GET /api/state": (req, res) => json(res, 200, currentState()),
"POST /api/gates": async (req, res) => {
const { key, on } = await readBody(req);
const settings = loadSettings();
settings.optInToggles = { ...settings.optInToggles, [key]: Boolean(on) };
saveSettings(settings);
json(res, 200, { ok: true });
},
"POST /api/run": async (req, res) => {
const {
mode = "all",
lane = "pr",
labels,
concurrency,
} = await readBody(req);
if (runManager.isRunning())
return json(res, 409, { error: "run already in progress" });
const tasks = selectTasks({ mode, labels });
if (tasks.length === 0)
return json(res, 400, { error: "no tasks selected" });
const settings = loadSettings();
const extraEnv = credentialsToEnv();
for (const [gate, on] of Object.entries(settings.optInToggles ?? {})) {
if (on) extraEnv[gate] = "1";
}
// Google access tokens expire hourly; a saved refresh token lets live
// runs re-mint GOOGLE_CALENDAR_ACCESS_TOKEN so the calendar suite stays
// armed without the operator re-pasting a token every hour.
const google = loadCredentials()["google-oauth"] ?? {};
if (
lane === "live" &&
google.GOOGLE_CLIENT_ID &&
google.GOOGLE_CLIENT_SECRET &&
google.GOOGLE_OAUTH_REFRESH_TOKEN
) {
try {
const { accessToken } = await refreshGoogleAccessToken({
clientId: google.GOOGLE_CLIENT_ID,
clientSecret: google.GOOGLE_CLIENT_SECRET,
refreshToken: google.GOOGLE_OAUTH_REFRESH_TOKEN,
});
setConnection("google-calendar", {
GOOGLE_CALENDAR_ACCESS_TOKEN: accessToken,
});
extraEnv.GOOGLE_CALENDAR_ACCESS_TOKEN = accessToken;
} catch (error) {
// error-policy:J4 explicit user-facing degrade — the run proceeds and
// the calendar suite self-skips loudly; the operator sees why here.
runManager.emit("event", {
type: "warning",
message: `Google token refresh failed: ${error?.message ?? error}`,
});
}
}
const runId = runManager.startRun({
tasks,
lane,
extraEnv,
concurrency: Number(concurrency) || 3,
});
json(res, 200, { runId, taskCount: tasks.length });
},
"POST /api/run/cancel": (req, res) =>
json(res, 200, { cancelled: runManager.cancel() }),
"POST /api/cloud/login/start": async (req, res) => {
const settings = loadSettings();
const result = await startCloudLogin({
baseUrl: settings.cloudBaseUrl ?? DEFAULT_CLOUD_BASE_URL,
});
json(res, 200, result);
},
"GET /api/cloud/login/poll": async (req, res, url) => {
const settings = loadSettings();
const result = await pollCloudLogin({
sessionId: url.searchParams.get("sessionId"),
baseUrl: settings.cloudBaseUrl ?? DEFAULT_CLOUD_BASE_URL,
});
if (result.status === "authenticated" && result.apiKey) {
setConnection("eliza-cloud", {
ELIZAOS_CLOUD_API_KEY: result.apiKey,
ELIZA_CLOUD_API_KEY: result.apiKey,
});
}
json(res, 200, { status: result.status });
},
"GET /oauth/google/start": (req, res, url) => {
const saved = loadCredentials()["google-oauth"] ?? {};
const clientId = saved.GOOGLE_CLIENT_ID ?? process.env.GOOGLE_CLIENT_ID;
const clientSecret =
saved.GOOGLE_CLIENT_SECRET ?? process.env.GOOGLE_CLIENT_SECRET;
if (!clientId || !clientSecret) {
return json(res, 400, {
error:
"save GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET on the Google OAuth connection first",
});
}
const { authorizeUrl } = startGoogleFlow({
clientId,
clientSecret,
redirectUri: `http://${HOST}:${PORT}/oauth/google/callback`,
});
res.writeHead(302, { Location: authorizeUrl });
res.end();
},
"GET /oauth/google/callback": async (req, res, url) => {
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const tokens = await completeGoogleFlow({ state, code });
const saved = loadCredentials()["google-oauth"] ?? {};
setConnection("google-oauth", {
...saved,
...(tokens.refreshToken
? { GOOGLE_OAUTH_REFRESH_TOKEN: tokens.refreshToken }
: {}),
});
setConnection("google-calendar", {
GOOGLE_CALENDAR_ACCESS_TOKEN: tokens.accessToken,
});
res.writeHead(200, { "Content-Type": "text/html" });
res.end(
"<body style='font-family:system-ui;background:#111;color:#eee;display:grid;place-items:center;height:100vh'><div>Google connected — you can close this tab and return to the test console.</div></body>",
);
},
};
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://${HOST}:${PORT}`);
try {
if (url.pathname === "/" || url.pathname === "/index.html") {
const html = fs.readFileSync(path.join(here, "ui", "index.html"));
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
return res.end(html);
}
if (url.pathname === "/api/events") {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
res.write(`data: ${JSON.stringify({ type: "hello" })}\n\n`);
sseClients.add(res);
req.on("close", () => sseClients.delete(res));
return;
}
// Connection CRUD + verify (path-parameterized, matched by prefix).
const connectionMatch = url.pathname.match(
/^\/api\/connections\/([a-z0-9-]+)(\/verify)?$/,
);
if (connectionMatch) {
const [, id, verifySuffix] = connectionMatch;
const connection = connectionById(id);
if (!connection)
return json(res, 404, { error: `unknown connection ${id}` });
if (req.method === "POST" && verifySuffix) {
const saved = loadCredentials()[id] ?? {};
const { values } = connectionStatus(connection, saved);
const result = await verifyConnection(connection, values);
return json(res, 200, result);
}
if (req.method === "POST") {
const { values } = await readBody(req);
// Merge over what's saved: the browser only ever sends fields the
// operator typed (it never holds raw saved secrets to echo back),
// so a blank field means "keep the existing value".
const merged = { ...(loadCredentials()[id] ?? {}) };
for (const field of connection.fields) {
const value = values?.[field.key];
if (typeof value === "string" && value.trim() !== "")
merged[field.key] = value.trim();
}
setConnection(id, merged);
return json(res, 200, { ok: true });
}
if (req.method === "DELETE") {
removeConnection(id);
return json(res, 200, { ok: true });
}
}
const logMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/log$/);
if (logMatch && req.method === "GET") {
const runId = decodeURIComponent(logMatch[1]);
const run = loadRun(runId);
const label = url.searchParams.get("task");
const entry = run?.tasks?.find((t) => t.label === label);
if (!entry) return json(res, 404, { error: "unknown run/task" });
const file = runLogPath(runId, path.basename(entry.log, ".log"));
if (!fs.existsSync(file)) return json(res, 404, { error: "log missing" });
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
return fs.createReadStream(file).pipe(res);
}
const handler = routes[`${req.method} ${url.pathname}`];
if (handler) return await handler(req, res, url);
json(res, 404, { error: "not found" });
} catch (error) {
// error-policy:J1 boundary translation — every route failure becomes a
// structured 500 so the UI renders an error state instead of hanging.
json(res, 500, { error: String(error?.message ?? error) });
}
});
server.listen(PORT, HOST, () => {
console.log(`[TestConsole] listening on http://${HOST}:${PORT}`);
console.log(`[TestConsole] state dir: ${consoleDir()}`);
});