184 lines
5.1 KiB
JavaScript
184 lines
5.1 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import { resolveFastChannelTestFiles } from "./extension-fast-channel-smoke.mjs";
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const repoRoot = path.resolve(__dirname, "..", "..");
|
|
|
|
function readStdin() {
|
|
return new Promise((resolve, reject) => {
|
|
let raw = "";
|
|
process.stdin.setEncoding("utf8");
|
|
process.stdin.on("data", (chunk) => {
|
|
raw += chunk;
|
|
});
|
|
process.stdin.on("end", () => {
|
|
resolve(raw);
|
|
});
|
|
process.stdin.on("error", reject);
|
|
});
|
|
}
|
|
|
|
function parseInput(raw) {
|
|
if (!raw.trim()) {
|
|
return {};
|
|
}
|
|
return JSON.parse(raw);
|
|
}
|
|
|
|
function safeFileName(value) {
|
|
return value.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
}
|
|
|
|
function stateFilePath(stateDir, providerId) {
|
|
mkdirSync(stateDir, { recursive: true });
|
|
return path.join(stateDir, `${safeFileName(providerId)}.json`);
|
|
}
|
|
|
|
function writeJson(filePath, value) {
|
|
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
}
|
|
|
|
function readJson(filePath) {
|
|
try {
|
|
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function runFastSmoke(extension) {
|
|
if (!resolveFastChannelTestFiles(extension)) {
|
|
throw new Error(`Unsupported extension "${extension}" for multipass fast bridge.`);
|
|
}
|
|
const result = spawnSync(
|
|
"node",
|
|
["scripts/e2e/extension-fast-channel-smoke.mjs", "--extension", extension],
|
|
{
|
|
cwd: repoRoot,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
shell: process.platform === "win32",
|
|
encoding: "utf8",
|
|
env: process.env,
|
|
},
|
|
);
|
|
if (result.status !== 0) {
|
|
if (result.stdout) {
|
|
process.stderr.write(result.stdout);
|
|
}
|
|
if (result.stderr) {
|
|
process.stderr.write(result.stderr);
|
|
}
|
|
throw new Error(
|
|
`Fast smoke failed for extension "${extension}" (exit ${String(result.status)}).`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function resolveThreadId(payload, fallbackId) {
|
|
const target = payload?.outbound?.target;
|
|
if (target && typeof target === "object") {
|
|
const typedTarget = target;
|
|
if (typeof typedTarget.threadId === "string" && typedTarget.threadId.trim()) {
|
|
return typedTarget.threadId.trim();
|
|
}
|
|
if (typeof typedTarget.channelId === "string" && typedTarget.channelId.trim()) {
|
|
return typedTarget.channelId.trim();
|
|
}
|
|
if (typeof typedTarget.id === "string" && typedTarget.id.trim()) {
|
|
return typedTarget.id.trim();
|
|
}
|
|
}
|
|
return fallbackId;
|
|
}
|
|
|
|
function renderUsage() {
|
|
process.stderr.write(
|
|
"Usage: node scripts/e2e/multipass-openclaw-bridge.mjs <probe|send|wait> <extension> <state-dir>\n",
|
|
);
|
|
}
|
|
|
|
async function main() {
|
|
const mode = process.argv[2] ?? "";
|
|
const extension = process.argv[3] ?? "";
|
|
const stateDir = process.argv[4] ?? "";
|
|
if (!mode || !extension || !stateDir) {
|
|
renderUsage();
|
|
process.exit(1);
|
|
}
|
|
|
|
const rawInput = await readStdin();
|
|
const payload = parseInput(rawInput);
|
|
const providerId =
|
|
(typeof payload?.provider?.id === "string" && payload.provider.id) ||
|
|
`${extension}-multipass-fast`;
|
|
const statePath = stateFilePath(stateDir, providerId);
|
|
|
|
if (mode === "probe") {
|
|
process.stdout.write(
|
|
`${JSON.stringify(
|
|
{
|
|
healthy: true,
|
|
details: [`extension=${extension}`, `repo=${repoRoot}`, `state=${statePath}`],
|
|
},
|
|
null,
|
|
2,
|
|
)}\n`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (mode === "send") {
|
|
runFastSmoke(extension);
|
|
const threadId = resolveThreadId(payload, `${extension}:ci-fast-target`);
|
|
const messageId = `${extension}-fast-${Date.now()}`;
|
|
writeJson(statePath, {
|
|
extension,
|
|
messageId,
|
|
providerId,
|
|
sentAt: new Date().toISOString(),
|
|
text: typeof payload?.outbound?.text === "string" ? payload.outbound.text : "",
|
|
threadId,
|
|
});
|
|
process.stdout.write(`${JSON.stringify({ accepted: true, messageId, threadId }, null, 2)}\n`);
|
|
return;
|
|
}
|
|
|
|
if (mode === "wait") {
|
|
const currentState = readJson(statePath) ?? {};
|
|
const waitNonce = typeof payload?.wait?.nonce === "string" ? payload.wait.nonce.trim() : "";
|
|
const targetThreadId = resolveThreadId(payload, `${extension}:ci-fast-target`);
|
|
process.stdout.write(
|
|
`${JSON.stringify(
|
|
{
|
|
message: {
|
|
author: "assistant",
|
|
id: `${extension}-inbound-${Date.now()}`,
|
|
sentAt: new Date().toISOString(),
|
|
text: `ACK ${waitNonce || currentState.text || "nonce-missing"}`,
|
|
threadId:
|
|
typeof currentState.threadId === "string" && currentState.threadId.trim()
|
|
? currentState.threadId
|
|
: targetThreadId,
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
)}\n`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
throw new Error(`Unknown mode "${mode}". Expected probe, send, or wait.`);
|
|
}
|
|
|
|
const entryHref = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : "";
|
|
if (import.meta.url === entryHref) {
|
|
await main();
|
|
}
|