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.
258 lines
7.9 KiB
JavaScript
258 lines
7.9 KiB
JavaScript
#!/usr/bin/env node
|
|
// S7: agent target package on send + dirty write conflict gate (client static + API accept).
|
|
// 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_S7_BASE || process.env.MNOTE_PI_LAB_BASE || "http://127.0.0.1:3017";
|
|
const ACTOR_ID = process.env.MNOTE_S7_ACTOR_ID || "s7-target-admin";
|
|
const AUTH = process.env.MNOTE_S7_AUTH || "Bearer s7-target";
|
|
const ROOT =
|
|
process.env.MNOTE_S7_ROOT ||
|
|
fs.mkdtempSync(path.join(os.tmpdir(), "mnote-s7-target-"));
|
|
|
|
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 };
|
|
}
|
|
|
|
function staticChecks() {
|
|
const piLab = path.join(
|
|
__dirname,
|
|
"..",
|
|
"rust/crates/mnote-web/browser/sidebar-page-ai-pi-lab-runtime.js",
|
|
);
|
|
const src = fs.readFileSync(piLab, "utf8");
|
|
assert(src.includes("mnote.agent_target_package.v1"), "must build agent target package v1");
|
|
assert(
|
|
src.includes("buildPiLabAgentTargetPackage") || src.includes("targetPackage"),
|
|
"must attach targetPackage on send",
|
|
);
|
|
assert(
|
|
src.includes("assertPiLabTargetWritable") || src.includes("page_ai_target_buffer_not_writable"),
|
|
"must gate dirty buffer before send",
|
|
);
|
|
assert(
|
|
src.includes("writeRequiresCleanBuffer") || src.includes("fail_on_dirty_or_stale"),
|
|
"policy must require clean buffer / fail on dirty",
|
|
);
|
|
assert(
|
|
src.includes("targetPackage: targetPackage") || src.includes("targetPackage:targetPackage"),
|
|
"send body must include targetPackage",
|
|
);
|
|
|
|
const runtimeRs = path.join(
|
|
__dirname,
|
|
"..",
|
|
"rust/crates/mnote-web/src/routes/page_ai_pi/runtime.rs",
|
|
);
|
|
const rs = fs.readFileSync(runtimeRs, "utf8");
|
|
assert(
|
|
rs.includes("target_package") && rs.includes("PiLabSendRequest"),
|
|
"server PiLabSendRequest must accept target_package",
|
|
);
|
|
console.log(" [static] client target package + dirty gate + server accept field OK");
|
|
}
|
|
|
|
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}:s7`,
|
|
workspace_name: "S7 target",
|
|
root_uri: rootUri,
|
|
root_path: rootPath,
|
|
source_kind: "local_folder",
|
|
permission: "write",
|
|
capabilities: ["ai"],
|
|
grant_source: "s7_target_smoke",
|
|
grant_created_by: ACTOR_ID,
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
if (seed.status === 403 && seed.body && seed.body.code === "dev_seed_disabled") {
|
|
throw new Error(
|
|
"S7 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)}`,
|
|
);
|
|
}
|
|
|
|
async function main() {
|
|
staticChecks();
|
|
|
|
fs.mkdirSync(ROOT, { recursive: true });
|
|
const pagePath = "s7.md";
|
|
fs.writeFileSync(path.join(ROOT, pagePath), "# S7 target package\n", "utf8");
|
|
const rootUri = `file://${ROOT}`;
|
|
const workspaceId = `local-ws:${ACTOR_ID}:s7`;
|
|
const documentId = `local-doc:${workspaceId}:${pagePath}`;
|
|
|
|
console.log(`\n🧪 S7 agent target package + dirty guard (base=${BASE})\n`);
|
|
|
|
await seedWorkspace(rootUri, ROOT);
|
|
|
|
const sessionId = `pi_lab_s7_${Date.now()}`;
|
|
const start = await fetchJson(`${BASE}/api/page-ai/pi/start`, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
sessionId,
|
|
rootUri,
|
|
workspaceId,
|
|
pagePath,
|
|
pageTitle: "S7 target",
|
|
}),
|
|
});
|
|
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;
|
|
|
|
const targetPackage = {
|
|
schema: "mnote.agent_target_package.v1",
|
|
source: "s7_smoke",
|
|
frozenAt: Date.now(),
|
|
primaryTargetId: documentId,
|
|
workspaceId,
|
|
sourceKind: "local_folder",
|
|
rootUri,
|
|
documentId,
|
|
resourceKind: "page",
|
|
workspacePath: {
|
|
schema: "mnote.workspace_path.v1",
|
|
workspaceId,
|
|
sourceKind: "local_folder",
|
|
rootUri,
|
|
relativePath: pagePath,
|
|
documentId,
|
|
resourceKind: "page",
|
|
},
|
|
currentFile: {
|
|
rootUri,
|
|
relativePath: pagePath,
|
|
documentId,
|
|
resourceKind: "page",
|
|
},
|
|
allowedFiles: [pagePath],
|
|
targets: [
|
|
{
|
|
targetId: documentId,
|
|
documentId,
|
|
workspaceId,
|
|
sourceKind: "local_folder",
|
|
rootUri,
|
|
relativePath: pagePath,
|
|
resourceKind: "page",
|
|
dirtyState: "clean",
|
|
policy: {
|
|
permission: "read_write",
|
|
writeRequiresCleanBuffer: true,
|
|
conflictPolicy: "fail_on_dirty_or_stale",
|
|
},
|
|
},
|
|
],
|
|
policy: {
|
|
writeRequiresExplicitTarget: true,
|
|
allowedFilesSource: "selected_page_ai_target",
|
|
conflictPolicy: "fail_on_dirty_or_stale",
|
|
},
|
|
};
|
|
|
|
// Server must accept targetPackage on send (deserialize, not 422).
|
|
const send = await fetchJson(`${BASE}/api/page-ai/pi/send`, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
sessionId: sid,
|
|
message: "S7 with target package",
|
|
rootUri,
|
|
workspaceId,
|
|
pagePath,
|
|
pageTitle: "S7 target",
|
|
targetPackage,
|
|
}),
|
|
});
|
|
assert(
|
|
send.status === 200 && send.body.ok !== false,
|
|
`send with targetPackage: ${send.status} ${JSON.stringify(send.body)}`,
|
|
);
|
|
assert(
|
|
send.body.accepted === true || send.body.sessionId || send.body.ok === true,
|
|
`send should accept prompt: ${JSON.stringify(send.body)}`,
|
|
);
|
|
console.log(" send accepts targetPackage");
|
|
|
|
// buffer-state query shape (may 404 if no buffer opened — still valid API).
|
|
const bufferQs = new URLSearchParams({
|
|
documentId,
|
|
sourceKind: "local_folder",
|
|
rootUri,
|
|
workspaceId,
|
|
relativePath: pagePath,
|
|
});
|
|
const buffer = await fetchJson(`${BASE}/api/documents/buffer-state?${bufferQs.toString()}`);
|
|
// Not found is OK for never-opened buffer; found must include dirtyState when present.
|
|
if (buffer.status === 200 && buffer.body.ok === true) {
|
|
const result = buffer.body.result || buffer.body;
|
|
assert(
|
|
result.dirtyState !== undefined || result.dirty_state !== undefined || result.status,
|
|
`buffer-state payload missing dirty fields: ${JSON.stringify(buffer.body)}`,
|
|
);
|
|
console.log(` buffer-state present dirtyState=${result.dirtyState || result.dirty_state}`);
|
|
} else {
|
|
console.log(
|
|
` buffer-state ${buffer.status} (no open buffer — client gate uses snapshot dirty; OK)`,
|
|
);
|
|
}
|
|
|
|
// Client error code contract for UI.
|
|
const piLab = fs.readFileSync(
|
|
path.join(__dirname, "..", "rust/crates/mnote-web/browser/sidebar-page-ai-pi-lab-runtime.js"),
|
|
"utf8",
|
|
);
|
|
assert(
|
|
piLab.includes("page_ai_target_buffer_not_writable"),
|
|
"UI must surface page_ai_target_buffer_not_writable",
|
|
);
|
|
console.log(" dirty conflict code page_ai_target_buffer_not_writable present");
|
|
|
|
console.log("\n✅ S7 agent target package + dirty guard smoke passed\n");
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error("\n❌ S7 smoke failed:", err.message || err);
|
|
process.exit(1);
|
|
});
|