test(onboard/parity): 补「计划即实际写入」端到端测试,修正 plan_onboarding 只读注释(P8c 收尾)

- 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 <noreply@anthropic.com>
This commit is contained in:
lanrtop 2026-10-03 10:08:43 +09:00
parent ea8ed26191
commit a3d1cc169b
2 changed files with 34 additions and 1 deletions

View File

@ -1701,7 +1701,8 @@ pub fn client_static_status(project_id: &str) -> Vec<ClientReport> {
// ── 一键接入计划(P8c):本机侧预计动作 + 项目文件演算 ────────────────────────── // ── 一键接入计划(P8c):本机侧预计动作 + 项目文件演算 ──────────────────────────
/// 一键接入写入前计划(只读:不写文件、不起子进程)。P8e 接入 MCP / Tauri 投影前暂无生产调用方。 /// 一键接入写入前计划(只读:不写文件、不起客户端 CLI;混合文件预警会起只读的
/// git status --no-optional-locks 与 git show)。P8e 接入 MCP / Tauri 投影前暂无生产调用方。
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) fn plan_onboarding( pub(crate) fn plan_onboarding(
project_id: &str, project_id: &str,

View File

@ -443,6 +443,38 @@ mod tests {
let _ = std::fs::remove_dir_all(&root); 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> = String::from_utf8_lossy(&status.stdout)
.lines()
.map(|line| line[3..].trim_matches('"').to_string())
.collect();
changed.sort();
let mut planned: Vec<String> = plan.files.iter().map(|f| f.path.clone()).collect();
planned.sort();
assert_eq!(changed, planned, "apply 写入的文件集合与计划一致");
let _ = std::fs::remove_dir_all(&root);
}
#[test] #[test]
fn plan_after_apply_is_noop_and_digest_tracks_changes() { fn plan_after_apply_is_noop_and_digest_tracks_changes() {
let root = temp_repo("applied"); let root = temp_repo("applied");