253 lines
8.8 KiB
Rust
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();
|
|
}
|
|
}
|