#!/usr/bin/env node "use strict"; const assert = require("node:assert"); const fs = require("node:fs"); const path = require("node:path"); const { execFileSync } = require("node:child_process"); const ROOT = process.env.MNOTE_REPO_ROOT || "/mnt/Data1T/mnote"; const TASK535 = path.join(ROOT, "scripts", "task535-page-ai-local-agent-clean-edit-smoke.js"); const OUTPUT_DIR = path.join(ROOT, "tmp", "task539-local-agent-audit-scope-contract"); const RESULT_PATH = path.join(OUTPUT_DIR, "result.json"); function run(command, args, options = {}) { return execFileSync(command, args, { cwd: ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], ...options, }); } function parseLastJsonObject(output) { const trimmed = String(output || "").trim(); const start = trimmed.lastIndexOf("\n{"); const jsonText = start >= 0 ? trimmed.slice(start + 1) : trimmed; return JSON.parse(jsonText); } function parseRunBody(task535Result) { const runRecord = (task535Result.captured || []).find((item) => item.kind === "run"); assert(runRecord, "task535 result 缺少 run payload"); return JSON.parse(runRecord.body || "{}"); } function main() { fs.mkdirSync(OUTPUT_DIR, { recursive: true }); const task535Output = run("node", [TASK535]); const task535Result = parseLastJsonObject(task535Output); assert.equal(task535Result.ok, true, "task535 clean edit smoke 必须通过"); const runBody = parseRunBody(task535Result); const allowedFiles = runBody.targetPackage?.allowedFiles || []; assert.deepEqual(allowedFiles, [task535Result.relativePath], "targetPackage.allowedFiles 应只包含当前目标文件"); assert.equal(runBody.targetPackage?.currentFile?.relativePath, task535Result.relativePath, "currentFile 应冻结为当前目标文件"); assert.equal(runBody.runTargetSnapshot?.schema, "mnote.page_ai_run_target_snapshot.v1", "run payload 应携带 frozen target snapshot"); assert(runBody.runTargetSnapshot?.frozenAt, "frozen target snapshot 应有 frozenAt"); if (runBody.agentTargetPackage) { assert.deepEqual(runBody.agentTargetPackage.allowedFiles || [], [task535Result.relativePath], "agentTargetPackage.allowedFiles 应只包含当前目标文件"); } assert.equal(task535Result.usedDocumentsSave, false, "clean edit 不应调用 /api/documents/save"); assert.equal(task535Result.usedMarkdownEdit, false, "clean edit 不应调用 mnote.doc.markdown_edit"); assert.equal(task535Result.usedPageSave, false, "clean edit 不应调用 mnote.page.save"); const cargoOutput = run("cargo", ["test", "-p", "mnote-web", "local_agent_audit", "--", "--test-threads=1"], { cwd: path.join(ROOT, "rust"), }); assert(cargoOutput.includes("local_agent_audit_allowed_files_override_folder_context"), "cargo test 应覆盖 allowedFiles 优先于 folder context"); assert(cargoOutput.includes("local_agent_audit_reads_allowed_files_from_agent_run_envelope"), "cargo test 应覆盖 agentRunEnvelope.allowedFiles"); assert(cargoOutput.includes("local_agent_audit_event_carries_agent_run_receipt"), "cargo test 应覆盖 receipt auditScope"); const result = { ok: true, task: "task539-local-agent-audit-scope-contract", task535: { resultPath: path.join(ROOT, "tmp", "task535-page-ai-local-agent-clean-edit-smoke", "result.json"), relativePath: task535Result.relativePath, targetAllowedFiles: allowedFiles, frozenAt: runBody.runTargetSnapshot.frozenAt, usedDocumentsSave: task535Result.usedDocumentsSave, usedMarkdownEdit: task535Result.usedMarkdownEdit, usedPageSave: task535Result.usedPageSave, }, backendAudit: { command: "cargo test -p mnote-web local_agent_audit -- --test-threads=1", expectedScope: "allowed_files", expectedFileCount: 1, }, }; fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8"); console.log(JSON.stringify(result, null, 2)); } try { main(); } catch (error) { fs.mkdirSync(OUTPUT_DIR, { recursive: true }); fs.writeFileSync( path.join(OUTPUT_DIR, "failure.json"), `${JSON.stringify({ ok: false, error: error.stack || error.message || String(error) }, null, 2)}\n`, "utf8", ); console.error(error.stack || error.message || String(error)); process.exit(1); }