#!/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); });