Persist PageTree expand state via control-plane view-state and align chevron/DOM with restored expansion; keep Sidex-style shallow page-tree scan and drop the unused recursive scanner that only added cargo noise. Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi into a module package, and retire Hermes/ACP/OpenHub recycle + root harness evidence from the index while gitignoring recycle and local diag dumps. Archive superseded design/bugs docs under old/, point architecture at ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor regressions so the working tree can stay clean.
184 lines
5.4 KiB
JavaScript
184 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
||
// Start mnote-web (mock Pi) on a free port and run S2–S7 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✅ S2–S7 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❌ S2–S7 runner failed:", err.message || err);
|
||
process.exit(1);
|
||
});
|