mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-16 17:36:07 +00:00
350 lines
11 KiB
JavaScript
350 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import {
|
|
apiJson,
|
|
createBrowser,
|
|
ensureAuthenticatedBrowser,
|
|
ensureEvidence,
|
|
evidencePaths,
|
|
exitCode,
|
|
loadEnvFiles,
|
|
localIsoWithOffset,
|
|
safeScreenshot,
|
|
scanBrowserDiagnostics,
|
|
writeResult,
|
|
} from "./lib/langbot-e2e.mjs";
|
|
|
|
const caseId = "wizard-scenario-routing";
|
|
await loadEnvFiles();
|
|
const paths = evidencePaths(caseId);
|
|
await ensureEvidence(paths);
|
|
const messageScreenshot = paths.screenshot.replace(
|
|
/\.png$/,
|
|
"-message-scenario.png",
|
|
);
|
|
const mobileScreenshot = paths.screenshot.replace(/\.png$/, "-mobile.png");
|
|
|
|
const startedAt = new Date();
|
|
const frontendUrl = process.env.LANGBOT_FRONTEND_URL || "";
|
|
const backendUrl = process.env.LANGBOT_BACKEND_URL || "";
|
|
|
|
let browser;
|
|
let token = "";
|
|
let botId = "";
|
|
let agentId = "";
|
|
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: "",
|
|
url: "",
|
|
visible_signals: [],
|
|
api: {},
|
|
diagnostics: null,
|
|
cleanup: null,
|
|
evidence: {
|
|
console_log: paths.consoleLog,
|
|
network_log: paths.networkLog,
|
|
screenshot: paths.screenshot,
|
|
message_scenario_screenshot: messageScreenshot,
|
|
mobile_screenshot: mobileScreenshot,
|
|
automation_result_json: paths.automationResultJson,
|
|
result_json: paths.resultJson,
|
|
},
|
|
evidence_collected: ["ui", "screenshot", "console", "api_diagnostic"],
|
|
};
|
|
|
|
try {
|
|
if (!frontendUrl) throw new Error("LANGBOT_FRONTEND_URL is not configured.");
|
|
if (!backendUrl) throw new Error("LANGBOT_BACKEND_URL is not 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);
|
|
}
|
|
|
|
token = await page.evaluate(() => localStorage.getItem("token") || "");
|
|
if (!token) {
|
|
result.status = "blocked";
|
|
throw new Error("Authenticated browser has no reusable local token.");
|
|
}
|
|
|
|
const adapters = await apiJson(backendUrl, "/api/v1/platform/adapters", {
|
|
token,
|
|
});
|
|
const catalog = adapters.json.data?.adapters || [];
|
|
const httpBot = catalog.find((adapter) => adapter.name === "http_bot");
|
|
const welcomeAdapters = catalog.filter((adapter) =>
|
|
(adapter.spec?.supported_events || []).includes("group.member_joined"),
|
|
);
|
|
result.api.adapters = {
|
|
http_status: adapters.status,
|
|
code: adapters.json.code ?? null,
|
|
http_bot_supports_message:
|
|
!httpBot?.spec?.supported_events?.length ||
|
|
httpBot.spec.supported_events.includes("message.received"),
|
|
http_bot_supports_member_joined:
|
|
httpBot?.spec?.supported_events?.includes("group.member_joined") || false,
|
|
welcome_adapter_count: welcomeAdapters.length,
|
|
};
|
|
if (adapters.status >= 400 || adapters.json.code !== 0) {
|
|
throw new Error(adapters.json.msg || "Adapter discovery failed.");
|
|
}
|
|
if (!httpBot || welcomeAdapters.length === 0) {
|
|
throw new Error(
|
|
"The adapter catalog does not contain the fixtures required by this case.",
|
|
);
|
|
}
|
|
if (result.api.adapters.http_bot_supports_member_joined) {
|
|
throw new Error(
|
|
"HTTP Bot unexpectedly declares group.member_joined support; update the case expectation.",
|
|
);
|
|
}
|
|
|
|
const progressUrl = `${backendUrl.replace(/\/$/, "")}/api/v1/system/wizard/progress`;
|
|
await page.route(progressUrl, async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ code: 0, msg: "ok", data: {} }),
|
|
});
|
|
});
|
|
|
|
await page.goto(`${frontendUrl.replace(/\/$/, "")}/wizard`, {
|
|
waitUntil: "domcontentloaded",
|
|
});
|
|
result.url = page.url();
|
|
await page
|
|
.getByText(
|
|
/What should this bot do\?|这个机器人要完成什么?|このボットで何を実現しますか?/,
|
|
)
|
|
.waitFor({ timeout: 15_000 });
|
|
|
|
await page
|
|
.getByRole("button", {
|
|
name: /Reply to messages|回复收到的消息|受信メッセージに返信/,
|
|
})
|
|
.click();
|
|
await page
|
|
.getByText(/HTTP Bot|HTTP 通用接入|HTTP ボット/, { exact: true })
|
|
.waitFor();
|
|
result.visible_signals.push("scenario-first", "message-http-compatible");
|
|
await safeScreenshot(page, messageScreenshot);
|
|
|
|
await page
|
|
.getByRole("button", {
|
|
name: /Welcome new members|欢迎新成员|新しいメンバーを歓迎/,
|
|
})
|
|
.click();
|
|
await page
|
|
.getByText(/Discord/, { exact: true })
|
|
.first()
|
|
.waitFor();
|
|
const httpBotCount = await page
|
|
.getByText(/HTTP Bot|HTTP 通用接入|HTTP ボット/, { exact: true })
|
|
.count();
|
|
if (httpBotCount !== 0) {
|
|
throw new Error("HTTP Bot remained visible for group.member_joined.");
|
|
}
|
|
const selectedScenario = page.getByRole("button", {
|
|
name: /Welcome new members|欢迎新成员|新しいメンバーを歓迎/,
|
|
});
|
|
if (
|
|
!(await selectedScenario.getByText("Agent", { exact: true }).isVisible())
|
|
) {
|
|
throw new Error(
|
|
"The non-message scenario is not visibly labeled as Agent.",
|
|
);
|
|
}
|
|
result.visible_signals.push(
|
|
"welcome-compatible-channel",
|
|
"http-filtered",
|
|
"agent-behavior-label",
|
|
);
|
|
|
|
await safeScreenshot(page, paths.screenshot);
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await page.waitForTimeout(250);
|
|
const horizontalOverflow = await page.evaluate(
|
|
() => document.documentElement.scrollWidth - window.innerWidth,
|
|
);
|
|
if (horizontalOverflow > 1) {
|
|
throw new Error(
|
|
`The mobile wizard overflows horizontally by ${horizontalOverflow}px.`,
|
|
);
|
|
}
|
|
await selectedScenario.scrollIntoViewIfNeeded();
|
|
await page
|
|
.getByText(/Discord/, { exact: true })
|
|
.first()
|
|
.waitFor();
|
|
await safeScreenshot(page, mobileScreenshot);
|
|
result.visible_signals.push("mobile-layout");
|
|
|
|
const fixtureSuffix = paths.runId.slice(-48);
|
|
const agent = await apiJson(backendUrl, "/api/v1/agents", {
|
|
method: "POST",
|
|
token,
|
|
body: {
|
|
kind: "agent",
|
|
name: `Wizard Scenario Agent ${fixtureSuffix}`,
|
|
description: "Temporary wizard scenario routing fixture",
|
|
emoji: "W",
|
|
enabled: true,
|
|
supported_event_patterns: ["group.member_joined"],
|
|
},
|
|
});
|
|
agentId = agent.json.data?.uuid || "";
|
|
result.api.create_agent = {
|
|
http_status: agent.status,
|
|
code: agent.json.code ?? null,
|
|
};
|
|
if (agent.status >= 400 || agent.json.code !== 0 || !agentId) {
|
|
throw new Error(agent.json.msg || "Failed to create the temporary Agent.");
|
|
}
|
|
|
|
const bot = await apiJson(backendUrl, "/api/v1/platform/bots", {
|
|
method: "POST",
|
|
token,
|
|
body: {
|
|
name: `Wizard Scenario Bot ${fixtureSuffix}`,
|
|
description: "Temporary disabled wizard scenario routing fixture",
|
|
adapter: "aiocqhttp",
|
|
adapter_config: {
|
|
host: "127.0.0.1",
|
|
port: 2280,
|
|
"access-token": "",
|
|
},
|
|
enable: false,
|
|
event_bindings: [],
|
|
},
|
|
});
|
|
botId = bot.json.data?.uuid || "";
|
|
result.api.create_bot = {
|
|
http_status: bot.status,
|
|
code: bot.json.code ?? null,
|
|
};
|
|
if (bot.status >= 400 || bot.json.code !== 0 || !botId) {
|
|
throw new Error(bot.json.msg || "Failed to create the temporary Bot.");
|
|
}
|
|
|
|
const update = await apiJson(
|
|
backendUrl,
|
|
`/api/v1/platform/bots/${encodeURIComponent(botId)}`,
|
|
{
|
|
method: "PUT",
|
|
token,
|
|
body: {
|
|
event_bindings: [
|
|
{
|
|
event_pattern: "group.member_joined",
|
|
target_type: "agent",
|
|
target_uuid: agentId,
|
|
filters: [],
|
|
priority: 0,
|
|
enabled: true,
|
|
description: "Welcome new members",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
);
|
|
result.api.bind_route = {
|
|
http_status: update.status,
|
|
code: update.json.code ?? null,
|
|
};
|
|
if (update.status >= 400 || update.json.code !== 0) {
|
|
throw new Error(update.json.msg || "Failed to bind the temporary route.");
|
|
}
|
|
|
|
const savedBot = await apiJson(
|
|
backendUrl,
|
|
`/api/v1/platform/bots/${encodeURIComponent(botId)}`,
|
|
{ token },
|
|
);
|
|
const savedRoute = savedBot.json.data?.bot?.event_bindings?.[0];
|
|
result.api.read_route = {
|
|
http_status: savedBot.status,
|
|
code: savedBot.json.code ?? null,
|
|
event_pattern: savedRoute?.event_pattern || null,
|
|
target_type: savedRoute?.target_type || null,
|
|
target_matches: savedRoute?.target_uuid === agentId,
|
|
};
|
|
if (
|
|
savedBot.status >= 400 ||
|
|
savedBot.json.code !== 0 ||
|
|
savedRoute?.event_pattern !== "group.member_joined" ||
|
|
savedRoute?.target_type !== "agent" ||
|
|
savedRoute?.target_uuid !== agentId
|
|
) {
|
|
throw new Error("The saved Bot did not return the expected Agent route.");
|
|
}
|
|
result.visible_signals.push("agent-route-persisted");
|
|
|
|
result.diagnostics = await scanBrowserDiagnostics(paths);
|
|
if (result.diagnostics.status !== "pass") {
|
|
throw new Error(result.diagnostics.reason);
|
|
}
|
|
result.status = "pass";
|
|
result.reason =
|
|
"Quick Start visibly filtered channels by scenario at desktop and mobile widths.";
|
|
} catch (error) {
|
|
if (!["blocked", "env_issue"].includes(result.status)) result.status = "fail";
|
|
result.reason = result.reason || error.message;
|
|
if (browser?.page) await safeScreenshot(browser.page, paths.screenshot);
|
|
} finally {
|
|
const cleanup = {};
|
|
if (botId && token && backendUrl) {
|
|
const deletedBot = await apiJson(
|
|
backendUrl,
|
|
`/api/v1/platform/bots/${encodeURIComponent(botId)}`,
|
|
{ method: "DELETE", token },
|
|
).catch((error) => ({
|
|
status: 0,
|
|
json: { code: null, msg: error.message },
|
|
}));
|
|
cleanup.bot_deleted = deletedBot.status < 400 && deletedBot.json.code === 0;
|
|
cleanup.bot_http_status = deletedBot.status;
|
|
}
|
|
if (agentId && token && backendUrl) {
|
|
const deletedAgent = await apiJson(
|
|
backendUrl,
|
|
`/api/v1/agents/${encodeURIComponent(agentId)}`,
|
|
{ method: "DELETE", token },
|
|
).catch((error) => ({
|
|
status: 0,
|
|
json: { code: null, msg: error.message },
|
|
}));
|
|
cleanup.agent_deleted =
|
|
deletedAgent.status < 400 && deletedAgent.json.code === 0;
|
|
cleanup.agent_http_status = deletedAgent.status;
|
|
}
|
|
result.cleanup = cleanup;
|
|
const cleanupFailed =
|
|
(botId && !cleanup.bot_deleted) || (agentId && !cleanup.agent_deleted);
|
|
if (cleanupFailed && result.status === "pass") {
|
|
result.status = "fail";
|
|
result.reason = "The temporary wizard routing fixtures were not deleted.";
|
|
}
|
|
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));
|