From a3d1cc169b09590db22af01c1ccc017d484744d8 Mon Sep 17 00:00:00 2001 From: lanrtop Date: Sat, 3 Oct 2026 10:08:43 +0900 Subject: [PATCH] =?UTF-8?q?test(onboard/parity):=20=E8=A1=A5=E3=80=8C?= =?UTF-8?q?=E8=AE=A1=E5=88=92=E5=8D=B3=E5=AE=9E=E9=99=85=E5=86=99=E5=85=A5?= =?UTF-8?q?=E3=80=8D=E7=AB=AF=E5=88=B0=E7=AB=AF=E6=B5=8B=E8=AF=95=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20plan=5Fonboarding=20=E5=8F=AA=E8=AF=BB?= =?UTF-8?q?=E6=B3=A8=E9=87=8A=EF=BC=88P8c=20=E6=94=B6=E5=B0=BE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - plan_predicts_exactly_what_apply_writes:空仓库先 plan 再经 DiskSink apply,逐文件比对计划内容哈希与 实际写入,并核对 git status 的文件集合与计划一致。P8d 的 digest 复核依赖这条不变式; 变异 PlanSink::read 忽略待写内容时,原有 plan 端到端测试仍通过,只有本测试与记录汇单元测试失败。 - mcp_inject::plan_onboarding 注释由「不起子进程」改为「不起客户端 CLI,混合文件预警会起只读 git」, 与 has_uncommitted_outside_managed 的实际行为一致。 cargo test 311 passed / 0 failed / 5 ignored。 Co-Authored-By: Claude Opus 5.5 --- src-tauri/src/mcp_inject.rs | 3 ++- src-tauri/src/onboard/plan.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/mcp_inject.rs b/src-tauri/src/mcp_inject.rs index 0a6e7f4..cef6253 100644 --- a/src-tauri/src/mcp_inject.rs +++ b/src-tauri/src/mcp_inject.rs @@ -1701,7 +1701,8 @@ pub fn client_static_status(project_id: &str) -> Vec { // ── 一键接入计划(P8c):本机侧预计动作 + 项目文件演算 ────────────────────────── -/// 一键接入写入前计划(只读:不写文件、不起子进程)。P8e 接入 MCP / Tauri 投影前暂无生产调用方。 +/// 一键接入写入前计划(只读:不写文件、不起客户端 CLI;混合文件预警会起只读的 +/// git status --no-optional-locks 与 git show)。P8e 接入 MCP / Tauri 投影前暂无生产调用方。 #[allow(dead_code)] pub(crate) fn plan_onboarding( project_id: &str, diff --git a/src-tauri/src/onboard/plan.rs b/src-tauri/src/onboard/plan.rs index ce4a916..79f6608 100644 --- a/src-tauri/src/onboard/plan.rs +++ b/src-tauri/src/onboard/plan.rs @@ -443,6 +443,38 @@ mod tests { let _ = std::fs::remove_dir_all(&root); } + /// 「看到的就是将写的」:计划列出的文件集合与内容哈希,和随后 apply 实际写入的完全一致 + /// (P8d 的 digest 复核依赖这条不变式)。 + #[test] + fn plan_predicts_exactly_what_apply_writes() { + let root = temp_repo("predict"); + let plan = plan_onboarding(&root, &PROJECT, machine("apply")); + assert!(write_project_files(&root, &PROJECT, &crate::DiskSink).is_empty()); + for file in &plan.files { + let written = std::fs::read_to_string(root.join(&file.path)).unwrap(); + assert_eq!( + sha256_hex(&written.replace("\r\n", "\n")), + file.content_sha256, + "{} 计划内容与实际写入不一致", + file.path + ); + } + let status = std::process::Command::new("git") + .args(["status", "--porcelain", "-uall"]) + .current_dir(&root) + .output() + .expect("git 可执行"); + let mut changed: Vec = String::from_utf8_lossy(&status.stdout) + .lines() + .map(|line| line[3..].trim_matches('"').to_string()) + .collect(); + changed.sort(); + let mut planned: Vec = plan.files.iter().map(|f| f.path.clone()).collect(); + planned.sort(); + assert_eq!(changed, planned, "apply 写入的文件集合与计划一致"); + let _ = std::fs::remove_dir_all(&root); + } + #[test] fn plan_after_apply_is_noop_and_digest_tracks_changes() { let root = temp_repo("applied");