mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-09 12:40:59 +00:00
206 lines
6.9 KiB
JavaScript
Executable File
206 lines
6.9 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
import { spawn } from "node:child_process";
|
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
import { dirname, resolve } from "node:path";
|
|
import { env } from "node:process";
|
|
import {
|
|
appendLine,
|
|
ensureEvidence,
|
|
evidencePaths,
|
|
loadEnvFiles,
|
|
redact,
|
|
writeResult,
|
|
} from "./lib/langbot-e2e.mjs";
|
|
|
|
const caseId = "ensure-fake-provider-cross-pipelines";
|
|
const DEFAULT_PIPELINE_A_NAME = "LangBot QA Fake Provider Debug Chat A";
|
|
const DEFAULT_PIPELINE_B_NAME = "LangBot QA Fake Provider Debug Chat B";
|
|
|
|
await loadEnvFiles();
|
|
const paths = evidencePaths(caseId);
|
|
await ensureEvidence(paths);
|
|
|
|
const writeEnv = process.argv.includes("--write-env");
|
|
const envLocalPath = resolve("skills/.env.local");
|
|
const pipelineAName = env.LANGBOT_FAKE_PROVIDER_PIPELINE_A_NAME || DEFAULT_PIPELINE_A_NAME;
|
|
const pipelineBName = env.LANGBOT_FAKE_PROVIDER_PIPELINE_B_NAME || DEFAULT_PIPELINE_B_NAME;
|
|
|
|
const result = {
|
|
source: "setup_automation",
|
|
case_id: caseId,
|
|
run_id: paths.runId,
|
|
status: "fail",
|
|
reason: "",
|
|
pipeline_a: {
|
|
name: pipelineAName,
|
|
id: "",
|
|
url: "",
|
|
},
|
|
pipeline_b: {
|
|
name: pipelineBName,
|
|
id: "",
|
|
url: "",
|
|
},
|
|
fake_provider: {
|
|
url: "",
|
|
base_url: "",
|
|
pid: null,
|
|
},
|
|
wrote_env: false,
|
|
evidence: {
|
|
console_log: paths.consoleLog,
|
|
automation_result_json: paths.automationResultJson,
|
|
result_json: paths.resultJson,
|
|
},
|
|
evidence_collected: ["api_diagnostic", "filesystem"],
|
|
};
|
|
|
|
try {
|
|
console.error(`[langbot-qa] configuring cross-pipeline QA fixtures: pipeline_a=\"${pipelineAName}\", pipeline_b=\"${pipelineBName}\"`);
|
|
console.error("[langbot-qa] run these fake-provider setup/probe commands serially when they share LANGBOT_FAKE_PROVIDER_URL.");
|
|
if (pipelineAName === pipelineBName) {
|
|
throw new Error("LANGBOT_FAKE_PROVIDER_PIPELINE_A_NAME and LANGBOT_FAKE_PROVIDER_PIPELINE_B_NAME must be different.");
|
|
}
|
|
|
|
const setupA = await runPipelineSetup(pipelineAName, "A");
|
|
const setupB = await runPipelineSetup(pipelineBName, "B");
|
|
result.pipeline_a = {
|
|
name: setupA.pipeline_name || pipelineAName,
|
|
id: setupA.pipeline_id || "",
|
|
url: setupA.pipeline_url || "",
|
|
};
|
|
result.pipeline_b = {
|
|
name: setupB.pipeline_name || pipelineBName,
|
|
id: setupB.pipeline_id || "",
|
|
url: setupB.pipeline_url || "",
|
|
};
|
|
result.fake_provider = {
|
|
url: setupB.fake_provider?.url || setupA.fake_provider?.url || "",
|
|
base_url: setupB.fake_provider?.base_url || setupA.fake_provider?.base_url || "",
|
|
pid: setupB.fake_provider?.pid ?? setupA.fake_provider?.pid ?? null,
|
|
};
|
|
|
|
if (!result.pipeline_a.url || !result.pipeline_b.url || !result.fake_provider.url) {
|
|
throw new Error("Cross-pipeline fake provider setup did not return both pipeline URLs and provider URL.");
|
|
}
|
|
|
|
if (writeEnv) {
|
|
await upsertEnvLocal(envLocalPath, {
|
|
LANGBOT_FAKE_PROVIDER_URL: result.fake_provider.url,
|
|
LANGBOT_FAKE_PROVIDER_BASE_URL: result.fake_provider.base_url,
|
|
LANGBOT_FAKE_PROVIDER_PID: result.fake_provider.pid ? String(result.fake_provider.pid) : "",
|
|
LANGBOT_FAKE_PROVIDER_PIPELINE_A_URL: result.pipeline_a.url,
|
|
LANGBOT_FAKE_PROVIDER_PIPELINE_A_NAME: result.pipeline_a.name,
|
|
LANGBOT_FAKE_PROVIDER_PIPELINE_B_URL: result.pipeline_b.url,
|
|
LANGBOT_FAKE_PROVIDER_PIPELINE_B_NAME: result.pipeline_b.name,
|
|
});
|
|
result.wrote_env = true;
|
|
}
|
|
|
|
result.status = "pass";
|
|
result.reason = "Fake provider cross-pipeline fixtures are configured.";
|
|
} catch (error) {
|
|
result.status = looksLikeEnvIssue(error) ? "env_issue" : "fail";
|
|
result.reason = safeReason(error.message);
|
|
} finally {
|
|
await writeResult(paths, result);
|
|
console.log(JSON.stringify(result, null, 2));
|
|
}
|
|
|
|
process.exit(result.status === "pass" ? 0 : result.status === "env_issue" ? 2 : 1);
|
|
|
|
function runPipelineSetup(pipelineName, label) {
|
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
const child = spawn(process.execPath, ["scripts/e2e/ensure-fake-provider-pipeline.mjs"], {
|
|
cwd: resolve("."),
|
|
env: {
|
|
...env,
|
|
LANGBOT_FAKE_PROVIDER_PIPELINE_NAME: pipelineName,
|
|
LANGBOT_FAKE_PROVIDER_FIRST_TOKEN_DELAY_MS: env.LANGBOT_FAKE_PROVIDER_FIRST_TOKEN_DELAY_MS || "25",
|
|
LANGBOT_FAKE_PROVIDER_CHUNK_DELAY_MS: env.LANGBOT_FAKE_PROVIDER_CHUNK_DELAY_MS || "10",
|
|
LANGBOT_FAKE_PROVIDER_CHUNK_COUNT: env.LANGBOT_FAKE_PROVIDER_CHUNK_COUNT || "0",
|
|
LANGBOT_FAKE_PROVIDER_FAIL_FIRST_N: "0",
|
|
LANGBOT_FAKE_PROVIDER_FAIL_EVERY_N: "0",
|
|
LANGBOT_FAKE_PROVIDER_FAULT_STATUS: env.LANGBOT_FAKE_PROVIDER_FAULT_STATUS || "500",
|
|
LANGBOT_FAKE_PROVIDER_FAIL_AFTER_FIRST_CHUNK: "false",
|
|
LANGBOT_FAKE_PROVIDER_DYNAMIC_RESPONSE: "true",
|
|
},
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
|
|
let stdout = "";
|
|
let stderr = "";
|
|
child.stdout.on("data", (chunk) => {
|
|
const text = chunk.toString();
|
|
stdout += text;
|
|
appendLine(paths.consoleLog, `[setup ${label} stdout] ${text.trimEnd()}`).catch(() => {});
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
const text = chunk.toString();
|
|
stderr += text;
|
|
appendLine(paths.consoleLog, `[setup ${label} stderr] ${text.trimEnd()}`).catch(() => {});
|
|
});
|
|
child.on("error", rejectPromise);
|
|
child.on("close", (code) => {
|
|
const parsed = parseJsonOutput(stdout);
|
|
if (code !== 0 || parsed.status !== "pass") {
|
|
rejectPromise(new Error(parsed.reason || stderr || `Fake provider pipeline setup ${label} exited with ${code}.`));
|
|
return;
|
|
}
|
|
resolvePromise(parsed);
|
|
});
|
|
});
|
|
}
|
|
|
|
function parseJsonOutput(text) {
|
|
const trimmed = String(text || "").trim();
|
|
if (!trimmed) return {};
|
|
try {
|
|
return JSON.parse(trimmed);
|
|
} catch {
|
|
const start = trimmed.indexOf("{");
|
|
const end = trimmed.lastIndexOf("}");
|
|
if (start >= 0 && end > start) {
|
|
try {
|
|
return JSON.parse(trimmed.slice(start, end + 1));
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
}
|
|
|
|
async function upsertEnvLocal(path, updates) {
|
|
await mkdir(dirname(path), { recursive: true });
|
|
let text = "";
|
|
try {
|
|
text = await readFile(path, "utf8");
|
|
} catch {
|
|
text = "";
|
|
}
|
|
const lines = text.split(/\r?\n/);
|
|
const seen = new Set();
|
|
const next = lines.map((line) => {
|
|
const trimmed = line.trim();
|
|
const match = trimmed.match(/^([A-Z][A-Z0-9_]*)=/);
|
|
if (!match || updates[match[1]] === undefined) return line;
|
|
seen.add(match[1]);
|
|
return `${match[1]}=${updates[match[1]]}`;
|
|
});
|
|
for (const [key, value] of Object.entries(updates)) {
|
|
if (!seen.has(key)) next.push(`${key}=${value}`);
|
|
}
|
|
await writeFile(path, `${next.join("\n").replace(/\n+$/, "")}\n`, "utf8");
|
|
}
|
|
|
|
function looksLikeEnvIssue(error) {
|
|
const message = String(error?.message || error || "");
|
|
return /fetch failed|ECONNREFUSED|ENOTFOUND|LANGBOT_.*not configured|Could not read recovery_key|Backend did not respond/i.test(message);
|
|
}
|
|
|
|
function safeReason(value) {
|
|
return redact(String(value || "")).slice(0, 1000);
|
|
}
|