mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-20 19:36:08 +00:00
test(agent-runner): strengthen local agent e2e gate
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { appendFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
import { basename, join, resolve } from "node:path";
|
||||
import { env } from "node:process";
|
||||
|
||||
const secretRe = /(?:authorization|bearer|token|secret|password|api[_-]?key|jwt|oauth)\s*[:=]\s*["']?[^"',\s]+/gi;
|
||||
@@ -71,6 +71,86 @@ export async function writeResult(paths, result) {
|
||||
}
|
||||
}
|
||||
|
||||
function browserDiagnosticFindings(source, text) {
|
||||
const findings = [];
|
||||
const lines = String(text || "").split(/\r?\n/);
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const line = lines[index];
|
||||
if (!line) continue;
|
||||
const lineNumber = index + 1;
|
||||
|
||||
if (source === "console") {
|
||||
const checks = [
|
||||
["pageerror", /\[pageerror\]/i],
|
||||
["frontend_uncaught_error", /\[error\].*(?:\bUncaught\b|Unhandled(?: promise rejection|Rejection)|TypeError|ReferenceError)/i],
|
||||
["http_5xx", /Failed to load resource: the server responded with a status of 5\d\d/i],
|
||||
["api_server_error", /\[error\].*Server error:/i],
|
||||
["plugin_runtime_timeout", /\[error\].*Action [A-Za-z0-9_]+ call timed out/i],
|
||||
];
|
||||
for (const [kind, regex] of checks) {
|
||||
if (!regex.test(line)) continue;
|
||||
findings.push({
|
||||
source,
|
||||
severity: "fail",
|
||||
kind,
|
||||
line: lineNumber,
|
||||
excerpt: redact(line.trim()),
|
||||
});
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source === "network") {
|
||||
if (/\[response\]\s+5\d\d\b/i.test(line)) {
|
||||
findings.push({
|
||||
source,
|
||||
severity: "fail",
|
||||
kind: "http_5xx",
|
||||
line: lineNumber,
|
||||
excerpt: redact(line.trim()),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (/\[requestfailed\]/i.test(line) && !/net::ERR_ABORTED/i.test(line)) {
|
||||
findings.push({
|
||||
source,
|
||||
severity: "warning",
|
||||
kind: "request_failed",
|
||||
line: lineNumber,
|
||||
excerpt: redact(line.trim()),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
export async function scanBrowserDiagnostics(paths) {
|
||||
const sources = [
|
||||
["console", paths.consoleLog],
|
||||
["network", paths.networkLog],
|
||||
];
|
||||
const findings = [];
|
||||
for (const [source, path] of sources) {
|
||||
let text = "";
|
||||
try {
|
||||
text = await readFile(path, "utf8");
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
findings.push(...browserDiagnosticFindings(source, text));
|
||||
}
|
||||
const hasFailure = findings.some((finding) => finding.severity === "fail");
|
||||
return {
|
||||
status: hasFailure ? "fail" : "pass",
|
||||
findings,
|
||||
reason: hasFailure
|
||||
? `Browser diagnostics found ${findings.filter((finding) => finding.severity === "fail").length} failing signal(s).`
|
||||
: "No failing browser diagnostics found.",
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadEnvFiles(paths = ["skills/.env", "skills/.env.local"]) {
|
||||
const processEnvKeys = new Set(Object.keys(env));
|
||||
for (const path of paths) {
|
||||
@@ -92,8 +172,28 @@ export async function loadEnvFiles(paths = ["skills/.env", "skills/.env.local"])
|
||||
}
|
||||
}
|
||||
|
||||
export async function readRecoveryKey(repo = env.LANGBOT_REPO || "../LangBot") {
|
||||
const configPath = resolve(repo, "data/config.yaml");
|
||||
export async function resolveLangBotRepo(repo = env.LANGBOT_REPO || "", cwd = process.cwd()) {
|
||||
if (repo) return resolve(repo);
|
||||
|
||||
const candidates = [
|
||||
resolve(cwd),
|
||||
basename(cwd) === "skills" ? resolve(cwd, "..") : "",
|
||||
resolve(cwd, "../LangBot"),
|
||||
resolve(cwd, "LangBot"),
|
||||
].filter(Boolean);
|
||||
|
||||
const seen = new Set();
|
||||
for (const candidate of candidates) {
|
||||
if (seen.has(candidate)) continue;
|
||||
seen.add(candidate);
|
||||
if (await pathExists(resolve(candidate, "data/config.yaml"))) return candidate;
|
||||
}
|
||||
|
||||
return resolve(cwd, "../LangBot");
|
||||
}
|
||||
|
||||
export async function readRecoveryKey(repo = env.LANGBOT_REPO || "") {
|
||||
const configPath = resolve(await resolveLangBotRepo(repo), "data/config.yaml");
|
||||
const config = await readFile(configPath, "utf8");
|
||||
const match = config.match(/^\s*recovery_key:\s*['"]?([^'"\s#]+)['"]?\s*$/m);
|
||||
return match?.[1] || "";
|
||||
@@ -202,6 +302,62 @@ export async function verifyBrowserToken(page, backendUrl) {
|
||||
}, backendUrl);
|
||||
}
|
||||
|
||||
export async function ensureAuthenticatedBrowser(page, {
|
||||
frontendUrl = env.LANGBOT_FRONTEND_URL || "",
|
||||
backendUrl = env.LANGBOT_BACKEND_URL || "",
|
||||
user = env.LANGBOT_E2E_LOGIN_USER || "",
|
||||
password = env.LANGBOT_E2E_LOGIN_PASSWORD || "LangBotE2ELocalPass!2026",
|
||||
recoveryKey = "",
|
||||
} = {}) {
|
||||
if (!frontendUrl) return { status: "env_issue", reason: "LANGBOT_FRONTEND_URL is not configured." };
|
||||
if (!backendUrl) return { status: "env_issue", reason: "LANGBOT_BACKEND_URL is not configured." };
|
||||
|
||||
const current = await verifyBrowserToken(page, backendUrl).catch((error) => ({
|
||||
authenticated: false,
|
||||
reason: error.message,
|
||||
}));
|
||||
if (current.authenticated) {
|
||||
return {
|
||||
status: "pass",
|
||||
reason: "Existing browser token is valid.",
|
||||
backend_token_check: null,
|
||||
browser_token_check: current,
|
||||
injected: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
status: "blocked",
|
||||
reason: "Browser profile is not authenticated for LANGBOT_FRONTEND_URL, and LANGBOT_E2E_LOGIN_USER is not configured for automatic local login.",
|
||||
backend_token_check: null,
|
||||
browser_token_check: current,
|
||||
injected: false,
|
||||
};
|
||||
}
|
||||
|
||||
const auth = await resetAndAuthLocalUser({ backendUrl, user, password, recoveryKey });
|
||||
await setBrowserToken(page, frontendUrl, auth.token);
|
||||
const browserCheck = await verifyBrowserToken(page, backendUrl);
|
||||
if (!browserCheck.authenticated) {
|
||||
return {
|
||||
status: "blocked",
|
||||
reason: browserCheck.reason || "Browser token check failed after automatic local login.",
|
||||
backend_token_check: auth.check,
|
||||
browser_token_check: browserCheck,
|
||||
injected: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "pass",
|
||||
reason: "Browser token injected from local recovery login.",
|
||||
backend_token_check: auth.check,
|
||||
browser_token_check: browserCheck,
|
||||
injected: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function exitCode(status) {
|
||||
if (status === "pass") return 0;
|
||||
if (status === "blocked" || status === "env_issue") return 2;
|
||||
|
||||
Reference in New Issue
Block a user