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,184 @@
|
||||
#!/usr/bin/env node
|
||||
// S4: Pi abort once — API abort works; client reducer is idempotent (static + mock API).
|
||||
// Requires mnote-web with MNOTE_WEB_ALLOW_DEV_FIXTURES=1 + MNOTE_PAGE_AI_PI_LAB_RUNTIME=mock.
|
||||
|
||||
"use strict";
|
||||
|
||||
const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
const BASE = process.env.MNOTE_S4_BASE || process.env.MNOTE_PI_LAB_BASE || "http://127.0.0.1:3017";
|
||||
const ACTOR_ID = process.env.MNOTE_S4_ACTOR_ID || "s4-abort-admin";
|
||||
const AUTH = process.env.MNOTE_S4_AUTH || "Bearer s4-abort";
|
||||
const ROOT =
|
||||
process.env.MNOTE_S4_ROOT ||
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), "mnote-s4-abort-"));
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
async function fetchJson(url, options = {}) {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: AUTH,
|
||||
"x-mnote-actor-id": ACTOR_ID,
|
||||
"x-mnote-actor-type": "admin",
|
||||
...(options.headers || {}),
|
||||
};
|
||||
const res = await fetch(url, { ...options, headers });
|
||||
const text = await res.text();
|
||||
let body = {};
|
||||
try {
|
||||
body = text ? JSON.parse(text) : {};
|
||||
} catch {
|
||||
body = { raw: text };
|
||||
}
|
||||
return { status: res.status, body };
|
||||
}
|
||||
|
||||
async function seedWorkspace(rootUri, rootPath) {
|
||||
const seed = await fetchJson(`${BASE}/api/dev/seed`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
seeds: [
|
||||
{
|
||||
kind: "setupWorkspace",
|
||||
user_id: ACTOR_ID,
|
||||
email: `${ACTOR_ID}@example.com`,
|
||||
username: ACTOR_ID,
|
||||
display_name: ACTOR_ID,
|
||||
role: "admin",
|
||||
password: process.env.MNOTE_E2E_PASSWORD || "MnoteE2E123!",
|
||||
workspace_id: `local-ws:${ACTOR_ID}:s4`,
|
||||
workspace_name: "S4 abort",
|
||||
root_uri: rootUri,
|
||||
root_path: rootPath,
|
||||
source_kind: "local_folder",
|
||||
permission: "write",
|
||||
capabilities: ["ai"],
|
||||
grant_source: "s4_abort_smoke",
|
||||
grant_created_by: ACTOR_ID,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
if (seed.status === 403 && seed.body && seed.body.code === "dev_seed_disabled") {
|
||||
throw new Error(
|
||||
"S4 smoke 需要 /api/dev/seed;请以 MNOTE_WEB_ALLOW_DEV_FIXTURES=1 启动 mnote-web。",
|
||||
);
|
||||
}
|
||||
assert(
|
||||
seed.status === 200 && seed.body.ok === true,
|
||||
`/api/dev/seed failed: ${seed.status} ${JSON.stringify(seed.body)}`,
|
||||
);
|
||||
}
|
||||
|
||||
function staticClientChecks() {
|
||||
const runtimePath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"rust/crates/mnote-web/browser/sidebar-page-ai-pi-lab-runtime.js",
|
||||
);
|
||||
const src = fs.readFileSync(runtimePath, "utf8");
|
||||
assert(
|
||||
src.includes("assistant_abort") && src.includes("STATE_ABORTED"),
|
||||
"client must handle assistant_abort → STATE_ABORTED",
|
||||
);
|
||||
assert(
|
||||
src.includes("abort is idempotent") ||
|
||||
(src.includes("runtimeStatus === STATE_ABORTED") && src.includes("assistant_abort")),
|
||||
"client reducer must short-circuit second assistant_abort",
|
||||
);
|
||||
// Optimistic single apply before fire-and-forget ABORT.
|
||||
assert(
|
||||
/function abortPrompt[\s\S]*?applyPiRunEvent\(\{\s*type:\s*'assistant_abort'/.test(src) ||
|
||||
/function abortPrompt[\s\S]*?applyPiRunEvent\(\{ type: 'assistant_abort'/.test(src),
|
||||
"abortPrompt must optimistically apply assistant_abort once",
|
||||
);
|
||||
assert(
|
||||
/function abortPrompt[\s\S]*?fetch\(API\.ABORT/.test(src) ||
|
||||
/function abortPrompt[\s\S]*?API\.ABORT/.test(src),
|
||||
"abortPrompt must still call ABORT API",
|
||||
);
|
||||
console.log(" [static] client abort once + idempotent reducer OK");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
staticClientChecks();
|
||||
|
||||
fs.mkdirSync(ROOT, { recursive: true });
|
||||
fs.writeFileSync(path.join(ROOT, "s4.md"), "# S4 abort\n", "utf8");
|
||||
const rootUri = `file://${ROOT}`;
|
||||
|
||||
console.log(`\n🧪 S4 Pi abort idempotent (base=${BASE}, actor=${ACTOR_ID})\n`);
|
||||
|
||||
await seedWorkspace(rootUri, ROOT);
|
||||
|
||||
const status = await fetchJson(`${BASE}/api/page-ai/pi/status`);
|
||||
assert(status.status === 200 && status.body.ok === true, `status: ${status.status}`);
|
||||
if (status.body.runtimeMode && status.body.runtimeMode !== "mock") {
|
||||
console.log(` ⚠ runtimeMode=${status.body.runtimeMode} (prefer mock for this smoke)`);
|
||||
}
|
||||
|
||||
const sessionId = `pi_lab_s4_${Date.now()}`;
|
||||
const start = await fetchJson(`${BASE}/api/page-ai/pi/start`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
sessionId,
|
||||
rootUri,
|
||||
workspaceId: `local-ws:${ACTOR_ID}:s4`,
|
||||
pagePath: "s4.md",
|
||||
pageTitle: "S4 abort",
|
||||
}),
|
||||
});
|
||||
assert(start.status === 200 && start.body.ok === true, `start: ${start.status} ${JSON.stringify(start.body)}`);
|
||||
const sid = (start.body.session && start.body.session.sessionId) || sessionId;
|
||||
console.log(` started ${sid}`);
|
||||
|
||||
// Send may stream; abort immediately after.
|
||||
const send = await fetchJson(`${BASE}/api/page-ai/pi/send`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
sessionId: sid,
|
||||
message: "S4 long answer please keep streaming",
|
||||
rootUri,
|
||||
}),
|
||||
});
|
||||
// Mock may accept or complete quickly — either is fine before abort.
|
||||
assert(
|
||||
send.status === 200 || send.status === 409 || send.status === 400,
|
||||
`send unexpected: ${send.status} ${JSON.stringify(send.body)}`,
|
||||
);
|
||||
console.log(` send status=${send.status}`);
|
||||
|
||||
const abort1 = await fetchJson(`${BASE}/api/page-ai/pi/abort`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ sessionId: sid }),
|
||||
});
|
||||
assert(
|
||||
abort1.status === 200 && abort1.body.ok === true && abort1.body.aborted === true,
|
||||
`abort1: ${abort1.status} ${JSON.stringify(abort1.body)}`,
|
||||
);
|
||||
assert(abort1.body.stopReason === "aborted" || abort1.body.aborted === true, "abort1 stopReason");
|
||||
console.log(" abort1 ok");
|
||||
|
||||
// Second abort must still succeed (idempotent server) without double-error.
|
||||
const abort2 = await fetchJson(`${BASE}/api/page-ai/pi/abort`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ sessionId: sid }),
|
||||
});
|
||||
assert(
|
||||
abort2.status === 200 && abort2.body.ok === true && abort2.body.aborted === true,
|
||||
`abort2 must be idempotent: ${abort2.status} ${JSON.stringify(abort2.body)}`,
|
||||
);
|
||||
console.log(" abort2 idempotent ok");
|
||||
|
||||
console.log("\n✅ S4 Pi abort idempotent smoke passed\n");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("\n❌ S4 smoke failed:", err.message || err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user