test(agent-runner): strengthen local agent e2e gate

This commit is contained in:
huanghuoguoguo
2026-07-01 20:20:43 +08:00
parent d0f8f080e9
commit 995888f6b2
44 changed files with 5678 additions and 1040 deletions
+62 -18
View File
@@ -26,7 +26,12 @@ const packagePath = resolve(
|| "skills/langbot-testing/fixtures/plugins/qa-plugin-smoke/dist/qa-plugin-smoke-0.1.0.lbpkg",
);
const expectedPluginId = env.LANGBOT_E2E_EXPECTED_PLUGIN_ID || "qa/plugin-smoke";
const expectedTool = env.LANGBOT_E2E_EXPECTED_TOOL || (expectedPluginId === "qa/plugin-smoke" ? "qa_plugin_echo" : "");
const expectedTools = (env.LANGBOT_E2E_EXPECTED_TOOLS || env.LANGBOT_E2E_EXPECTED_TOOL || (
expectedPluginId === "qa/plugin-smoke" ? "qa_plugin_echo" : ""
))
.split(",")
.map((item) => item.trim())
.filter(Boolean);
const expectedRunnerId = env.LANGBOT_E2E_EXPECTED_RUNNER_ID || "";
const result = {
@@ -40,6 +45,7 @@ const result = {
package_preview: null,
task_id: null,
task: null,
reinstall_reason: "",
plugin_present_before: false,
plugin_present_after: false,
tool_names: [],
@@ -64,21 +70,23 @@ try {
}
result.plugin_present_before = await hasPlugin(backendUrl, auth.token);
if (!result.plugin_present_before) {
const form = new FormData();
form.set("file", new Blob([bytes]), packagePath.split("/").pop());
const response = await fetch(`${backendUrl.replace(/\/$/, "")}/api/v1/plugins/install/local`, {
method: "POST",
headers: { Authorization: `Bearer ${auth.token}` },
body: form,
});
const json = await response.json().catch(() => ({}));
if (response.status >= 400 || json.code !== 0) {
throw new Error(json.msg || `Plugin install request failed with HTTP ${response.status}.`);
if (expectedTools.length > 0) {
result.tool_names = await listToolNames(backendUrl, auth.token);
}
const missingToolsBefore = expectedTools.filter((tool) => !result.tool_names.includes(tool));
if (result.plugin_present_before && missingToolsBefore.length > 0) {
result.reinstall_reason = `Installed plugin is missing expected tools: ${missingToolsBefore.join(", ")}`;
const removeTask = await removePlugin(backendUrl, auth.token);
if (!isTaskComplete(removeTask)) {
throw new Error(`Plugin reinstall cleanup did not complete successfully: ${JSON.stringify(removeTask)}`);
}
result.task_id = json.data?.task_id ?? null;
if (!result.task_id) throw new Error("Plugin install response did not include task_id.");
result.task = await waitForTask(backendUrl, auth.token, result.task_id);
result.plugin_present_before = false;
await sleep(1000);
}
if (!result.plugin_present_before) {
result.task = await installPlugin(backendUrl, auth.token, bytes, packagePath);
result.task_id = result.task?.id || result.task_id;
if (!isTaskComplete(result.task)) {
throw new Error(`Plugin install task did not complete successfully: ${JSON.stringify(result.task)}`);
}
@@ -87,10 +95,11 @@ try {
await sleep(1000);
result.plugin_present_after = await hasPlugin(backendUrl, auth.token);
if (!result.plugin_present_after) throw new Error(`${expectedPluginId} is not listed by /api/v1/plugins after install.`);
if (expectedTool) {
if (expectedTools.length > 0) {
result.tool_names = await listToolNames(backendUrl, auth.token);
if (!result.tool_names.includes(expectedTool)) {
throw new Error(`${expectedTool} is not listed by /api/v1/tools after install.`);
const missingTools = expectedTools.filter((tool) => !result.tool_names.includes(tool));
if (missingTools.length > 0) {
throw new Error(`${missingTools.join(", ")} is not listed by /api/v1/tools after install.`);
}
}
if (expectedRunnerId) {
@@ -121,6 +130,41 @@ async function hasPlugin(backendUrl, token) {
});
}
async function installPlugin(backendUrl, token, bytes, packagePath) {
const form = new FormData();
form.set("file", new Blob([bytes]), packagePath.split("/").pop());
const response = await fetch(`${backendUrl.replace(/\/$/, "")}/api/v1/plugins/install/local`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
body: form,
});
const json = await response.json().catch(() => ({}));
if (response.status >= 400 || json.code !== 0) {
throw new Error(json.msg || `Plugin install request failed with HTTP ${response.status}.`);
}
const taskId = json.data?.task_id ?? null;
if (!taskId) throw new Error("Plugin install response did not include task_id.");
const task = await waitForTask(backendUrl, token, taskId);
return { id: taskId, ...task };
}
async function removePlugin(backendUrl, token) {
const [author, pluginName] = expectedPluginId.split("/");
if (!author || !pluginName) throw new Error(`Invalid expected plugin id: ${expectedPluginId}`);
const response = await apiJson(
backendUrl,
`/api/v1/plugins/${encodeURIComponent(author)}/${encodeURIComponent(pluginName)}?delete_data=false`,
{ method: "DELETE", token },
);
if (response.status >= 400 || response.json.code !== 0) {
throw new Error(response.json.msg || `Plugin delete request failed with HTTP ${response.status}.`);
}
const taskId = response.json.data?.task_id ?? null;
if (!taskId) throw new Error("Plugin delete response did not include task_id.");
const task = await waitForTask(backendUrl, token, taskId);
return { id: taskId, ...task };
}
async function previewPackage(backendUrl, token, bytes, packagePath) {
const form = new FormData();
form.set("file", new Blob([bytes]), packagePath.split("/").pop());