//! 通过真实 CLI 验证规则来源、原子合并及拒绝输入;只操作独占临时项目。 use super::{Directory, success}; use std::os::unix::fs::{MetadataExt, PermissionsExt, symlink}; use std::path::Path; use std::process::{Command, Output}; fn cli(root: &Path, arguments: &[&str]) -> Output { Command::new(env!("CARGO_BIN_EXE_dever")) .current_dir(root) .args(arguments) .output() .unwrap() } fn template() -> String { std::fs::read_to_string( Path::new(env!("CARGO_MANIFEST_DIR")).join("../skills/assets/template/AGENTS.md"), ) .unwrap() } #[test] fn path_uses_current_toolchain_without_project_version_or_mutation() { let directory = Directory::new(); let root = directory.0.join("app"); dever::compiler::project::create(&root).unwrap(); let settings = root.join("config/setting.json"); let original = std::fs::read(&settings).unwrap(); let value: serde_json::Value = serde_json::from_slice(&original).unwrap(); assert!(value.get("dever").is_none()); let agents = std::fs::read(root.join("AGENTS.md")).unwrap(); let expected = Path::new(env!("CARGO_MANIFEST_DIR")) .join("../skills") .canonicalize() .unwrap(); for (cwd, arguments) in [ (root.as_path(), vec!["skill", "path", "."]), (root.as_path(), vec!["skill", "path"]), (directory.0.as_path(), vec!["skill", "path"]), ] { assert_eq!( success(cli(cwd, &arguments)).trim(), expected.to_str().unwrap() ); } assert_eq!(std::fs::read(&settings).unwrap(), original); assert_eq!(std::fs::read(root.join("AGENTS.md")).unwrap(), agents); // 查询与安装都不创建项目配置,更不引入项目版本锁定。 std::fs::remove_file(&settings).unwrap(); success(cli(&root, &["skill", "path", "."])); success(cli(&root, &["skill", "install"])); assert!(!settings.exists()); assert!(!cli(&directory.0, &["skill", "path", "."]).status.success()); assert!(!cli(&directory.0, &["skill", "install"]).status.success()); assert!(!directory.0.join("AGENTS.md").exists()); assert!(!cli(&root, &["skill", "install", "."]).status.success()); } #[test] fn install_preserves_unmarked_content_permissions_and_is_idempotent() { let directory = Directory::new(); let root = directory.0.join("app"); dever::compiler::project::create(&root).unwrap(); let agents = root.join("AGENTS.md"); std::fs::remove_file(&agents).unwrap(); success(cli(&root, &["skill", "install"])); assert_eq!(std::fs::read_to_string(&agents).unwrap(), template()); assert_eq!(std::fs::metadata(&agents).unwrap().mode() & 0o777, 0o644); let prefix = "# 项目规则\r\n保留这些内容。"; std::fs::write(&agents, prefix).unwrap(); std::fs::set_permissions(&agents, std::fs::Permissions::from_mode(0o640)).unwrap(); success(cli(&root, &["skill", "install"])); let installed = format!("{prefix}\n\n{}", template()); assert_eq!(std::fs::read_to_string(&agents).unwrap(), installed); let before = std::fs::metadata(&agents).unwrap(); assert_eq!(before.mode() & 0o777, 0o640); success(cli(&root, &["skill", "install"])); let after = std::fs::metadata(&agents).unwrap(); assert_eq!(std::fs::read_to_string(&agents).unwrap(), installed); assert_eq!(before.ino(), after.ino()); assert_eq!(before.modified().unwrap(), after.modified().unwrap()); let prefix = "保留头部\r\n\r\n"; let suffix = "\r\n\r\n保留尾部\r\n"; std::fs::write( &agents, format!( "{prefix}\r\n旧规则\r\n{suffix}" ), ) .unwrap(); success(cli(&root, &["skill", "install"])); assert_eq!( std::fs::read_to_string(&agents).unwrap(), format!("{prefix}{}{suffix}", template().trim_end_matches('\n')) ); // 同一项目的并发安装不得附加两份规则或留下暂存文件。 std::fs::remove_file(&agents).unwrap(); let mut first = Command::new(env!("CARGO_BIN_EXE_dever")) .current_dir(&root) .args(["skill", "install"]) .stdout(std::process::Stdio::null()) .spawn() .unwrap(); success(cli(&root, &["skill", "install"])); assert!(first.wait().unwrap().success()); assert_eq!(std::fs::read_to_string(&agents).unwrap(), template()); assert!(!std::fs::read_dir(&root).unwrap().any(|entry| { entry .unwrap() .file_name() .to_string_lossy() .starts_with(".dever-skill-") })); } #[test] fn install_rejects_malformed_markers_and_links_without_mutation() { let directory = Directory::new(); let root = directory.0.join("app"); dever::compiler::project::create(&root).unwrap(); let agents = root.join("AGENTS.md"); let start = ""; let end = ""; for text in [ start.to_owned(), end.to_owned(), format!("{end}\n{start}"), format!("{start}\n{end}\n{start}\n{end}"), format!("{start}\n{start}\n{end}"), format!("{start}\n{end}\n{end}"), "".into(), format!("inline {start}\n{end}"), ] { std::fs::write(&agents, &text).unwrap(); assert!( !cli(&root, &["skill", "install"]).status.success(), "{text}" ); assert_eq!(std::fs::read_to_string(&agents).unwrap(), text); } std::fs::write(&agents, [0xff, 0xfe]).unwrap(); assert!(!cli(&root, &["skill", "install"]).status.success()); assert_eq!(std::fs::read(&agents).unwrap(), [0xff, 0xfe]); std::fs::remove_file(&agents).unwrap(); let target = directory.0.join("private"); std::fs::write(&target, "untouched").unwrap(); symlink(&target, &agents).unwrap(); assert!(!cli(&root, &["skill", "install"]).status.success()); assert_eq!(std::fs::read_to_string(&target).unwrap(), "untouched"); assert!(std::fs::symlink_metadata(&agents).unwrap().is_symlink()); std::fs::remove_file(&agents).unwrap(); std::fs::create_dir(&agents).unwrap(); assert!(!cli(&root, &["skill", "install"]).status.success()); assert!(agents.is_dir()); let linked = directory.0.join("linked"); symlink(&root, &linked).unwrap(); assert!( !cli(&directory.0, &["skill", "path", linked.to_str().unwrap()]) .status .success() ); assert!(!std::fs::read_dir(&root).unwrap().any(|entry| { entry .unwrap() .file_name() .to_string_lossy() .starts_with(".dever-skill-") })); }