Files
mnote/scripts/run-s2-s7-smokes.js
T

184 lines
5.4 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
// Start mnote-web (mock Pi) on a free port and run S2S7 acceptance smokes.
// Does not touch unrelated workspace state. Prefer existing BASE if set.
"use strict";
const { spawn } = require("node:child_process");
const fs = require("node:fs");
const net = require("node:net");
const os = require("node:os");
const path = require("node:path");
const { pathToFileURL } = require("node:url");
const REPO = path.resolve(__dirname, "..");
const RUST_DIR = path.join(REPO, "rust");
const BINARY = path.join(RUST_DIR, "target/debug/mnote-web");
const TIMEOUT_MS = Number(process.env.MNOTE_S2_S7_TIMEOUT_MS || 120_000);
const EXTERNAL_BASE = (process.env.MNOTE_PI_LAB_BASE || process.env.MNOTE_S2_BASE || "").replace(
/\/+$/,
"",
);
const SMOKES = [
"task-s2-skills-mcp-delete-no-ghost-smoke.js",
"task-s3-directory-grants-allowed-roots-smoke.js",
"task-s4-pi-abort-idempotent-smoke.js",
"task-s5-rag-health-summary-filetree-smoke.js",
"task-s6-pi-resume-history-hydrate-smoke.js",
"task-s7-agent-target-dirty-guard-smoke.js",
];
function findFreePort() {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.listen(0, "127.0.0.1", () => {
const address = server.address();
if (!address || typeof address === "string") {
server.close(() => reject(new Error("无法分配测试端口")));
return;
}
const port = address.port;
server.close(() => resolve(port));
});
server.on("error", reject);
});
}
async function waitForGateway(baseUrl) {
const deadline = Date.now() + TIMEOUT_MS;
let lastError = null;
while (Date.now() < deadline) {
try {
const response = await fetch(`${baseUrl}/api/gateway/health`, {
redirect: "manual",
cache: "no-store",
});
if (response.ok && response.headers.get("x-mnote-web-owner") === "mnote-web") {
return;
}
lastError = new Error(`gateway health: ${response.status}`);
} catch (error) {
lastError = error;
}
await new Promise((r) => setTimeout(r, 300));
}
throw lastError || new Error("gateway health 未就绪");
}
function startGateway(port, controlPlaneDb) {
const env = {
...process.env,
MNOTE_WEB_ALLOW_DEV_FIXTURES: "1",
MNOTE_WEB_BIND: `127.0.0.1:${port}`,
MNOTE_WEB_PUBLIC_BIND: `127.0.0.1:${port}`,
MNOTE_CONTROL_PLANE_BACKEND: "libsql-local",
MNOTE_TURSO_LOCAL_PATH: controlPlaneDb,
MNOTE_PAGE_AI_PI_LAB: "1",
MNOTE_PAGE_AI_PI_LAB_RUNTIME: "mock",
MNOTE_PAGE_AI_PI_WARMUP: "0",
MNOTE_PAGE_AI_PI_WARMUP_SEND: "0",
RUST_LOG: process.env.RUST_LOG || "warn",
};
const cmd = fs.existsSync(BINARY)
? [BINARY]
: ["cargo", "run", "-q", "-p", "mnote-web", "--bin", "mnote-web"];
const child = spawn(cmd[0], cmd.slice(1), {
cwd: RUST_DIR,
env,
stdio: ["ignore", "pipe", "pipe"],
});
let stderr = "";
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
if (stderr.length > 20_000) stderr = stderr.slice(-20_000);
});
child.stdout.on("data", () => {});
child._s2s7Stderr = () => stderr;
return child;
}
function runSmoke(script, base) {
return new Promise((resolve, reject) => {
const env = {
...process.env,
MNOTE_PI_LAB_BASE: base,
MNOTE_S2_BASE: base,
MNOTE_S3_BASE: base,
MNOTE_S4_BASE: base,
MNOTE_S5_BASE: base,
MNOTE_S6_BASE: base,
MNOTE_S7_BASE: base,
};
const child = spawn(process.execPath, [path.join(__dirname, script)], {
cwd: REPO,
env,
stdio: ["ignore", "pipe", "pipe"],
});
let out = "";
let err = "";
child.stdout.on("data", (c) => {
out += c.toString();
process.stdout.write(c);
});
child.stderr.on("data", (c) => {
err += c.toString();
process.stderr.write(c);
});
child.on("exit", (code) => {
if (code === 0) resolve({ script, out });
else reject(new Error(`${script} exited ${code}\n${err || out}`));
});
});
}
async function main() {
let child = null;
let base = EXTERNAL_BASE;
const tmpDbDir = fs.mkdtempSync(path.join(os.tmpdir(), "mnote-s2s7-cp-"));
const controlPlaneDb = path.join(tmpDbDir, "control-plane-s2s7.db");
try {
if (!base) {
const port = await findFreePort();
base = `http://127.0.0.1:${port}`;
console.log(`\n🚀 starting mnote-web mock on ${base}\n`);
child = startGateway(port, controlPlaneDb);
child.on("exit", (code, signal) => {
if (code && code !== 0) {
console.error(`mnote-web exited code=${code} signal=${signal}`);
console.error(child._s2s7Stderr && child._s2s7Stderr());
}
});
await waitForGateway(base);
console.log(" gateway ready\n");
} else {
console.log(`\n🔗 using external BASE ${base}\n`);
await waitForGateway(base);
}
const results = [];
for (const script of SMOKES) {
console.log(`── ${script} ──`);
await runSmoke(script, base);
results.push(script);
}
console.log(`\n✅ S2S7 all passed (${results.length}):\n ${results.join("\n ")}\n`);
} finally {
if (child && !child.killed) {
child.kill("SIGTERM");
await new Promise((r) => setTimeout(r, 500));
try {
child.kill("SIGKILL");
} catch {
/* ignore */
}
}
}
}
main().catch((err) => {
console.error("\n❌ S2S7 runner failed:", err.message || err);
process.exit(1);
});