use anyhow::Result;
use std::path::{Path, PathBuf};
use tokio::fs;
mod common;
mod fixtures;
use common::TestProject;
use fixtures::MarkdownFixture;
trait MarkdownFixtureExt {
async fn write_to(&self, dir: &Path) -> Result<PathBuf>;
}
impl MarkdownFixtureExt for MarkdownFixture {
async fn write_to(&self, dir: &Path) -> Result<PathBuf> {
let file_path = dir.join(&self.path);
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent).await?;
}
fs::write(&file_path, &self.content).await?;
Ok(file_path)
}
}
#[tokio::test]
async fn test_path_separators() {
let project = TestProject::new().await.unwrap();
let official_repo = project.create_source_repo("official").await.unwrap();
official_repo
.add_resource("agents", "windows-agent", "# Windows Agent\n\nA test agent")
.await
.unwrap();
official_repo
.add_resource("agents", "unix-agent", "# Unix Agent\n\nA test agent")
.await
.unwrap();
official_repo.commit_all("Initial commit").unwrap();
official_repo.tag_version("v1.0.0").unwrap();
let source_path_str = official_repo.bare_file_url(project.sources_path()).unwrap();
let manifest_content = if cfg!(windows) {
format!(
r#"
[sources]
official = "{source_path_str}"
[agents]
windows-agent = {{ source = "official", path = "agents\\windows-agent.md", version = "v1.0.0" }}
unix-agent = {{ source = "official", path = "agents/unix-agent.md", version = "v1.0.0" }}
[snippets]
local-snippet = {{ path = ".\\snippets\\local.md" }}
"#
)
} else {
format!(
r#"
[sources]
official = "{source_path_str}"
[agents]
unix-agent = {{ source = "official", path = "agents/unix-agent.md", version = "v1.0.0" }}
windows-agent = {{ source = "official", path = "agents\\windows-agent.md", version = "v1.0.0" }}
[snippets]
local-snippet = {{ path = "./snippets/local.md" }}
"#
)
};
project.write_manifest(&manifest_content).await.unwrap();
let snippets_dir = project.project_path().join("snippets");
fs::create_dir_all(&snippets_dir).await.unwrap();
let local_snippet = MarkdownFixture::snippet("local");
local_snippet.write_to(project.project_path()).await.unwrap();
let output = project.run_agpm(&["validate"]).unwrap();
assert!(output.success);
assert!(output.stdout.contains("✓"));
}
#[cfg(windows)]
#[tokio::test]
async fn test_long_paths_windows() {
let project = TestProject::new().await.unwrap();
let long_name = "a".repeat(100);
let official_repo = project.create_source_repo("official").await.unwrap();
official_repo
.add_resource("agents", &long_name, &format!("# {}\n\nA test agent", long_name))
.await
.unwrap();
official_repo.commit_all("Initial commit").unwrap();
official_repo.tag_version("v1.0.0").unwrap();
let source_path_str = official_repo.bare_file_url(project.sources_path()).unwrap();
let manifest_content = format!(
r#"
[sources]
official = "{}"
[agents]
{} = {{ source = "official", path = "agents/{}.md", version = "v1.0.0" }}
"#,
source_path_str, long_name, long_name
);
project.write_manifest(&manifest_content).await.unwrap();
let output = project.run_agpm(&["install", "--no-cache"]).unwrap();
assert!(output.success); assert!(output.stdout.contains("Installed") || output.stdout.contains("Installing"));
}
#[tokio::test]
async fn test_case_conflict_detection() {
let project = TestProject::new().await.unwrap();
let official_repo = project.create_source_repo("official").await.unwrap();
official_repo
.add_resource("agents", "myagent-lower", "# MyAgent Lower\n\nA test agent")
.await
.unwrap();
official_repo
.add_resource("agents", "MyAgent-upper", "# MyAgent Upper\n\nA test agent")
.await
.unwrap();
official_repo.commit_all("Initial commit").unwrap();
official_repo.tag_version("v1.0.0").unwrap();
let source_path_str = official_repo.bare_file_url(project.sources_path()).unwrap();
let manifest_content = format!(
r#"
[sources]
official = "{source_path_str}"
[agents]
myagent = {{ source = "official", path = "agents/myagent-lower.md", version = "v1.0.0" }}
MyAgent = {{ source = "official", path = "agents/MyAgent-upper.md", version = "v1.0.0" }}
"#
);
project.write_manifest(&manifest_content).await.unwrap();
let manifest_path = project.project_path().join("agpm.toml");
let output = project
.run_agpm(&["--manifest-path", manifest_path.to_str().unwrap(), "validate"])
.unwrap();
assert!(!output.success);
assert!(output.stderr.contains("Case conflict"));
assert!(output.stderr.contains("myagent"));
assert!(output.stderr.contains("MyAgent"));
}
#[tokio::test]
async fn test_home_directory_expansion() {
let project = TestProject::new().await.unwrap();
let manifest_content = if cfg!(windows) {
r#"
[sources]
local = "~/agpm-sources/local.git"
[agents]
home-agent = { source = "local", path = "agents/home-agent.md", version = "v1.0.0" }
[snippets]
home-snippet = { path = "~\\Documents\\snippets\\home.md" }
"#
} else {
r#"
[sources]
local = "~/agpm-sources/local.git"
[agents]
home-agent = { source = "local", path = "agents/home-agent.md", version = "v1.0.0" }
[snippets]
home-snippet = { path = "~/Documents/snippets/home.md" }
"#
};
project.write_manifest(manifest_content).await.unwrap();
let output = project.run_agpm(&["validate"]).unwrap();
assert!(output.success); assert!(output.stdout.contains("✓")); }
#[tokio::test]
async fn test_line_endings() {
let project = TestProject::new().await.unwrap();
let manifest_content = if cfg!(windows) {
"[sources]\r\nofficial = \"https://github.com/example-org/agpm-official.git\"\r\n\r\n[agents]\r\ntest-agent = { source = \"official\", path = \"agents/test.md\", version = \"v1.0.0\" }\r\n"
} else {
"[sources]\nofficial = \"https://github.com/example-org/agpm-official.git\"\n\n[agents]\ntest-agent = { source = \"official\", path = \"agents/test.md\", version = \"v1.0.0\" }\n"
};
project.write_manifest(manifest_content).await.unwrap();
let output = project.run_agpm(&["validate"]).unwrap();
assert!(output.success);
assert!(output.stdout.contains("✓"));
}
#[tokio::test]
async fn test_git_command_platform() {
let project = TestProject::new().await.unwrap();
let official_repo = project.create_source_repo("official").await.unwrap();
official_repo
.add_resource("agents", "test-agent", "# Test Agent\n\nA test agent")
.await
.unwrap();
official_repo.commit_all("Initial commit").unwrap();
official_repo.tag_version("v1.0.0").unwrap();
let source_path_str = official_repo.bare_file_url(project.sources_path()).unwrap();
let manifest_content = format!(
r#"
[sources]
official = "{source_path_str}"
[agents]
test-agent = {{ source = "official", path = "agents/test-agent.md", version = "v1.0.0" }}
"#
);
project.write_manifest(&manifest_content).await.unwrap();
let output = project.run_agpm(&["install", "--no-cache"]).unwrap();
assert!(
output.stdout.contains("Installing")
|| output.stdout.contains("Cloning")
|| output.stdout.contains("Installed"),
"Expected 'Installing', 'Cloning', or 'Installed' in stdout: {}",
output.stdout
);
if !output.success {
assert!(
output.stderr.contains("Git operation failed")
|| output.stderr.contains("not a git repository")
|| output.stderr.contains("worktree add"),
"Unexpected failure: {}",
output.stderr
);
}
}
#[cfg(unix)]
#[tokio::test]
async fn test_unix_permissions() {
use std::os::unix::fs::PermissionsExt;
let project = TestProject::new().await.unwrap();
let test_repo = project.create_source_repo("test-source").await.unwrap();
test_repo
.add_resource("snippets", "example", "# Example Snippet\n\nA test snippet")
.await
.unwrap();
test_repo.commit_all("Initial commit").unwrap();
test_repo.tag_version("v1.0.0").unwrap();
let source_url = test_repo.bare_file_url(project.sources_path()).unwrap();
let manifest_content = format!(
r#"
[sources]
test = "{source_url}"
[snippets]
remote-snippet = {{ source = "test", path = "snippets/example.md", version = "v1.0.0" }}
"#
);
project.write_manifest(&manifest_content).await.unwrap();
let parent_dir = project.project_path().join("restricted_parent");
fs::create_dir_all(&parent_dir).await.unwrap();
let restricted_cache = parent_dir.join("cache");
let mut perms = fs::metadata(&parent_dir).await.unwrap().permissions();
perms.set_mode(0o555); fs::set_permissions(&parent_dir, perms).await.unwrap();
let output = project
.run_agpm_with_env(&["install"], &[("AGPM_CACHE_DIR", restricted_cache.to_str().unwrap())])
.unwrap();
assert!(!output.success);
assert!(
output.stderr.contains("Permission denied")
|| output.stderr.contains("Access denied")
|| output.stderr.contains("Failed to create"),
"Expected permission error, got: {}",
output.stderr
);
let mut perms = fs::metadata(&parent_dir).await.unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&parent_dir, perms).await.unwrap();
}
#[cfg(windows)]
#[tokio::test]
async fn test_windows_drive_letters() {
let project = TestProject::new().await.unwrap();
let manifest_content = format!(
r#"
[sources]
official = "https://github.com/example-org/agpm-official.git"
[snippets]
absolute-snippet = {{ path = "C:\\temp\\snippet.md" }}
unc-snippet = {{ path = "\\\\server\\share\\snippet.md" }}
relative-snippet = {{ path = "{}\\snippets\\relative.md" }}
"#,
project.project_path().to_str().unwrap().replace("\\", "\\\\")
);
project.write_manifest(&manifest_content).await.unwrap();
let snippets_dir = project.project_path().join("snippets");
fs::create_dir_all(&snippets_dir).await.unwrap();
fs::write(snippets_dir.join("relative.md"), "# Relative snippet").await.unwrap();
let output = project.run_agpm(&["validate", "--resolve"]).unwrap();
assert!(!output.success); assert!(
output.stderr.contains("Local dependency 'absolute-snippet' not found at")
|| output.stderr.contains("Local dependency 'unc-snippet' not found at"),
"Expected dependency not found error, got: {}",
output.stderr
);
assert!(
output.stderr.contains("C:\\temp\\snippet.md")
|| output.stderr.contains("\\\\server\\share\\snippet.md"),
"Expected path in error message, got: {}",
output.stderr
);
}
#[tokio::test]
async fn test_concurrent_operations() {
let project = TestProject::new().await.unwrap();
let official_repo = project.create_source_repo("official").await.unwrap();
official_repo
.add_resource("agents", "concurrent-agent", "# Concurrent Agent\n\nA test agent")
.await
.unwrap();
official_repo.commit_all("Initial commit").unwrap();
official_repo.tag_version("v1.0.0").unwrap();
let source_url = official_repo.bare_file_url(project.sources_path()).unwrap();
let manifest_content = format!(
r#"
[sources]
official = "{source_url}"
[agents]
concurrent-agent = {{ source = "official", path = "agents/concurrent-agent.md", version = "v1.0.0" }}
"#
);
project.write_manifest(&manifest_content).await.unwrap();
let output = project.run_agpm(&["validate"]).unwrap();
assert!(output.success);
assert!(output.stdout.contains("✓"));
let mut handles = vec![];
for i in 0..3 {
let project_path = project.project_path().to_path_buf();
let cache_path = project.cache_path().to_path_buf();
let handle = std::thread::spawn(move || {
let mut cmd = assert_cmd::Command::cargo_bin("agpm").unwrap();
cmd.current_dir(&project_path)
.env("AGPM_CACHE_DIR", &cache_path)
.arg("validate")
.env("AGPM_PARALLEL_ID", i.to_string())
.assert()
.success();
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
let output = project.run_agpm(&["validate"]).unwrap();
assert!(output.success);
assert!(output.stdout.contains("✓"));
}
#[tokio::test]
async fn test_unicode_filenames() {
let project = TestProject::new().await.unwrap();
let manifest_content = r#"
[sources]
"官方" = "https://github.com/example-org/官方代理.git"
[agents]
"日本語エージェント" = { source = "官方", path = "agents/日本語.md", version = "v1.0.0" }
"émoji-agent" = { source = "官方", path = "agents/🚀emoji.md", version = "v1.0.0" }
"#;
project.write_manifest(manifest_content).await.unwrap();
let output = project.run_agpm(&["validate"]).unwrap();
assert!(output.success); assert!(output.stdout.contains("✓"));
}
#[cfg(unix)]
#[tokio::test]
async fn test_symlink_handling() {
let project = TestProject::new().await.unwrap();
let target_dir = project.cache_path().join("target");
let link_dir = project.cache_path().join("link");
fs::create_dir_all(&target_dir).await.unwrap();
std::os::unix::fs::symlink(&target_dir, &link_dir).unwrap();
let manifest_content = format!(
r#"
[snippets]
symlink-snippet = {{ path = "{}/snippet.md" }}
"#,
link_dir.to_str().unwrap()
);
project.write_manifest(&manifest_content).await.unwrap();
fs::write(target_dir.join("snippet.md"), "# Symlinked snippet").await.unwrap();
let output = project.run_agpm(&["validate", "--resolve"]).unwrap();
assert!(output.success); assert!(output.stdout.contains("✓"));
}
#[tokio::test]
async fn test_shell_compatibility() {
let project = TestProject::new().await.unwrap();
let manifest_content = r#"
[sources]
official = "https://github.com/example-org/agpm-official.git"
[agents]
my-agent = { source = "official", path = "agents/my-agent.md", version = "v1.0.0" }
"#;
project.write_manifest(manifest_content).await.unwrap();
let output = project.run_agpm(&["--help"]).unwrap();
assert!(output.success);
assert!(output.stdout.contains("AGPM is a Git-based package manager"));
}
#[tokio::test]
async fn test_temp_directory_platform() {
let project = TestProject::new().await.unwrap();
let official_repo = project.create_source_repo("official").await.unwrap();
official_repo
.add_resource("agents", "temp-test-agent", "# Temp Test Agent\n\nA test agent")
.await
.unwrap();
official_repo.commit_all("Initial commit").unwrap();
official_repo.tag_version("v1.0.0").unwrap();
let source_url = official_repo.bare_file_url(project.sources_path()).unwrap();
let manifest_content = format!(
r#"
[sources]
official = "{source_url}"
[agents]
temp-test-agent = {{ source = "official", path = "agents/temp-test-agent.md", version = "v1.0.0" }}
"#
);
project.write_manifest(&manifest_content).await.unwrap();
let output = project.run_agpm(&["validate"]).unwrap();
assert!(output.success);
assert!(output.stdout.contains("✓"));
}