Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,181 @@
|
||||
use super::Directory;
|
||||
use dever::compiler::{project::Project, toolchain::BuildTarget};
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Output};
|
||||
|
||||
mod packaging;
|
||||
mod skill;
|
||||
|
||||
fn cli(arguments: &[&str]) -> Output {
|
||||
Command::new(env!("CARGO_BIN_EXE_dever"))
|
||||
.args(arguments)
|
||||
.output()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn success(output: Output) -> String {
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
String::from_utf8(output.stdout).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_creates_plain_source_and_separate_docs_without_overwriting() {
|
||||
let directory = Directory::new();
|
||||
for name in ["plain", "second"] {
|
||||
let root = directory.0.join(name);
|
||||
let text = root.to_str().unwrap();
|
||||
let arguments = vec!["new", text];
|
||||
success(cli(&arguments));
|
||||
let suffix = "dever";
|
||||
assert!(
|
||||
root.join(format!("module/hello/greeting/app.{suffix}"))
|
||||
.is_file()
|
||||
);
|
||||
assert!(
|
||||
root.join(format!("test/hello/greeting/greet.{suffix}"))
|
||||
.is_file()
|
||||
);
|
||||
assert!(root.join("module/hello/greeting/README.md").is_file());
|
||||
assert!(root.join("module/hello/greeting/doc/greeting.md").is_file());
|
||||
let project = Project::load(&root).unwrap();
|
||||
assert_eq!(project.check().unwrap().commands.len(), 1);
|
||||
success(cli(&["check", text]));
|
||||
success(cli(&["fmt", text, "--check"]));
|
||||
let snapshot = std::fs::read(root.join("README.md")).unwrap();
|
||||
assert!(!cli(&arguments).status.success());
|
||||
assert_eq!(std::fs::read(root.join("README.md")).unwrap(), snapshot);
|
||||
}
|
||||
let path = directory.0.join("invalid");
|
||||
assert!(
|
||||
!cli(&["new", path.to_str().unwrap(), "--markdown"])
|
||||
.status
|
||||
.success()
|
||||
);
|
||||
assert!(!path.exists());
|
||||
let link = directory.0.join("link");
|
||||
std::os::unix::fs::symlink(directory.0.join("plain"), &link).unwrap();
|
||||
let rejected = cli(&["new", link.to_str().unwrap()]);
|
||||
assert!(!rejected.status.success());
|
||||
assert!(String::from_utf8_lossy(&rejected.stderr).contains(link.to_str().unwrap()));
|
||||
assert_eq!(
|
||||
std::fs::read_link(&link).unwrap(),
|
||||
directory.0.join("plain")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn template_runs_tests_and_builds_a_real_native_executable_for_the_selected_target() {
|
||||
let directory = Directory::new();
|
||||
let root = directory.0.join("app");
|
||||
dever::compiler::project::create(&root).unwrap();
|
||||
let root = root.to_str().unwrap();
|
||||
success(cli(&["test", root]));
|
||||
let result = success(cli(&[
|
||||
"run",
|
||||
root,
|
||||
"--",
|
||||
"hello.greeting.greet",
|
||||
r#"{"name":"Dever"}"#,
|
||||
]));
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&result).unwrap()["data"],
|
||||
"Hello, Dever"
|
||||
);
|
||||
let binary = directory.0.join("hello");
|
||||
success(cli(&[
|
||||
"build",
|
||||
root,
|
||||
"--output",
|
||||
binary.to_str().unwrap(),
|
||||
"--target",
|
||||
BuildTarget::host().unwrap().name(),
|
||||
]));
|
||||
BuildTarget::host()
|
||||
.unwrap()
|
||||
.validate_executable(&std::fs::read(&binary).unwrap())
|
||||
.unwrap();
|
||||
let result = success(
|
||||
Command::new(&binary)
|
||||
.args(["hello.greeting.greet", r#"{"name":"native"}"#])
|
||||
.output()
|
||||
.unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&result).unwrap()["data"],
|
||||
"Hello, native"
|
||||
);
|
||||
let before = std::fs::read(&binary).unwrap();
|
||||
assert!(
|
||||
!cli(&["build", root, "--output", binary.to_str().unwrap()])
|
||||
.status
|
||||
.success()
|
||||
);
|
||||
assert_eq!(std::fs::read(&binary).unwrap(), before);
|
||||
assert!(
|
||||
!cli(&["run", root, "--target", "linux-aarch64"])
|
||||
.status
|
||||
.success()
|
||||
);
|
||||
assert!(
|
||||
!cli(&[
|
||||
"build",
|
||||
root,
|
||||
"--output",
|
||||
binary.to_str().unwrap(),
|
||||
"--target",
|
||||
"unsupported"
|
||||
])
|
||||
.status
|
||||
.success()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elf_validation_checks_architecture_and_executable_type() {
|
||||
let mut header = vec![0_u8; 64];
|
||||
header[..7].copy_from_slice(b"\x7fELF\x02\x01\x01");
|
||||
header[16..18].copy_from_slice(&2_u16.to_le_bytes());
|
||||
header[18..20].copy_from_slice(&183_u16.to_le_bytes());
|
||||
assert!(
|
||||
BuildTarget::LinuxAarch64
|
||||
.validate_executable(&header)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
BuildTarget::LinuxX86_64
|
||||
.validate_executable(&header)
|
||||
.is_err()
|
||||
);
|
||||
header[16..18].copy_from_slice(&1_u16.to_le_bytes());
|
||||
assert!(
|
||||
BuildTarget::LinuxAarch64
|
||||
.validate_executable(&header)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
BuildTarget::LinuxAarch64
|
||||
.validate_executable(b"short")
|
||||
.is_err()
|
||||
);
|
||||
assert!(BuildTarget::parse("linux-x86_64").is_ok());
|
||||
assert!(BuildTarget::parse("esp32").is_err());
|
||||
}
|
||||
|
||||
fn copy_tree(source: &Path, destination: &Path) {
|
||||
if source.is_dir() {
|
||||
std::fs::create_dir_all(destination).unwrap();
|
||||
for entry in std::fs::read_dir(source)
|
||||
.unwrap_or_else(|error| panic!("{}: {error}", source.display()))
|
||||
{
|
||||
let entry = entry.unwrap();
|
||||
copy_tree(&entry.path(), &destination.join(entry.file_name()));
|
||||
}
|
||||
} else {
|
||||
std::fs::create_dir_all(destination.parent().unwrap()).unwrap();
|
||||
std::fs::copy(source, destination).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
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::<serde_json::Value>(&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());
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
//! 通过真实 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}<!-- dever-language:start -->\r\n旧规则\r\n<!-- dever-language:end -->{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 = "<!-- dever-language:start -->";
|
||||
let end = "<!-- dever-language: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}"),
|
||||
"<!-- dever-language:unknown -->".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-")
|
||||
}));
|
||||
}
|
||||
Reference in new issue
Block a user