mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-25 13:56:08 +00:00
feat(wizard): start bot setup from scenarios
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
#!/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));
|
||||
@@ -190,7 +190,8 @@
|
||||
"sandbox-skill-authoring-e2e",
|
||||
"sandbox-skill-authoring-edit-existing-e2e",
|
||||
"skill-discovery-via-mcp-gateway",
|
||||
"webui-login-state"
|
||||
"webui-login-state",
|
||||
"wizard-scenario-routing"
|
||||
],
|
||||
"case_summaries": [
|
||||
{
|
||||
@@ -1647,6 +1648,31 @@
|
||||
"screenshot",
|
||||
"console"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "wizard-scenario-routing",
|
||||
"title": "Quick Start filters channels by the selected bot scenario",
|
||||
"mode": "agent-browser",
|
||||
"area": "wizard",
|
||||
"type": "feature",
|
||||
"priority": "p0",
|
||||
"risk": "medium",
|
||||
"ci_eligible": false,
|
||||
"tags": [
|
||||
"wizard",
|
||||
"eba",
|
||||
"event-routing",
|
||||
"productization"
|
||||
],
|
||||
"automation": "scripts/e2e/wizard-scenario-routing.mjs",
|
||||
"setup_automation": [],
|
||||
"setup_provides_env": [],
|
||||
"evidence_required": [
|
||||
"ui",
|
||||
"screenshot",
|
||||
"console",
|
||||
"api_diagnostic"
|
||||
]
|
||||
}
|
||||
],
|
||||
"suites": [
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
id: wizard-scenario-routing
|
||||
title: "Quick Start filters channels by the selected bot scenario"
|
||||
mode: agent-browser
|
||||
area: wizard
|
||||
type: feature
|
||||
priority: p0
|
||||
risk: medium
|
||||
ci_eligible: false
|
||||
tags:
|
||||
- wizard
|
||||
- eba
|
||||
- event-routing
|
||||
- productization
|
||||
skills:
|
||||
- langbot-env-setup
|
||||
- langbot-testing
|
||||
env:
|
||||
- LANGBOT_FRONTEND_URL
|
||||
- LANGBOT_BACKEND_URL
|
||||
- LANGBOT_BROWSER_PROFILE
|
||||
automation: scripts/e2e/wizard-scenario-routing.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 disabled temporary Bot and Agent may be created and deleted."
|
||||
steps:
|
||||
- "Open Quick Start and select Reply to messages."
|
||||
- "Confirm HTTP Bot is available for the message scenario."
|
||||
- "Select Welcome new members."
|
||||
- "Confirm a compatible channel remains visible and HTTP Bot is removed."
|
||||
- "Create a disabled temporary Bot and Agent, persist a group.member_joined route, and read it back through the API."
|
||||
- "Verify the scenario and channel layout at desktop and mobile widths."
|
||||
checks:
|
||||
- "UI: Scenario selection appears before channel selection."
|
||||
- "UI: Reply to messages includes HTTP Bot."
|
||||
- "UI: Welcome new members includes a compatible event-capable channel."
|
||||
- "UI: Welcome new members excludes HTTP Bot because it does not declare group.member_joined support."
|
||||
- "UI: The selected non-message scenario is labeled as an Agent behavior."
|
||||
- "Console: No unexpected frontend errors appear during the flow."
|
||||
- "Network: Adapter discovery returns without a 5xx response."
|
||||
- "API: A non-message event route to an Agent is normalized and persisted."
|
||||
- "Cleanup: The temporary Bot and Agent are deleted after evidence is collected."
|
||||
- "Isolation: Wizard progress writes are stubbed in the browser and do not change backend state."
|
||||
evidence_required:
|
||||
- ui
|
||||
- screenshot
|
||||
- console
|
||||
- api_diagnostic
|
||||
diagnostics:
|
||||
- "The temporary Bot remains disabled, so the case cannot connect to an external messaging platform."
|
||||
- "A passing adapter API response without the visible filtered channel list is not a pass."
|
||||
troubleshooting:
|
||||
- backend-not-listening
|
||||
- proxy-env-mismatch
|
||||
Reference in New Issue
Block a user