use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn create_test_rust_crate(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
fs::write(
path.join("Cargo.toml"),
r#"[package]
name = "test-crate"
version = "0.1.0"
edition = "2024"
"#,
)?;
fs::create_dir(path.join(".git"))?;
let src_dir = path.join("src");
fs::create_dir(&src_dir)?;
fs::write(src_dir.join("lib.rs"), "// test lib\n")?;
Ok(())
}
fn create_test_config() -> rust_bucket::config::Config {
rust_bucket::config::Config {
rust_bucket_version: env!("CARGO_PKG_VERSION").to_string(),
test_timeout: 120,
project_name: "test-project".to_string(),
}
}
#[test]
fn test_init_on_fresh_repo() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
create_test_rust_crate(temp_dir.path())?;
assert!(temp_dir.path().join("Cargo.toml").exists());
assert!(temp_dir.path().join(".git").exists());
let conflicts = rust_bucket::generator::check_conflicts(temp_dir.path());
assert!(conflicts.is_empty(), "Fresh repo should have no conflicts");
let config = create_test_config();
let config_path = temp_dir.path().join("rust-bucket.toml");
config.save(&config_path)?;
let (_temp_template_dir, template_path) = rust_bucket::templates::extract_to_temp()?;
let files_generated =
rust_bucket::generator::render(&template_path, temp_dir.path(), &config, false)?;
let claude_symlink = rust_bucket::generator::create_claude_symlink(temp_dir.path())?;
let managed_files = rust_bucket::templates::managed_files();
assert_eq!(
files_generated.len() + 1,
managed_files.len(),
"Should generate all managed files (render + symlink)"
);
for file in managed_files {
let file_path = temp_dir.path().join(file);
assert!(
file_path.exists() || file_path.is_symlink(),
"Managed file should exist: {}",
file_path.display()
);
}
assert!(claude_symlink.is_symlink(), "CLAUDE.md should be a symlink");
let link_target = fs::read_link(&claude_symlink)?;
assert_eq!(
link_target
.to_str()
.ok_or("link target is not valid UTF-8")?,
"AGENTS.md",
"CLAUDE.md should point to AGENTS.md"
);
assert!(config_path.exists(), "rust-bucket.toml should exist");
let loaded_config = rust_bucket::config::Config::load(&config_path)?;
assert_eq!(loaded_config.test_timeout, 120);
Ok(())
}
#[test]
fn test_init_fails_on_conflict() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
create_test_rust_crate(temp_dir.path())?;
fs::write(temp_dir.path().join("AGENTS.md"), "existing content")?;
let conflicts = rust_bucket::generator::check_conflicts(temp_dir.path());
assert!(!conflicts.is_empty(), "Should detect conflict");
assert_eq!(conflicts.len(), 1, "Should detect exactly one conflict");
assert!(
conflicts[0].ends_with("AGENTS.md"),
"Conflict should be AGENTS.md"
);
let config = create_test_config();
let (_temp_template_dir, template_path) = rust_bucket::templates::extract_to_temp()?;
let result = rust_bucket::generator::render(&template_path, temp_dir.path(), &config, false);
assert!(result.is_err(), "Render should fail on conflict");
match result.unwrap_err() {
rust_bucket::generator::GeneratorError::ConflictError(conflict_list) => {
assert_eq!(conflict_list.len(), 1);
assert!(conflict_list[0].ends_with("AGENTS.md"));
}
e => panic!("Expected ConflictError, got: {:?}", e),
}
Ok(())
}
#[test]
fn test_init_force_overwrites() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
create_test_rust_crate(temp_dir.path())?;
let agents_path = temp_dir.path().join("AGENTS.md");
fs::write(&agents_path, "OLD CONTENT SHOULD BE REPLACED")?;
let old_content = fs::read_to_string(&agents_path)?;
assert_eq!(old_content, "OLD CONTENT SHOULD BE REPLACED");
let config = create_test_config();
let (_temp_template_dir, template_path) = rust_bucket::templates::extract_to_temp()?;
rust_bucket::generator::render(&template_path, temp_dir.path(), &config, true)?;
assert!(agents_path.exists(), "AGENTS.md should still exist");
let new_content = fs::read_to_string(&agents_path)?;
assert_ne!(
new_content, old_content,
"Content should have been overwritten"
);
assert!(
new_content.contains("Generated by rust-bucket"),
"New content should have version stamp"
);
assert!(
new_content.contains("Guide for Agents"),
"New content should have expected AGENTS.md content"
);
assert!(
!new_content.contains("OLD CONTENT"),
"Old content should be gone"
);
Ok(())
}
#[test]
fn test_update_preserves_config() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
create_test_rust_crate(temp_dir.path())?;
let mut config = create_test_config();
config.test_timeout = 300; let config_path = temp_dir.path().join("rust-bucket.toml");
config.save(&config_path)?;
let (_temp_template_dir1, template_path1) = rust_bucket::templates::extract_to_temp()?;
rust_bucket::generator::render(&template_path1, temp_dir.path(), &config, false)?;
let nextest_path = temp_dir.path().join(".config/nextest.toml");
let nextest_content = fs::read_to_string(&nextest_path)?;
assert!(
nextest_content.contains("300s"),
"Initial nextest.toml should have 300s timeout"
);
let mut loaded_config = rust_bucket::config::Config::load(&config_path)?;
assert_eq!(
loaded_config.test_timeout, 300,
"Loaded config should preserve custom timeout"
);
loaded_config.rust_bucket_version = "0.2.0".to_string();
loaded_config.save(&config_path)?;
let (_temp_template_dir2, template_path2) = rust_bucket::templates::extract_to_temp()?;
rust_bucket::generator::render(&template_path2, temp_dir.path(), &loaded_config, true)?;
let updated_nextest_content = fs::read_to_string(&nextest_path)?;
assert!(
updated_nextest_content.contains("300s"),
"Updated nextest.toml should still have 300s timeout"
);
let final_config = rust_bucket::config::Config::load(&config_path)?;
assert_eq!(final_config.rust_bucket_version, "0.2.0");
assert_eq!(
final_config.test_timeout, 300,
"Timeout should be preserved"
);
Ok(())
}
#[test]
fn test_version_stamp_in_generated_files() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
create_test_rust_crate(temp_dir.path())?;
let config = create_test_config();
let (_temp_template_dir, template_path) = rust_bucket::templates::extract_to_temp()?;
rust_bucket::generator::render(&template_path, temp_dir.path(), &config, false)?;
let files_to_check = vec![
("AGENTS.md", "<!-- Generated by rust-bucket", true),
("RUST_STYLE_GUIDE.md", "<!-- Generated by rust-bucket", true),
("TESTING.md", "<!-- Generated by rust-bucket", true),
(
".claude/agents/coordinator.md",
"<!-- Generated by rust-bucket",
true,
),
(".config/nextest.toml", "# Generated by rust-bucket", true),
("deny.toml", "# Generated by rust-bucket", true),
("rustfmt.toml", "# Generated by rust-bucket", true),
(
".devcontainer/Dockerfile",
"# Generated by rust-bucket",
true,
),
(
".devcontainer/devcontainer.json",
"rust-bucket v",
false, ),
(".beads/config.yaml", "# Generated by rust-bucket", true),
];
for (file_path, expected_stamp_text, should_have_do_not_edit) in files_to_check {
let full_path = temp_dir.path().join(file_path);
assert!(full_path.exists(), "File should exist: {}", file_path);
let content = fs::read_to_string(&full_path)?;
assert!(
content.contains(expected_stamp_text),
"File {} should contain version stamp '{}', but content is:\n{}",
file_path,
expected_stamp_text,
content.lines().take(5).collect::<Vec<_>>().join("\n")
);
assert!(
content.contains(&format!("v{}", env!("CARGO_PKG_VERSION"))),
"File {} should contain version number v{}",
file_path,
env!("CARGO_PKG_VERSION")
);
if should_have_do_not_edit {
assert!(
content.contains("DO NOT EDIT"),
"File {} should contain DO NOT EDIT warning",
file_path
);
}
}
Ok(())
}