Files
core/test/dever-cli-tests/project_skill.rs
T

253 lines
8.8 KiB
Rust

use super::*;
use std::os::unix::fs::{PermissionsExt, symlink};
use std::process::Output;
struct Project {
temporary: TemporaryDirectory,
layout: Layout,
root: PathBuf,
key: Ed25519KeyPair,
}
impl Project {
fn new() -> Self {
let temporary = TemporaryDirectory::new();
let layout = Layout::new(temporary.path().join("machine"));
layout.initialize().unwrap();
let key = signing_key();
fs::write(
layout.state().join("trusted-release-key"),
format!("{}\n", hex(key.public_key().as_ref())),
)
.unwrap();
write_release(
&layout,
&key,
"1.0.0",
&temporary.path().join("dispatch"),
true,
true,
);
MachineManager::new(layout.clone())
.install_requested("1.0.0")
.unwrap();
fs::copy(
fixture_binary(env!("CARGO_BIN_EXE_dever-launcher")),
layout.bin().join("dever"),
)
.unwrap();
let root = temporary.path().join("project");
fs::create_dir_all(root.join("module")).unwrap();
fs::create_dir(root.join("config")).unwrap();
fs::write(root.join("config/setting.json"), "{}").unwrap();
Self {
temporary,
layout,
root,
key,
}
}
fn run(&self, root: &Path, extra: &[&str]) -> Output {
Command::new(self.layout.bin().join("dever"))
.args(["skill", "install"])
.args(extra)
.current_dir(root)
.env_clear()
.output()
.unwrap()
}
fn succeeds(&self) {
let output = self.run(&self.root, &[]);
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(String::from_utf8_lossy(&output.stdout).contains("AGENTS.md"));
self.assert_no_staging();
}
fn fails(&self, message: &str) {
let output = self.run(&self.root, &[]);
assert!(!output.status.success());
assert!(
String::from_utf8_lossy(&output.stderr).contains(message),
"{}",
String::from_utf8_lossy(&output.stderr)
);
self.assert_no_staging();
}
fn assert_no_staging(&self) {
assert!(fs::read_dir(&self.root).unwrap().all(|entry| {
!entry
.unwrap()
.file_name()
.to_string_lossy()
.starts_with(".dever-skill-")
}));
}
fn sign_manifest(&self, manifest: &ReleaseManifest) {
let release = self.layout.versions().join("1.0.0");
let bytes = serde_json::to_vec(manifest).unwrap();
fs::write(release.join("manifest.json"), &bytes).unwrap();
fs::write(
release.join("manifest.sig"),
hex(self.key.sign(&bytes).as_ref()),
)
.unwrap();
}
}
#[test]
fn project_skill_preserves_user_bytes_permissions_and_updates_only_its_block() {
let fixture = Project::new();
let agents = fixture.root.join("AGENTS.md");
let before = "# 项目规则\r\n保留用户内容,无末尾换行";
fs::write(&agents, before).unwrap();
fs::set_permissions(&agents, fs::Permissions::from_mode(0o640)).unwrap();
fixture.succeeds();
assert_eq!(
fs::read_to_string(&agents).unwrap(),
format!("{before}\n\n{}", project_rules("1.0.0"))
);
assert_eq!(
fs::metadata(&agents).unwrap().permissions().mode() & 0o777,
0o640
);
let installed = fs::read(&agents).unwrap();
fixture.succeeds();
assert_eq!(fs::read(&agents).unwrap(), installed);
let prefix = "# 自定义前文\r\n\r\n";
let suffix = "\r\n# 自定义后文\r\n原样保留";
fs::write(
&agents,
format!(
"{prefix}<!-- dever-language:start -->\n旧规则\n<!-- dever-language:end -->{suffix}"
),
)
.unwrap();
fixture.succeeds();
assert_eq!(
fs::read_to_string(&agents).unwrap(),
format!("{prefix}{}{suffix}", project_rules("1.0.0").trim_end())
);
}
#[test]
fn project_skill_rejects_ambiguous_or_unsafe_agents_without_changes() {
let fixture = Project::new();
let agents = fixture.root.join("AGENTS.md");
for content in [
"<!-- dever-language:start -->",
"<!-- dever-language:end -->",
"<!-- dever-language:end --><!-- dever-language:start -->",
"<!-- dever-language:start --><!-- dever-language:start --><!-- dever-language:end -->",
"<!-- dever-language:start --><!-- dever-language:end --><!-- dever-language:start --><!-- dever-language:end -->",
] {
fs::write(&agents, content).unwrap();
fixture.fails("exactly one complete");
assert_eq!(fs::read_to_string(&agents).unwrap(), content);
}
fs::write(&agents, [0xff, 0xfe]).unwrap();
fixture.fails("UTF-8");
assert_eq!(fs::read(&agents).unwrap(), [0xff, 0xfe]);
fs::write(&agents, "do not overwrite").unwrap();
fs::set_permissions(&agents, fs::Permissions::from_mode(0o444)).unwrap();
fixture.fails("read-only");
fs::set_permissions(&agents, fs::Permissions::from_mode(0o644)).unwrap();
fs::remove_file(&agents).unwrap();
fs::create_dir(&agents).unwrap();
fixture.fails("regular file");
fs::remove_dir(&agents).unwrap();
let other = fixture.temporary.path().join("other-agents");
fs::write(&other, "untouched").unwrap();
symlink(&other, &agents).unwrap();
fixture.fails("regular file");
assert!(fs::symlink_metadata(&agents).unwrap().is_symlink());
assert_eq!(fs::read_to_string(&other).unwrap(), "untouched");
}
#[test]
fn project_skill_requires_project_root_valid_version_and_signed_template() {
let fixture = Project::new();
let agents = fixture.root.join("AGENTS.md");
let output = fixture.run(&fixture.root, &["unwanted-directory"]);
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stderr).contains("accepts no arguments"));
assert!(!fixture.root.join("unwanted-directory").exists());
let output = fixture.run(fixture.temporary.path(), &[]);
assert!(!output.status.success());
assert!(!fixture.temporary.path().join("AGENTS.md").exists());
fs::remove_dir(fixture.root.join("module")).unwrap();
symlink(fixture.temporary.path(), fixture.root.join("module")).unwrap();
fixture.fails("real module/");
fs::remove_file(fixture.root.join("module")).unwrap();
fs::create_dir(fixture.root.join("module")).unwrap();
let settings = fixture.root.join("config/setting.json");
for invalid in [
"not json",
"[]",
r#"{"dever":{"version":"latest"}}"#,
r#"{"dever":{"version":"9.0.0"}}"#,
] {
fs::write(&settings, invalid).unwrap();
let output = fixture.run(&fixture.root, &[]);
assert!(!output.status.success());
assert!(!agents.exists());
}
fs::remove_file(&settings).unwrap();
fixture.fails("cannot read");
fs::write(&settings, "{}").unwrap();
let template = fixture
.layout
.versions()
.join("1.0.0/skills/dever-language/assets/template/AGENTS.md");
fs::write(&template, "tampered instructions").unwrap();
let output = fixture.run(&fixture.root, &[]);
assert!(!output.status.success());
assert!(!agents.exists());
fixture.assert_no_staging();
}
#[test]
fn project_skill_refuses_unsigned_extra_or_invalid_signed_template() {
let fixture = Project::new();
let agents = fixture.root.join("AGENTS.md");
fs::write(&agents, "user-owned instructions").unwrap();
let release = fixture.layout.versions().join("1.0.0");
let mut manifest: ReleaseManifest =
serde_json::from_slice(&fs::read(release.join("manifest.json")).unwrap()).unwrap();
let logical = "skills/dever-language/assets/template/AGENTS.md";
manifest.artifacts.retain(|entry| entry.path != logical);
fixture.sign_manifest(&manifest);
fixture.fails("does not declare resource");
assert_eq!(
fs::read_to_string(&agents).unwrap(),
"user-owned instructions"
);
let template = release.join(logical);
for invalid in [
"no managed block",
"<!-- dever-language:start -->missing end",
"outside\n<!-- dever-language:start -->rules<!-- dever-language:end -->",
] {
fs::write(&template, invalid).unwrap();
manifest.artifacts.retain(|entry| entry.path != logical);
manifest.artifacts.push(artifact(&template, logical));
fixture.sign_manifest(&manifest);
let output = fixture.run(&fixture.root, &[]);
assert!(!output.status.success());
assert_eq!(
fs::read_to_string(&agents).unwrap(),
"user-owned instructions"
);
fixture.assert_no_staging();
}
}