use std::fs;
use std::path::Path;
use tempfile::TempDir;
fn create_test_rust_crate(path: &Path) {
fs::write(
path.join("Cargo.toml"),
r#"[package]
name = "test-crate"
version = "0.1.0"
edition = "2024"
"#,
)
.unwrap();
fs::create_dir(path.join(".git")).unwrap();
let src_dir = path.join("src");
fs::create_dir(&src_dir).unwrap();
fs::write(src_dir.join("lib.rs"), "// test lib\n").unwrap();
}
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() {
let temp_dir = TempDir::new().unwrap();
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).unwrap();
let (_temp_template_dir, template_path) = rust_bucket::templates::extract_to_temp().unwrap();
let files_generated =
rust_bucket::generator::render(&template_path, temp_dir.path(), &config, false).unwrap();
let claude_symlink = rust_bucket::generator::create_claude_symlink(temp_dir.path()).unwrap();
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).unwrap();
assert_eq!(
link_target.to_str().unwrap(),
"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).unwrap();
assert_eq!(loaded_config.test_timeout, 120);
}
#[test]
fn test_init_fails_on_conflict() {
let temp_dir = TempDir::new().unwrap();
create_test_rust_crate(temp_dir.path());
fs::write(temp_dir.path().join("AGENTS.md"), "existing content").unwrap();
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().unwrap();
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),
}
}
#[test]
fn test_init_force_overwrites() {
let temp_dir = TempDir::new().unwrap();
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").unwrap();
let old_content = fs::read_to_string(&agents_path).unwrap();
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().unwrap();
let result = rust_bucket::generator::render(&template_path, temp_dir.path(), &config, true);
assert!(result.is_ok(), "Render with force should succeed");
assert!(agents_path.exists(), "AGENTS.md should still exist");
let new_content = fs::read_to_string(&agents_path).unwrap();
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"
);
}
#[test]
fn test_update_preserves_config() {
let temp_dir = TempDir::new().unwrap();
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).unwrap();
let (_temp_template_dir1, template_path1) = rust_bucket::templates::extract_to_temp().unwrap();
rust_bucket::generator::render(&template_path1, temp_dir.path(), &config, false).unwrap();
let nextest_path = temp_dir.path().join(".config/nextest.toml");
let nextest_content = fs::read_to_string(&nextest_path).unwrap();
assert!(
nextest_content.contains("300s"),
"Initial nextest.toml should have 300s timeout"
);
let mut loaded_config = rust_bucket::config::Config::load(&config_path).unwrap();
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).unwrap();
let (_temp_template_dir2, template_path2) = rust_bucket::templates::extract_to_temp().unwrap();
rust_bucket::generator::render(&template_path2, temp_dir.path(), &loaded_config, true).unwrap();
let updated_nextest_content = fs::read_to_string(&nextest_path).unwrap();
assert!(
updated_nextest_content.contains("300s"),
"Updated nextest.toml should still have 300s timeout"
);
let final_config = rust_bucket::config::Config::load(&config_path).unwrap();
assert_eq!(final_config.rust_bucket_version, "0.2.0");
assert_eq!(
final_config.test_timeout, 300,
"Timeout should be preserved"
);
}
#[test]
fn test_version_stamp_in_generated_files() {
let temp_dir = TempDir::new().unwrap();
create_test_rust_crate(temp_dir.path());
let config = create_test_config();
let (_temp_template_dir, template_path) = rust_bucket::templates::extract_to_temp().unwrap();
rust_bucket::generator::render(&template_path, temp_dir.path(), &config, false).unwrap();
let files_to_check = vec![
("AGENTS.md", "<!-- Generated by rust-bucket", true),
("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).unwrap();
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
);
}
}
}