mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-17 01:46:07 +00:00
feat(agent-runner): finalize 4.x processor integration
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import {
|
||||
apiJson,
|
||||
createBrowser,
|
||||
ensureAuthenticatedBrowser,
|
||||
ensureEvidence,
|
||||
evidencePaths,
|
||||
exitCode,
|
||||
loadEnvFiles,
|
||||
localIsoWithOffset,
|
||||
safeScreenshot,
|
||||
scanBrowserDiagnostics,
|
||||
writeResult,
|
||||
} from "./lib/langbot-e2e.mjs";
|
||||
|
||||
const caseId = "agent-runner-health-visibility";
|
||||
await loadEnvFiles();
|
||||
const paths = evidencePaths(caseId);
|
||||
await ensureEvidence(paths);
|
||||
const readyScreenshot = paths.screenshot.replace(/\.png$/, "-ready.png");
|
||||
const unavailableScreenshot = paths.screenshot.replace(
|
||||
/\.png$/,
|
||||
"-unavailable.png",
|
||||
);
|
||||
|
||||
const startedAt = new Date();
|
||||
const frontendUrl = process.env.LANGBOT_FRONTEND_URL || "";
|
||||
const backendUrl = process.env.LANGBOT_BACKEND_URL || "";
|
||||
const missingRunnerId = "plugin:qa/missing-runner/default";
|
||||
|
||||
let browser;
|
||||
let token = "";
|
||||
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,
|
||||
ready_screenshot: readyScreenshot,
|
||||
unavailable_screenshot: unavailableScreenshot,
|
||||
automation_result_json: paths.automationResultJson,
|
||||
result_json: paths.resultJson,
|
||||
},
|
||||
evidence_collected: ["ui", "screenshot", "console", "api_diagnostic"],
|
||||
};
|
||||
|
||||
function schemaDefaults(items = []) {
|
||||
return Object.fromEntries(
|
||||
items
|
||||
.filter((item) => item.name && Object.hasOwn(item, "default"))
|
||||
.map((item) => [item.name, item.default]),
|
||||
);
|
||||
}
|
||||
|
||||
async function openRunnerSettings(page, agentUrl) {
|
||||
await page.goto(agentUrl, { waitUntil: "domcontentloaded" });
|
||||
await page
|
||||
.getByRole("button", { name: /Runner|运行器|ランナー/ })
|
||||
.first()
|
||||
.waitFor({ timeout: 15_000 });
|
||||
await page
|
||||
.getByRole("button", { name: /Runner|运行器|ランナー/ })
|
||||
.first()
|
||||
.click();
|
||||
}
|
||||
|
||||
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 pluginStatus = await apiJson(
|
||||
backendUrl,
|
||||
"/api/v1/system/status/plugin-system",
|
||||
{ token },
|
||||
);
|
||||
const pluginData = pluginStatus.json.data || {};
|
||||
result.api.plugin_status = {
|
||||
http_status: pluginStatus.status,
|
||||
code: pluginStatus.json.code ?? null,
|
||||
is_enable: pluginData.is_enable ?? null,
|
||||
is_connected: pluginData.is_connected ?? null,
|
||||
};
|
||||
if (pluginStatus.status >= 400 || pluginStatus.json.code !== 0) {
|
||||
result.status = "env_issue";
|
||||
throw new Error(pluginStatus.json.msg || "Plugin status request failed.");
|
||||
}
|
||||
if (!pluginData.is_enable || !pluginData.is_connected) {
|
||||
result.status = "env_issue";
|
||||
throw new Error("The plugin runtime is not enabled and connected.");
|
||||
}
|
||||
|
||||
const metadata = await apiJson(backendUrl, "/api/v1/agents/_/metadata", {
|
||||
token,
|
||||
});
|
||||
const runnerTab = metadata.json.data?.runner_config;
|
||||
const runnerStage = runnerTab?.stages?.find(
|
||||
(stage) => stage.name === "runner",
|
||||
);
|
||||
const runnerOptions =
|
||||
runnerStage?.config?.find((item) => item.name === "id")?.options || [];
|
||||
const runner = runnerOptions[0];
|
||||
result.api.agent_metadata = {
|
||||
http_status: metadata.status,
|
||||
code: metadata.json.code ?? null,
|
||||
runner_count: runnerOptions.length,
|
||||
selected_runner: runner?.name || null,
|
||||
};
|
||||
if (metadata.status >= 400 || metadata.json.code !== 0) {
|
||||
throw new Error(metadata.json.msg || "Agent metadata request failed.");
|
||||
}
|
||||
if (!runner?.name) {
|
||||
result.status = "blocked";
|
||||
throw new Error("No registered AgentRunner is available for the UI check.");
|
||||
}
|
||||
|
||||
const runnerConfigStage = runnerTab.stages.find(
|
||||
(stage) => stage.name === runner.name,
|
||||
);
|
||||
const create = await apiJson(backendUrl, "/api/v1/agents", {
|
||||
method: "POST",
|
||||
token,
|
||||
body: {
|
||||
kind: "agent",
|
||||
name: `Runner Health ${paths.runId.slice(-40)}`,
|
||||
description: "Temporary AgentRunner health visibility fixture",
|
||||
emoji: "H",
|
||||
component_ref: runner.name,
|
||||
config: {
|
||||
runner: { id: runner.name, "expire-time": 0 },
|
||||
runner_config: {
|
||||
[runner.name]: schemaDefaults(runnerConfigStage?.config),
|
||||
},
|
||||
},
|
||||
enabled: true,
|
||||
supported_event_patterns: ["message.*"],
|
||||
},
|
||||
});
|
||||
agentId = create.json.data?.uuid || "";
|
||||
result.api.create_agent = {
|
||||
http_status: create.status,
|
||||
code: create.json.code ?? null,
|
||||
};
|
||||
if (create.status >= 400 || create.json.code !== 0 || !agentId) {
|
||||
throw new Error(create.json.msg || "Failed to create the temporary Agent.");
|
||||
}
|
||||
|
||||
const agentUrl = `${frontendUrl.replace(/\/$/, "")}/home/agents?id=${encodeURIComponent(agentId)}`;
|
||||
result.url = agentUrl;
|
||||
await openRunnerSettings(page, agentUrl);
|
||||
await page
|
||||
.getByText(/Runner ready|运行器已就绪|Runner の準備完了/, { exact: true })
|
||||
.waitFor({ timeout: 15_000 });
|
||||
result.visible_signals.push("registered-runner-ready");
|
||||
await safeScreenshot(page, readyScreenshot);
|
||||
|
||||
const staleUpdate = await apiJson(
|
||||
backendUrl,
|
||||
`/api/v1/agents/${encodeURIComponent(agentId)}`,
|
||||
{
|
||||
method: "PUT",
|
||||
token,
|
||||
body: {
|
||||
component_ref: missingRunnerId,
|
||||
config: {
|
||||
runner: { id: missingRunnerId, "expire-time": 0 },
|
||||
runner_config: { [missingRunnerId]: {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
result.api.set_stale_runner = {
|
||||
http_status: staleUpdate.status,
|
||||
code: staleUpdate.json.code ?? null,
|
||||
};
|
||||
if (staleUpdate.status >= 400 || staleUpdate.json.code !== 0) {
|
||||
throw new Error(
|
||||
staleUpdate.json.msg || "Failed to set the stale runner fixture.",
|
||||
);
|
||||
}
|
||||
|
||||
await openRunnerSettings(page, agentUrl);
|
||||
await page
|
||||
.getByText(
|
||||
/Selected runner is unavailable|所选运行器不可用|選択した Runner は利用できません/,
|
||||
{ exact: true },
|
||||
)
|
||||
.waitFor({ timeout: 15_000 });
|
||||
await page.getByRole("link", { name: /Extensions|扩展|拡張機能/ }).waitFor();
|
||||
result.visible_signals.push("stale-runner-unavailable", "recovery-action");
|
||||
await safeScreenshot(page, unavailableScreenshot);
|
||||
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 =
|
||||
"Agent Runner settings visibly distinguished a registered runner from a stale binding.";
|
||||
} 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 (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;
|
||||
if (agentId && !cleanup.agent_deleted && result.status === "pass") {
|
||||
result.status = "fail";
|
||||
result.reason = "The temporary runner health Agent was 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));
|
||||
@@ -70,7 +70,7 @@ const startedAt = new Date();
|
||||
const targets = [
|
||||
{
|
||||
id: "local-agent",
|
||||
expected_runner_id: "plugin:langbot/local-agent/default",
|
||||
expected_runner_id: "plugin:langbot-team/LocalAgent/default",
|
||||
pipeline_url: firstEnv("LANGBOT_LOCAL_AGENT_PIPELINE_URL"),
|
||||
pipeline_name: firstEnv("LANGBOT_LOCAL_AGENT_PIPELINE_NAME"),
|
||||
require_func_call_model: true,
|
||||
@@ -79,7 +79,7 @@ const targets = [
|
||||
},
|
||||
{
|
||||
id: "acp-agent-runner",
|
||||
expected_runner_id: "plugin:langbot/acp-agent-runner/default",
|
||||
expected_runner_id: "plugin:langbot-team/ACPAgentRunner/default",
|
||||
pipeline_url: firstEnv("LANGBOT_ACP_AGENT_RUNNER_PIPELINE_URL", "LANGBOT_AGENT_RUNNER_PIPELINE_URL"),
|
||||
pipeline_name: firstEnv("LANGBOT_ACP_AGENT_RUNNER_PIPELINE_NAME", "LANGBOT_AGENT_RUNNER_PIPELINE_NAME"),
|
||||
require_func_call_model: false,
|
||||
@@ -219,7 +219,7 @@ async function run() {
|
||||
return metadata.author && metadata.name ? `${metadata.author}/${metadata.name}` : "";
|
||||
})
|
||||
.filter(Boolean);
|
||||
const requiredPlugins = ["langbot/local-agent", "langbot/acp-agent-runner", "qa/plugin-smoke"];
|
||||
const requiredPlugins = ["langbot-team/LocalAgent", "langbot-team/ACPAgentRunner", "qa/plugin-smoke"];
|
||||
const pluginPresence = Object.fromEntries(requiredPlugins.map((id) => [id, installedPluginIds.includes(id)]));
|
||||
for (const [id, present] of Object.entries(pluginPresence)) {
|
||||
addCheck(`plugin:${id}`, present ? "pass" : "blocked", { plugin_id: id, reason: present ? "" : "Required plugin is not listed by /api/v1/plugins." });
|
||||
@@ -309,7 +309,7 @@ async function run() {
|
||||
const config = pipeline.config || {};
|
||||
const aiConfig = config.ai && typeof config.ai === "object" ? config.ai : {};
|
||||
const runner = aiConfig.runner && typeof aiConfig.runner === "object" ? aiConfig.runner : {};
|
||||
const runnerId = runner.id || runner.runner || "";
|
||||
const runnerId = runner.id || "";
|
||||
const runnerConfigs = aiConfig.runner_config && typeof aiConfig.runner_config === "object" ? aiConfig.runner_config : {};
|
||||
const runnerConfig = runnerConfigs[runnerId] && typeof runnerConfigs[runnerId] === "object" ? runnerConfigs[runnerId] : {};
|
||||
const pipelineSummary = {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
writeResult,
|
||||
} from "./lib/langbot-e2e.mjs";
|
||||
|
||||
const RUNNER_ID = "plugin:langbot/acp-agent-runner/default";
|
||||
const RUNNER_ID = "plugin:langbot-team/ACPAgentRunner/default";
|
||||
const DEFAULT_PIPELINE_NAME = "Agent QA ACP Claude Debug Chat";
|
||||
const DEFAULT_LOCAL_PASSWORD = "LangBotE2ELocalPass!2026";
|
||||
const caseId = "ensure-acp-agent-runner-pipeline";
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
writeResult,
|
||||
} from "./lib/langbot-e2e.mjs";
|
||||
|
||||
const RUNNER_ID = "local-agent";
|
||||
const RUNNER_ID = "plugin:langbot-team/LocalAgent/default";
|
||||
const DEFAULT_LOCAL_PASSWORD = "LangBotE2ELocalPass!2026";
|
||||
const DEFAULT_PIPELINE_NAME = "LangBot QA Fake Provider Debug Chat";
|
||||
const DEFAULT_PROVIDER_NAME = "LangBot QA Fake OpenAI Provider";
|
||||
@@ -511,8 +511,11 @@ async function ensurePipeline({ backendUrl, token, name, modelUuid }) {
|
||||
|
||||
const config = pipeline.config && typeof pipeline.config === "object" ? pipeline.config : {};
|
||||
const ai = config.ai && typeof config.ai === "object" ? config.ai : {};
|
||||
const existingLocalAgentConfig = ai["local-agent"] && typeof ai["local-agent"] === "object"
|
||||
? ai["local-agent"]
|
||||
const runnerConfigs = ai.runner_config && typeof ai.runner_config === "object"
|
||||
? ai.runner_config
|
||||
: {};
|
||||
const existingLocalAgentConfig = runnerConfigs[RUNNER_ID] && typeof runnerConfigs[RUNNER_ID] === "object"
|
||||
? runnerConfigs[RUNNER_ID]
|
||||
: {};
|
||||
const localAgentConfig = {
|
||||
timeout: 60,
|
||||
@@ -546,10 +549,12 @@ async function ensurePipeline({ backendUrl, token, name, modelUuid }) {
|
||||
runner: {
|
||||
...(ai.runner && typeof ai.runner === "object" ? ai.runner : {}),
|
||||
id: RUNNER_ID,
|
||||
runner: RUNNER_ID,
|
||||
"expire-time": 0,
|
||||
},
|
||||
"local-agent": localAgentConfig,
|
||||
runner_config: {
|
||||
...runnerConfigs,
|
||||
[RUNNER_ID]: localAgentConfig,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
writeResult,
|
||||
} from "./lib/langbot-e2e.mjs";
|
||||
|
||||
const RUNNER_ID = "plugin:langbot/local-agent/default";
|
||||
const RUNNER_ID = "plugin:langbot-team/LocalAgent/default";
|
||||
const SPACE_PROVIDER_UUID = "00000000-0000-0000-0000-000000000000";
|
||||
const DEFAULT_PIPELINE_NAME = "Agent QA Local Agent Debug Chat";
|
||||
const DEFAULT_LOCAL_PASSWORD = "LangBotE2ELocalPass!2026";
|
||||
|
||||
@@ -31,7 +31,7 @@ await ensureEvidence(paths);
|
||||
const backendUrl = (env.LANGBOT_BACKEND_URL || "").replace(/\/$/, "");
|
||||
const pipelineUrl = env.LANGBOT_E2E_PIPELINE_URL || env.LANGBOT_LOCAL_AGENT_PIPELINE_URL || env.LANGBOT_PIPELINE_URL || "";
|
||||
const pipelineName = env.LANGBOT_E2E_PIPELINE_NAME || env.LANGBOT_LOCAL_AGENT_PIPELINE_NAME || env.LANGBOT_PIPELINE_NAME || "";
|
||||
const expectedRunnerId = env.LANGBOT_E2E_EXPECTED_RUNNER_ID || "plugin:langbot/local-agent/default";
|
||||
const expectedRunnerId = env.LANGBOT_E2E_EXPECTED_RUNNER_ID || "plugin:langbot-team/LocalAgent/default";
|
||||
const expectedText = env.LANGBOT_E2E_EXPECTED_TEXT || "qa_steering_sentinel_6194";
|
||||
const responseTimeoutMs = positiveInt(env.LANGBOT_E2E_RESPONSE_TIMEOUT_MS, 240000);
|
||||
const followupDelayMs = 1000;
|
||||
@@ -446,7 +446,7 @@ async function inspectPipeline(page, { backendUrl, pipelineUrl, pipelineName, ex
|
||||
}
|
||||
const config = pipeline.config || {};
|
||||
const runner = config.ai?.runner || {};
|
||||
const runnerId = runner.id || runner.runner || "";
|
||||
const runnerId = runner.id || "";
|
||||
if (!runnerId) {
|
||||
return {
|
||||
status: "blocked",
|
||||
@@ -455,7 +455,7 @@ async function inspectPipeline(page, { backendUrl, pipelineUrl, pipelineName, ex
|
||||
pipeline_id: pipelineId,
|
||||
pipeline_name: pipeline.name,
|
||||
matched_by: matchedBy,
|
||||
reason: "Pipeline has no ai.runner.id or legacy ai.runner.runner.",
|
||||
reason: "Pipeline has no ai.runner.id.",
|
||||
};
|
||||
}
|
||||
if (expectedRunnerId && runnerId !== expectedRunnerId) {
|
||||
|
||||
@@ -429,7 +429,7 @@ async function inspectAndPatchPipelineConfig(page, {
|
||||
const config = JSON.parse(JSON.stringify(pipeline.config || {}));
|
||||
const aiConfig = config.ai && typeof config.ai === "object" ? config.ai : {};
|
||||
const runner = aiConfig.runner && typeof aiConfig.runner === "object" ? aiConfig.runner : {};
|
||||
const runnerId = runner.id || runner.runner || "";
|
||||
const runnerId = runner.id || "";
|
||||
if (!runnerId) {
|
||||
return {
|
||||
status: "blocked",
|
||||
@@ -438,7 +438,7 @@ async function inspectAndPatchPipelineConfig(page, {
|
||||
pipeline_id: pipelineId,
|
||||
pipeline_name: pipeline.name,
|
||||
matched_by: matchedBy,
|
||||
reason: "Pipeline has no ai.runner.id or legacy ai.runner.runner.",
|
||||
reason: "Pipeline has no ai.runner.id.",
|
||||
};
|
||||
}
|
||||
if (expectedRunnerId && runnerId !== expectedRunnerId) {
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import {
|
||||
apiJson,
|
||||
createBrowser,
|
||||
ensureAuthenticatedBrowser,
|
||||
ensureEvidence,
|
||||
evidencePaths,
|
||||
exitCode,
|
||||
loadEnvFiles,
|
||||
localIsoWithOffset,
|
||||
safeScreenshot,
|
||||
scanBrowserDiagnostics,
|
||||
writeResult,
|
||||
} from "./lib/langbot-e2e.mjs";
|
||||
|
||||
const caseId = "wizard-runner-marketplace-catalog";
|
||||
await loadEnvFiles();
|
||||
const paths = evidencePaths(caseId);
|
||||
await ensureEvidence(paths);
|
||||
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 = "";
|
||||
|
||||
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: {},
|
||||
marketplace_request: null,
|
||||
diagnostics: null,
|
||||
cleanup: {},
|
||||
evidence: {
|
||||
screenshot: paths.screenshot,
|
||||
mobile_screenshot: mobileScreenshot,
|
||||
console_log: paths.consoleLog,
|
||||
network_log: paths.networkLog,
|
||||
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;
|
||||
page.on("request", (request) => {
|
||||
if (
|
||||
!/\/api\/v1\/marketplace\/(extensions|plugins)\/search$/.test(
|
||||
new URL(request.url()).pathname,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const payload = request.postDataJSON();
|
||||
if (payload?.component_filter === "AgentRunner") {
|
||||
result.marketplace_request = {
|
||||
endpoint: new URL(request.url()).pathname,
|
||||
component_filter: payload.component_filter,
|
||||
type_filter: payload.type_filter || null,
|
||||
page_size: payload.page_size || null,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// The assertion below reports a missing or malformed catalog request.
|
||||
}
|
||||
});
|
||||
|
||||
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 token is unavailable.");
|
||||
}
|
||||
|
||||
const [plugins, metadata, system] = await Promise.all([
|
||||
apiJson(backendUrl, "/api/v1/plugins", { token }),
|
||||
apiJson(backendUrl, "/api/v1/pipelines/_/metadata", { token }),
|
||||
apiJson(backendUrl, "/api/v1/system/info", { token }),
|
||||
]);
|
||||
const runnerStage = metadata.json.data?.configs
|
||||
?.find((config) => config.name === "ai")
|
||||
?.stages?.find((stage) => stage.name === "runner");
|
||||
const runnerOptions =
|
||||
runnerStage?.config?.find((item) => item.name === "id")?.options || [];
|
||||
const installedPlugins = plugins.json.data?.plugins || [];
|
||||
result.api.clean_state = {
|
||||
plugin_count: installedPlugins.length,
|
||||
runner_count: runnerOptions.length,
|
||||
wizard_status: system.json.data?.wizard_status || null,
|
||||
};
|
||||
if (installedPlugins.length !== 0 || runnerOptions.length !== 0) {
|
||||
result.status = "blocked";
|
||||
throw new Error(
|
||||
`This case requires zero plugins and zero runners; found ${installedPlugins.length} plugins and ${runnerOptions.length} runners.`,
|
||||
);
|
||||
}
|
||||
if (system.json.data?.wizard_status !== "none") {
|
||||
result.status = "blocked";
|
||||
throw new Error(
|
||||
`This case requires a first-run instance; wizard status is ${system.json.data?.wizard_status}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const suffix = paths.runId.slice(-40);
|
||||
const bot = await apiJson(backendUrl, "/api/v1/platform/bots", {
|
||||
method: "POST",
|
||||
token,
|
||||
body: {
|
||||
name: `Runner Catalog Wizard ${suffix}`,
|
||||
description: "Temporary clean-runner catalog fixture",
|
||||
adapter: "aiocqhttp",
|
||||
adapter_config: {
|
||||
host: "127.0.0.1",
|
||||
port: 2280,
|
||||
"access-token": "",
|
||||
},
|
||||
enable: false,
|
||||
event_bindings: [],
|
||||
},
|
||||
});
|
||||
botId = bot.json.data?.uuid || "";
|
||||
if (bot.status >= 400 || bot.json.code !== 0 || !botId) {
|
||||
throw new Error(bot.json.msg || "Failed to create the temporary Bot.");
|
||||
}
|
||||
|
||||
const progress = await apiJson(backendUrl, "/api/v1/system/wizard/progress", {
|
||||
method: "PUT",
|
||||
token,
|
||||
body: {
|
||||
step: 2,
|
||||
selected_scenario: "message_reply",
|
||||
selected_adapter: "aiocqhttp",
|
||||
created_bot_uuid: botId,
|
||||
bot_saved: true,
|
||||
selected_runner: null,
|
||||
},
|
||||
});
|
||||
if (progress.status >= 400 || progress.json.code !== 0) {
|
||||
throw new Error(progress.json.msg || "Failed to prepare Wizard progress.");
|
||||
}
|
||||
|
||||
await page.goto(`${frontendUrl.replace(/\/$/, "")}/wizard`, {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
result.url = page.url();
|
||||
await page
|
||||
.getByText(/Select an AI Engine|选择 AI 引擎|AIエンジンを選択/, {
|
||||
exact: true,
|
||||
})
|
||||
.waitFor({ timeout: 15_000 });
|
||||
await page
|
||||
.getByText(
|
||||
/No AgentRunner extensions are published yet|市场暂未发布 AgentRunner 扩展|AgentRunner 拡張機能はまだ公開されていません/,
|
||||
{ exact: true },
|
||||
)
|
||||
.waitFor({ timeout: 15_000 });
|
||||
const browseLink = page.getByRole("link", {
|
||||
name: /Browse Runner Extensions|浏览运行器扩展|Runner 拡張機能を見る/,
|
||||
});
|
||||
await browseLink.waitFor();
|
||||
const href = await browseLink.getAttribute("href");
|
||||
if (href !== "/home/extensions?type=plugin&component=AgentRunner") {
|
||||
throw new Error(`Unexpected Runner marketplace URL: ${href}`);
|
||||
}
|
||||
const nextButton = page.getByRole("button", {
|
||||
name: /Create & Deploy|创建并部署|作成&デプロイ/,
|
||||
});
|
||||
if (!(await nextButton.isDisabled())) {
|
||||
throw new Error("Wizard allowed continuing without an installed Runner.");
|
||||
}
|
||||
if (!result.marketplace_request) {
|
||||
throw new Error(
|
||||
"Wizard did not request the AgentRunner Marketplace catalog.",
|
||||
);
|
||||
}
|
||||
|
||||
result.visible_signals.push(
|
||||
"first-run-ai-engine-step",
|
||||
"empty-runner-catalog-state",
|
||||
"runner-marketplace-link",
|
||||
"next-disabled-without-runner",
|
||||
);
|
||||
await safeScreenshot(page, paths.screenshot);
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.waitForTimeout(250);
|
||||
const overflow = await page.evaluate(
|
||||
() => document.documentElement.scrollWidth - window.innerWidth,
|
||||
);
|
||||
if (overflow > 1) {
|
||||
throw new Error(`Runner catalog Wizard overflows mobile by ${overflow}px.`);
|
||||
}
|
||||
await browseLink.scrollIntoViewIfNeeded();
|
||||
await safeScreenshot(page, mobileScreenshot);
|
||||
result.visible_signals.push("mobile-layout");
|
||||
|
||||
result.diagnostics = await scanBrowserDiagnostics(paths);
|
||||
if (result.diagnostics.status !== "pass") {
|
||||
throw new Error(result.diagnostics.reason);
|
||||
}
|
||||
result.status = "pass";
|
||||
result.reason =
|
||||
"A clean first-run instance requested the AgentRunner catalog, showed a safe empty state, and prevented continuing without a Runner.";
|
||||
} 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 (token && backendUrl) {
|
||||
const resetProgress = await apiJson(
|
||||
backendUrl,
|
||||
"/api/v1/system/wizard/progress",
|
||||
{
|
||||
method: "PUT",
|
||||
token,
|
||||
body: {
|
||||
step: 0,
|
||||
selected_scenario: null,
|
||||
selected_adapter: null,
|
||||
created_bot_uuid: null,
|
||||
bot_saved: false,
|
||||
selected_runner: null,
|
||||
},
|
||||
},
|
||||
).catch(() => ({ status: 0, json: {} }));
|
||||
cleanup.progress_reset =
|
||||
resetProgress.status < 400 && resetProgress.json.code === 0;
|
||||
}
|
||||
if (botId && token && backendUrl) {
|
||||
const deletedBot = await apiJson(
|
||||
backendUrl,
|
||||
`/api/v1/platform/bots/${encodeURIComponent(botId)}`,
|
||||
{ method: "DELETE", token },
|
||||
).catch(() => ({ status: 0, json: {} }));
|
||||
cleanup.bot_deleted = deletedBot.status < 400 && deletedBot.json.code === 0;
|
||||
}
|
||||
result.cleanup = cleanup;
|
||||
if (
|
||||
result.status === "pass" &&
|
||||
(!cleanup.progress_reset || (botId && !cleanup.bot_deleted))
|
||||
) {
|
||||
result.status = "fail";
|
||||
result.reason = "The clean Wizard fixtures were not fully reset.";
|
||||
}
|
||||
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));
|
||||
Reference in New Issue
Block a user