mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-16 17:36:07 +00:00
test(skills): add bot event routing product flow
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import {
|
||||
apiJson,
|
||||
bodyText,
|
||||
createBrowser,
|
||||
ensureAuthenticatedBrowser,
|
||||
ensureEvidence,
|
||||
evidencePaths,
|
||||
exitCode,
|
||||
loadEnvFiles,
|
||||
localIsoWithOffset,
|
||||
safeScreenshot,
|
||||
scanBrowserDiagnostics,
|
||||
writeResult,
|
||||
} from "./lib/langbot-e2e.mjs";
|
||||
|
||||
const caseId = "bot-event-routing-product-flow";
|
||||
await loadEnvFiles();
|
||||
const paths = evidencePaths(caseId);
|
||||
await ensureEvidence(paths);
|
||||
|
||||
const startedAt = new Date();
|
||||
const frontendUrl = process.env.LANGBOT_FRONTEND_URL || "";
|
||||
const backendUrl = process.env.LANGBOT_BACKEND_URL || "";
|
||||
const fixtureName = `EBA Product Flow ${paths.runId}`;
|
||||
|
||||
let browser;
|
||||
let token = "";
|
||||
let botId = "";
|
||||
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: "",
|
||||
bot_id: "",
|
||||
visible_signals: [],
|
||||
api: {},
|
||||
diagnostics: null,
|
||||
cleanup: 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", "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.");
|
||||
}
|
||||
|
||||
await page.goto(`${frontendUrl.replace(/\/$/, "")}/home/bots?id=new`, {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
await page
|
||||
.getByText(/Create Bot|创建机器人|ボットを作成/)
|
||||
.first()
|
||||
.waitFor({ timeout: 15_000 });
|
||||
await page.getByRole("combobox").first().click();
|
||||
await page
|
||||
.getByRole("option", { name: /HTTP Bot|HTTP 通用接入|HTTP ボット/ })
|
||||
.click();
|
||||
await page
|
||||
.getByText(/Event Routing|事件路由|イベントルーティング/)
|
||||
.first()
|
||||
.waitFor();
|
||||
await page
|
||||
.getByRole("button", { name: /Add Route|添加路由|ルートを追加/ })
|
||||
.waitFor();
|
||||
result.visible_signals.push("create-mode-routing");
|
||||
|
||||
const create = await apiJson(backendUrl, "/api/v1/platform/bots", {
|
||||
method: "POST",
|
||||
token,
|
||||
body: {
|
||||
name: fixtureName,
|
||||
description: "Temporary EBA product-flow fixture",
|
||||
adapter: "http_bot",
|
||||
adapter_config: {
|
||||
inbound_secret: "eba-product-flow-local-only",
|
||||
callback_url: "http://127.0.0.1:9/langbot-e2e-unused",
|
||||
outbound_secret: "",
|
||||
default_session_type: "person",
|
||||
signature_required: false,
|
||||
callback_timeout: 1,
|
||||
callback_max_retries: 0,
|
||||
},
|
||||
enable: true,
|
||||
event_bindings: [
|
||||
{
|
||||
id: "qa-message-discard",
|
||||
event_pattern: "message.received",
|
||||
target_type: "discard",
|
||||
target_uuid: "",
|
||||
filters: [],
|
||||
priority: 0,
|
||||
enabled: true,
|
||||
description: "Discard the deterministic QA event",
|
||||
order: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
botId = create.json.data?.uuid || "";
|
||||
result.api.create_bot = {
|
||||
http_status: create.status,
|
||||
code: create.json.code ?? null,
|
||||
};
|
||||
result.bot_id = botId;
|
||||
if (create.status >= 400 || create.json.code !== 0 || !botId) {
|
||||
throw new Error(create.json.msg || "Failed to create the temporary Bot.");
|
||||
}
|
||||
|
||||
const botUrl = `${frontendUrl.replace(/\/$/, "")}/home/bots?id=${encodeURIComponent(botId)}`;
|
||||
await page.goto(botUrl, { waitUntil: "domcontentloaded" });
|
||||
await page
|
||||
.waitForLoadState("networkidle", { timeout: 10_000 })
|
||||
.catch(() => {});
|
||||
result.url = page.url();
|
||||
|
||||
await page
|
||||
.getByText(/Event Routing|事件路由|イベントルーティング/)
|
||||
.first()
|
||||
.waitFor({ timeout: 15_000 });
|
||||
await page
|
||||
.getByText(
|
||||
/Events this adapter can receive|此适配器可接收的事件|このアダプターが受信できるイベント/,
|
||||
)
|
||||
.waitFor();
|
||||
await page
|
||||
.getByText(/Message received|收到消息|メッセージを受信/)
|
||||
.first()
|
||||
.waitFor();
|
||||
result.visible_signals.push(
|
||||
"event-routing",
|
||||
"adapter-capabilities",
|
||||
"friendly-event-name",
|
||||
);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: /Test route|测试路由|ルートをテスト/ })
|
||||
.click();
|
||||
await page.getByRole("dialog").waitFor();
|
||||
await page
|
||||
.getByRole("button", { name: /Preview route|预览路由|ルートをプレビュー/ })
|
||||
.click();
|
||||
await page
|
||||
.getByText(/Route matched|已命中路由|ルートに一致しました/)
|
||||
.waitFor({ timeout: 15_000 });
|
||||
await page
|
||||
.getByText(/Discard|丢弃|破棄/)
|
||||
.first()
|
||||
.waitFor();
|
||||
result.visible_signals.push("dry-run-matched", "discard-target");
|
||||
|
||||
await page
|
||||
.getByRole("button", {
|
||||
name: /Run saved route|运行已保存路由|保存済みルートを実行/,
|
||||
})
|
||||
.click();
|
||||
await page
|
||||
.getByText(
|
||||
/saved route ran successfully|已保存路由运行成功|保存済みルートを実行しました/,
|
||||
)
|
||||
.waitFor({ timeout: 20_000 });
|
||||
result.visible_signals.push("test-event-dispatched");
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: /Close|关闭|閉じる/ })
|
||||
.first()
|
||||
.click();
|
||||
await page.getByRole("dialog").waitFor({ state: "hidden" });
|
||||
await page
|
||||
.getByText(/Discarded|已丢弃|破棄済み/)
|
||||
.first()
|
||||
.waitFor({ timeout: 10_000 });
|
||||
result.visible_signals.push("route-status-discarded");
|
||||
|
||||
const text = await bodyText(page);
|
||||
if (/\bEBA event\b/.test(text)) {
|
||||
throw new Error(
|
||||
"The primary route surface exposes an internal EBA runtime message.",
|
||||
);
|
||||
}
|
||||
|
||||
const status = await apiJson(
|
||||
backendUrl,
|
||||
`/api/v1/platform/bots/${encodeURIComponent(botId)}/event-routes/status`,
|
||||
{ token },
|
||||
);
|
||||
const latest = status.json.data?.routes?.find(
|
||||
(route) => route.binding_id === "qa-message-discard",
|
||||
);
|
||||
result.api.route_status = {
|
||||
http_status: status.status,
|
||||
code: status.json.code ?? null,
|
||||
last_status: latest?.last_status || null,
|
||||
};
|
||||
if (
|
||||
status.status >= 400 ||
|
||||
status.json.code !== 0 ||
|
||||
latest?.last_status !== "discarded"
|
||||
) {
|
||||
throw new Error(
|
||||
"Route status API did not confirm the discarded synthetic event.",
|
||||
);
|
||||
}
|
||||
|
||||
await safeScreenshot(page, paths.screenshot);
|
||||
result.diagnostics = await scanBrowserDiagnostics(paths);
|
||||
if (result.diagnostics.status !== "pass") {
|
||||
throw new Error(result.diagnostics.reason);
|
||||
}
|
||||
result.status = "pass";
|
||||
result.reason =
|
||||
"Bot event routing, dry-run, synthetic dispatch, and visible route status passed in the WebUI.";
|
||||
} 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 {
|
||||
if (botId && token && backendUrl) {
|
||||
const cleanup = await apiJson(
|
||||
backendUrl,
|
||||
`/api/v1/platform/bots/${encodeURIComponent(botId)}`,
|
||||
{ method: "DELETE", token },
|
||||
).catch((error) => ({
|
||||
status: 0,
|
||||
json: { code: null, msg: error.message },
|
||||
}));
|
||||
result.cleanup = {
|
||||
http_status: cleanup.status,
|
||||
code: cleanup.json.code ?? null,
|
||||
deleted: cleanup.status < 400 && cleanup.json.code === 0,
|
||||
};
|
||||
if (!result.cleanup.deleted && result.status === "pass") {
|
||||
result.status = "fail";
|
||||
result.reason =
|
||||
cleanup.json.msg || "The temporary Bot could not be 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));
|
||||
@@ -151,6 +151,7 @@
|
||||
"agent-runner-qa-debug-chat",
|
||||
"agent-runner-release-preflight",
|
||||
"agent-runner-runtime-chaos",
|
||||
"bot-event-routing-product-flow",
|
||||
"dify-agent-debug-chat",
|
||||
"langbot-fake-provider-debug-chat-cross-pipeline-isolation",
|
||||
"langbot-fake-provider-debug-chat-fault-recovery",
|
||||
@@ -482,6 +483,31 @@
|
||||
"filesystem"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bot-event-routing-product-flow",
|
||||
"title": "Bot event routing can be configured and tested from the WebUI",
|
||||
"mode": "agent-browser",
|
||||
"area": "bot",
|
||||
"type": "feature",
|
||||
"priority": "p0",
|
||||
"risk": "medium",
|
||||
"ci_eligible": false,
|
||||
"tags": [
|
||||
"bot",
|
||||
"eba",
|
||||
"event-routing",
|
||||
"productization"
|
||||
],
|
||||
"automation": "scripts/e2e/bot-event-routing-product-flow.mjs",
|
||||
"setup_automation": [],
|
||||
"setup_provides_env": [],
|
||||
"evidence_required": [
|
||||
"ui",
|
||||
"screenshot",
|
||||
"console",
|
||||
"api_diagnostic"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dify-agent-debug-chat",
|
||||
"title": "Dify AgentRunner returns a response through Pipeline Debug Chat",
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
id: bot-event-routing-product-flow
|
||||
title: "Bot event routing can be configured and tested from the WebUI"
|
||||
mode: agent-browser
|
||||
area: bot
|
||||
type: feature
|
||||
priority: p0
|
||||
risk: medium
|
||||
ci_eligible: false
|
||||
tags:
|
||||
- bot
|
||||
- eba
|
||||
- event-routing
|
||||
- productization
|
||||
skills:
|
||||
- langbot-env-setup
|
||||
- langbot-testing
|
||||
env:
|
||||
- LANGBOT_FRONTEND_URL
|
||||
- LANGBOT_BACKEND_URL
|
||||
- LANGBOT_BROWSER_PROFILE
|
||||
automation: scripts/e2e/bot-event-routing-product-flow.mjs
|
||||
automation_env:
|
||||
- LANGBOT_FRONTEND_URL
|
||||
- LANGBOT_BACKEND_URL
|
||||
- LANGBOT_BROWSER_PROFILE
|
||||
- LANGBOT_CHROMIUM_EXECUTABLE
|
||||
preconditions:
|
||||
- "The target is a local test instance where a temporary HTTP Bot may be created and deleted."
|
||||
steps:
|
||||
- "Open Create Bot, choose HTTP Bot, and confirm event routing can be configured before the first save."
|
||||
- "Create a temporary HTTP Bot with a saved message.received route to the discard processor."
|
||||
- "Open the Bot configuration in the WebUI."
|
||||
- "Confirm the adapter capability summary, friendly event name, target, and route status are visible."
|
||||
- "Open Test event route and run a dry-run against the current form."
|
||||
- "Run the saved runtime route with a synthetic event."
|
||||
- "Close the dialog and confirm the route card shows the latest discarded status."
|
||||
checks:
|
||||
- "UI: A user can choose a channel and add event routes during initial Bot creation."
|
||||
- "UI: Event routing uses user-facing labels and does not require the raw event name in the primary route card."
|
||||
- "UI: Dry-run visibly reports that the route matched the discard processor."
|
||||
- "UI: Saved-route execution visibly succeeds, explains its side-effect boundary, and updates route status to discarded."
|
||||
- "Console: No unexpected frontend errors appear during the flow."
|
||||
- "Network: Bot, dry-run, route-status, and test-event requests return without 5xx responses."
|
||||
- "Cleanup: The temporary Bot is deleted after evidence is collected."
|
||||
evidence_required:
|
||||
- ui
|
||||
- screenshot
|
||||
- console
|
||||
- api_diagnostic
|
||||
diagnostics:
|
||||
- "The fixture deliberately uses the discard processor so the product-flow test cannot invoke a model, tool, or external callback."
|
||||
- "A passing API call without the visible matched and discarded UI states is not a pass."
|
||||
troubleshooting:
|
||||
- backend-not-listening
|
||||
- proxy-env-mismatch
|
||||
Reference in New Issue
Block a user