chore: land tree view-state, vault, Pi module split, and repo hygiene
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.
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
#!/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);
|
||||
});
|
||||
Reference in New Issue
Block a user