Files
LangBot/skills/scripts/e2e/workspace-plugin-pages-smoke.mjs
T
2026-07-19 22:46:01 +08:00

187 lines
6.2 KiB
JavaScript

#!/usr/bin/env node
import { join } from "node:path";
import { env } from "node:process";
import {
apiJson,
createBrowser,
ensureAuthenticatedBrowser,
ensureEvidence,
evidencePaths,
exitCode,
loadEnvFiles,
localIsoWithOffset,
safeScreenshot,
scanBrowserDiagnostics,
writeResult,
} from "./lib/langbot-e2e.mjs";
await loadEnvFiles();
const caseId = env.LBS_CASE_ID || "workspace-plugin-pages-smoke";
const paths = evidencePaths(caseId);
await ensureEvidence(paths);
const startedAt = new Date();
const frontendUrl = env.LANGBOT_FRONTEND_URL || "";
const backendUrl = env.LANGBOT_BACKEND_URL || "";
const targets = [
{
id: "control-plane",
author: "langbot",
plugin: "agent-control-plane",
page: "control-plane",
signal: "Agent Control Plane",
endpoint: "/health",
method: "GET",
},
{
id: "memory-console",
author: "langbot-team",
plugin: "LongTermMemory",
page: "memory_console",
signal: "Memory Console",
endpoint: "/summary",
method: "GET",
},
{
id: "langrag-observability",
author: "langbot-team",
plugin: "LangRAG",
page: "observability",
signal: "LangRAG Observability",
endpoint: "/snapshot",
method: "GET",
},
{
id: "skill-authoring",
author: "huanghuoguoguo",
plugin: "skill-authoring",
page: "authoring",
signal: "Skill Authoring",
endpoint: "/health",
method: "GET",
},
];
const result = {
source: "automation",
case_id: caseId,
run_id: paths.runId,
started_at: startedAt.toISOString(),
started_at_local: localIsoWithOffset(startedAt),
finished_at: "",
finished_at_local: "",
status: "fail",
reason: "",
frontend_url: frontendUrl,
backend_url: backendUrl,
pages: [],
browser_diagnostics: null,
evidence: {
console_log: paths.consoleLog,
network_log: paths.networkLog,
screenshot: paths.screenshot,
automation_result_json: paths.automationResultJson,
result_json: paths.resultJson,
},
evidence_collected: ["ui", "screenshot", "console", "network", "api_diagnostic"],
};
let browser;
try {
if (!frontendUrl || !backendUrl) throw new Error("LANGBOT_FRONTEND_URL and LANGBOT_BACKEND_URL must be configured.");
browser = await createBrowser(paths);
const { page } = browser;
await page.goto(frontendUrl, { waitUntil: "domcontentloaded" });
const auth = await ensureAuthenticatedBrowser(page, { frontendUrl, backendUrl });
if (auth.status !== "pass") {
result.status = auth.status;
throw new Error(auth.reason);
}
const token = await page.evaluate(() => localStorage.getItem("token") || "");
const pluginsResponse = await apiJson(backendUrl, "/api/v1/plugins", { token });
if (pluginsResponse.status >= 400 || pluginsResponse.json.code !== 0) {
throw new Error(pluginsResponse.json.msg || "Failed to list installed plugins.");
}
const installed = new Map((pluginsResponse.json.data?.plugins || []).map((item) => {
const metadata = item.manifest?.manifest?.metadata || item.manifest?.metadata || {};
return [`${metadata.author || ""}/${metadata.name || ""}`, item];
}));
const missing = targets.filter((target) => {
const plugin = installed.get(`${target.author}/${target.plugin}`);
if (!plugin) return true;
const pages = plugin.manifest?.manifest?.spec?.pages || plugin.manifest?.spec?.pages || [];
return !pages.some((pageSpec) => pageSpec.id === target.page);
});
if (missing.length) {
result.status = "blocked";
throw new Error(`Required plugin page fixture(s) are not installed or do not register the expected Page ID: ${missing.map((item) => item.id).join(", ")}`);
}
for (const target of targets) {
const routeId = `${target.author}/${target.plugin}/${target.page}`;
const route = `${frontendUrl.replace(/\/$/, "")}/home/plugin-pages?id=${encodeURIComponent(routeId)}`;
const pageResult = {
id: target.id,
route,
visible_signal: target.signal,
ui_status: "fail",
api_status: "fail",
screenshot: join(paths.evidenceDir, `${target.id}.png`),
page_api: null,
};
result.pages.push(pageResult);
await page.goto(route, { waitUntil: "domcontentloaded" });
await page.waitForLoadState("networkidle", { timeout: 15_000 }).catch(() => {});
const frame = page.frameLocator("iframe").first();
await frame.getByText(target.signal, { exact: true }).first().waitFor({ state: "visible", timeout: 30_000 });
pageResult.ui_status = "pass";
await safeScreenshot(page, pageResult.screenshot);
const apiResponse = await apiJson(
backendUrl,
`/api/v1/plugins/${encodeURIComponent(target.author)}/${encodeURIComponent(target.plugin)}/page-api`,
{
method: "POST",
token,
body: { page_id: target.page, endpoint: target.endpoint, method: target.method, body: null },
},
);
pageResult.page_api = {
endpoint: target.endpoint,
method: target.method,
http_status: apiResponse.status,
code: apiResponse.json.code ?? null,
has_data: apiResponse.json.data !== undefined && apiResponse.json.data !== null,
};
if (apiResponse.status >= 400 || apiResponse.json.code !== 0 || apiResponse.json.data == null) {
throw new Error(`${target.id} Page API ${target.method} ${target.endpoint} failed.`);
}
pageResult.api_status = "pass";
}
await safeScreenshot(page, paths.screenshot);
result.browser_diagnostics = await scanBrowserDiagnostics(paths);
if (result.browser_diagnostics.status !== "pass") {
throw new Error(result.browser_diagnostics.reason);
}
result.status = "pass";
result.reason = "All workspace plugin pages rendered in the LangBot WebUI and their read-only Page APIs passed.";
} catch (error) {
if (result.status === "fail" && /configured|Playwright is not installed|ECONNREFUSED|ERR_CONNECTION/i.test(error.message)) {
result.status = "env_issue";
}
result.reason = error.message;
} finally {
if (browser?.page) await safeScreenshot(browser.page, paths.screenshot);
if (browser) await browser.close().catch(() => {});
const finishedAt = new Date();
result.finished_at = finishedAt.toISOString();
result.finished_at_local = localIsoWithOffset(finishedAt);
await writeResult(paths, result);
console.log(JSON.stringify(result, null, 2));
}
process.exit(exitCode(result.status));