use super::{BuildTarget, Directory, copy_tree, success}; use dever::compiler::toolchain::packaging; use sha2::{Digest, Sha256}; use std::path::Path; use std::process::Command; #[test] fn unsigned_bootstrap_relocates_builds_and_rejects_changed_inputs() { let directory = Directory::new(); let author = directory.0.join("author"); let source = author.join("source"); let checkout = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap(); for path in [ "rust/Cargo.toml", "rust/Cargo.lock", "rust/rust-toolchain.toml", "rust/src", "lib", "skills", "LANGUAGE.md", "DOCUMENTATION.md", ] { copy_tree(&checkout.join(path), &source.join(path)); } copy_tree( Path::new(env!("CARGO_BIN_EXE_dever")), &author.join("bin/dever"), ); std::fs::create_dir(author.join("config")).unwrap(); let compiler = std::fs::read(author.join("bin/dever")).unwrap(); let mut input = serde_json::json!({ "format": "dever-local-bootstrap-input-v1", "version": env!("CARGO_PKG_VERSION"), "target": BuildTarget::host().unwrap().name(), "source": "source", "core": {"source": "bin/dever", "sha256": format!("{:x}", Sha256::digest(&compiler))} }); let settings = author.join("config/setting.json"); std::fs::write(&settings, serde_json::to_vec(&input).unwrap()).unwrap(); let bundle = directory.0.join("bootstrap"); success( Command::new(env!("CARGO_BIN_EXE_dever")) .args([ "release", author.to_str().unwrap(), "--output", bundle.to_str().unwrap(), ]) .output() .unwrap(), ); let manifest = packaging::verify(&bundle).unwrap(); assert!(!manifest.signed); assert!(manifest.artifacts.len() > 100); assert!(!bundle.join("config").exists()); assert!(packaging::create(&author, &source.join("rust/src/recursive")).is_err()); assert!(!source.join("rust/src/recursive").exists()); assert!(packaging::create(&author, &bundle).is_err()); packaging::verify(&bundle).unwrap(); let rejected = directory.0.join("rejected"); input["core"]["sha256"] = "0".repeat(64).into(); std::fs::write(&settings, serde_json::to_vec(&input).unwrap()).unwrap(); assert!(packaging::create(&author, &rejected).is_err()); assert!(!rejected.exists()); input["core"]["sha256"] = format!("{:x}", Sha256::digest(&compiler)).into(); input["source"] = "../author/source".into(); std::fs::write(&settings, serde_json::to_vec(&input).unwrap()).unwrap(); assert!(packaging::create(&author, &rejected).is_err()); // 作者输入移除并移动包后,实际 CLI 构建必须使用包内 runtime。 std::fs::remove_dir_all(&author).unwrap(); let moved = directory.0.join("moved"); std::fs::rename(&bundle, &moved).unwrap(); let cli = moved.join("bin/dever"); let project = directory.0.join("project"); success( Command::new(&cli) .args(["new", project.to_str().unwrap()]) .output() .unwrap(), ); success( Command::new(&cli) .args(["check", project.to_str().unwrap()]) .output() .unwrap(), ); let selected = success( Command::new(&cli) .args(["skill", "path", project.to_str().unwrap()]) .output() .unwrap(), ); assert_eq!(selected.trim(), moved.join("skills").to_str().unwrap()); let agents = project.join("AGENTS.md"); std::fs::write(&agents, "# 保留项目规则\n").unwrap(); success( Command::new(&cli) .current_dir(&project) .args(["skill", "install"]) .output() .unwrap(), ); let installed = std::fs::read(&agents).unwrap(); assert!(String::from_utf8_lossy(&installed).starts_with("# 保留项目规则\n")); success( Command::new(&cli) .current_dir(&project) .args(["skill", "install"]) .output() .unwrap(), ); assert_eq!(std::fs::read(&agents).unwrap(), installed); // 包内规则被篡改或移除时不能回退到作者源码,也不能修改项目文件。 let template = moved.join("skills/assets/template/AGENTS.md"); let original = std::fs::read(&template).unwrap(); std::fs::write(&template, "tampered").unwrap(); let failed = Command::new(&cli) .current_dir(&project) .args(["skill", "install"]) .output() .unwrap(); assert!(!failed.status.success()); assert!(String::from_utf8_lossy(&failed.stderr).contains("artifact changed")); assert_eq!(std::fs::read(&agents).unwrap(), installed); std::fs::remove_file(&template).unwrap(); assert!( !Command::new(&cli) .current_dir(&project) .args(["skill", "path", "."]) .output() .unwrap() .status .success() ); assert_eq!(std::fs::read(&agents).unwrap(), installed); std::fs::write(&template, original).unwrap(); let binary = directory.0.join("hello"); success( Command::new(&cli) .args([ "build", project.to_str().unwrap(), "--output", binary.to_str().unwrap(), ]) .output() .unwrap(), ); assert!(moved.join("rust/target/generated").is_dir()); let output = success( Command::new(&binary) .args(["hello.greeting.greet", r#"{"name":"relocated"}"#]) .output() .unwrap(), ); assert_eq!( serde_json::from_str::(&output).unwrap()["data"], "Hello, relocated" ); packaging::verify(&moved).unwrap(); // 连同已生成的 Cargo 缓存再次搬移,不能保留旧 runtime 绝对路径。 let relocated = directory.0.join("relocated"); std::fs::rename(&moved, &relocated).unwrap(); success( Command::new(relocated.join("bin/dever")) .args(["build", project.to_str().unwrap(), "--output"]) .arg(directory.0.join("rebuilt")) .output() .unwrap(), ); std::fs::rename(&relocated, &moved).unwrap(); std::fs::write(moved.join("rust/src/runtime/lib.rs"), "tampered").unwrap(); assert!(packaging::verify(&moved).is_err()); let failed = Command::new(&cli) .args([ "build", project.to_str().unwrap(), "--output", directory.0.join("tampered").to_str().unwrap(), ]) .output() .unwrap(); assert!(!failed.status.success()); assert!(String::from_utf8_lossy(&failed.stderr).contains("artifact changed")); assert!(!directory.0.join("tampered").exists()); std::fs::remove_file(moved.join("bootstrap.json")).unwrap(); let missing = Command::new(&cli) .args([ "build", project.to_str().unwrap(), "--output", directory.0.join("missing").to_str().unwrap(), ]) .output() .unwrap(); assert!(!missing.status.success()); assert!(String::from_utf8_lossy(&missing.stderr).contains("bootstrap.json")); assert!(!directory.0.join("missing").exists()); }