use std::fs;
use lorum::adapters::{all_rules_adapters, find_rules_adapter};
use lorum::rules::{self, RulesFile, RulesSection};
use lorum::sync;
#[test]
fn test_rule_full_workflow() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let init_rules = RulesFile {
preamble: "# Project Rules\n\nThis file defines AI coding rules managed by lorum.\n\
Each `##` heading defines a rule section that can be synced to target tools."
.to_owned(),
sections: vec![RulesSection {
name: "Code Style".to_owned(),
content: "Add your code style rules here.".to_owned(),
}],
};
rules::save_rules(root, &init_rules).unwrap();
assert!(root.join(".lorum/RULES.md").exists());
let mut rules = rules::load_rules(root).unwrap();
rules.sections.push(RulesSection {
name: "Testing".to_owned(),
content: "Run cargo test".to_owned(),
});
rules::save_rules(root, &rules).unwrap();
let mut rules = rules::load_rules(root).unwrap();
rules.sections.push(RulesSection {
name: "Style".to_owned(),
content: "Use 4 spaces".to_owned(),
});
rules::save_rules(root, &rules).unwrap();
let rules = rules::load_rules(root).unwrap();
assert_eq!(rules.sections.len(), 3);
let content = rules::render_rules(&rules);
let results = sync::sync_rules_all(root, &content);
assert_eq!(results.len(), 7); for r in &results {
assert!(r.success, "sync failed for {}: {:?}", r.tool, r.error);
}
let cursor_path = root.join(".cursorrules");
assert!(cursor_path.exists(), ".cursorrules should exist");
assert_eq!(fs::read_to_string(&cursor_path).unwrap(), content);
let windsurf_path = root.join(".windsurfrules");
assert!(windsurf_path.exists(), ".windsurfrules should exist");
assert_eq!(fs::read_to_string(&windsurf_path).unwrap(), content);
let codex_path = root.join(".codex").join("rules.md");
assert!(codex_path.exists(), ".codex/rules.md should exist");
assert_eq!(fs::read_to_string(&codex_path).unwrap(), content);
}
#[test]
fn test_rule_sync_dry_run() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let init_rules = RulesFile {
preamble: "# Project Rules".to_owned(),
sections: vec![RulesSection {
name: "Style".to_owned(),
content: "Use tabs".to_owned(),
}],
};
rules::save_rules(root, &init_rules).unwrap();
let content = rules::render_rules(&init_rules);
let results = sync::dry_run_rules_all(root, &content);
assert_eq!(results.len(), 7); for r in &results {
assert!(
r.success,
"dry_run read failed for {}: {:?}",
r.tool, r.error
);
assert!(r.needs_update, "{} should need update", r.tool);
}
assert!(!root.join(".cursorrules").exists());
assert!(!root.join(".windsurfrules").exists());
assert!(!root.join(".codex").join("rules.md").exists());
assert!(!root.join("CLAUDE.md").exists());
assert!(!root.join("AGENTS.md").exists());
assert!(!root.join(".trae").exists());
}
#[test]
fn test_rule_sync_filtered_tools() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let init_rules = RulesFile {
preamble: "# Project Rules".to_owned(),
sections: vec![RulesSection {
name: "Testing".to_owned(),
content: "Run cargo test".to_owned(),
}],
};
rules::save_rules(root, &init_rules).unwrap();
let content = rules::render_rules(&init_rules);
let tools: Vec<String> = vec!["cursor".to_owned()];
let results = sync::sync_rules_tools(root, &content, &tools);
assert_eq!(results.len(), 1);
assert!(results[0].success);
assert_eq!(results[0].tool, "cursor");
assert!(
root.join(".cursorrules").exists(),
".cursorrules should exist"
);
assert!(
!root.join(".windsurfrules").exists(),
".windsurfrules should not exist"
);
assert!(
!root.join(".codex").join("rules.md").exists(),
".codex/rules.md should not exist"
);
}
#[test]
fn test_rule_import_from_cursor() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let original_content = "Always use meaningful variable names.\nPrefer iterators over loops.";
let cursor_path = root.join(".cursorrules");
fs::write(&cursor_path, original_content).unwrap();
let adapter = find_rules_adapter("cursor").expect("cursor adapter should exist");
let imported_content = adapter
.read_rules(root)
.unwrap()
.expect("should read .cursorrules");
let section_name = "Imported from cursor";
let imported_rules = RulesFile {
preamble: "# Project Rules\n\nThis file defines AI coding rules managed by lorum.\n\
Each `##` heading defines a rule section that can be synced to target tools."
.to_owned(),
sections: vec![RulesSection {
name: section_name.to_owned(),
content: imported_content.clone(),
}],
};
rules::save_rules(root, &imported_rules).unwrap();
assert!(root.join(".lorum/RULES.md").exists());
let loaded = rules::load_rules(root).unwrap();
let section = loaded
.section(section_name)
.expect("imported section should exist");
assert_eq!(section.content, original_content);
}
#[test]
fn test_rule_import_into_existing() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let existing = RulesFile {
preamble: "# Project Rules".to_owned(),
sections: vec![RulesSection {
name: "Existing Section".to_owned(),
content: "Do not modify.".to_owned(),
}],
};
rules::save_rules(root, &existing).unwrap();
let windsurf_content = "Use TypeScript strict mode.\nNo any types.";
let windsurf_path = root.join(".windsurfrules");
fs::write(&windsurf_path, windsurf_content).unwrap();
let adapter = find_rules_adapter("windsurf").expect("windsurf adapter should exist");
let imported = adapter
.read_rules(root)
.unwrap()
.expect("should read .windsurfrules");
let section_name = "Imported from windsurf";
let mut rules = rules::load_rules(root).unwrap();
rules.sections.retain(|s| s.name != section_name);
rules.sections.push(RulesSection {
name: section_name.to_owned(),
content: imported,
});
rules::save_rules(root, &rules).unwrap();
let loaded = rules::load_rules(root).unwrap();
assert_eq!(loaded.sections.len(), 2);
assert_eq!(
loaded.section("Existing Section").unwrap().content,
"Do not modify."
);
let imported_section = loaded
.section(section_name)
.expect("imported section should exist");
assert_eq!(imported_section.content, windsurf_content);
}
#[test]
fn test_rule_sync_creates_backup() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let old_content = "Old cursor rules content.";
let cursor_path = root.join(".cursorrules");
fs::write(&cursor_path, old_content).unwrap();
let rules = RulesFile {
preamble: "# Project Rules".to_owned(),
sections: vec![RulesSection {
name: "Style".to_owned(),
content: "Use 4 spaces.".to_owned(),
}],
};
rules::save_rules(root, &rules).unwrap();
let content = rules::render_rules(&rules);
let results = sync::sync_rules_all(root, &content);
for r in &results {
assert!(r.success, "sync failed for {}: {:?}", r.tool, r.error);
}
let backups = lorum::backup::list_backups("cursor").unwrap();
assert!(
!backups.is_empty(),
"at least one backup should exist for cursor"
);
let latest_backup = &backups[0];
let backup_content = fs::read_to_string(&latest_backup.path).unwrap();
assert_eq!(backup_content, old_content);
let new_content = fs::read_to_string(&cursor_path).unwrap();
assert_eq!(new_content, content);
assert_ne!(new_content, old_content);
}
#[test]
fn test_rule_crud_sequence() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let mut rules = RulesFile {
preamble: "# Project Rules".to_owned(),
sections: vec![],
};
rules::save_rules(root, &rules).unwrap();
assert!(root.join(".lorum/RULES.md").exists());
rules.sections.push(RulesSection {
name: "A".to_owned(),
content: "content A".to_owned(),
});
rules::save_rules(root, &rules).unwrap();
let loaded = rules::load_rules(root).unwrap();
assert_eq!(loaded.sections.len(), 1);
assert_eq!(loaded.sections[0].name, "A");
assert_eq!(loaded.sections[0].content, "content A");
rules.sections.push(RulesSection {
name: "B".to_owned(),
content: "content B".to_owned(),
});
rules::save_rules(root, &rules).unwrap();
let loaded = rules::load_rules(root).unwrap();
assert_eq!(loaded.sections.len(), 2);
assert_eq!(loaded.sections[0].name, "A");
assert_eq!(loaded.sections[1].name, "B");
let mut loaded = rules::load_rules(root).unwrap();
let section_a = loaded
.sections
.iter_mut()
.find(|s| s.name == "A")
.expect("section A should exist");
section_a.content = "updated A".to_owned();
rules::save_rules(root, &loaded).unwrap();
let loaded = rules::load_rules(root).unwrap();
assert_eq!(loaded.sections.len(), 2);
assert_eq!(loaded.sections[0].content, "updated A");
assert_eq!(loaded.sections[1].name, "B");
assert_eq!(loaded.sections[1].content, "content B");
let mut loaded = rules::load_rules(root).unwrap();
loaded.sections.retain(|s| s.name != "B");
rules::save_rules(root, &loaded).unwrap();
let loaded = rules::load_rules(root).unwrap();
assert_eq!(loaded.sections.len(), 1);
assert_eq!(loaded.sections[0].name, "A");
assert_eq!(loaded.sections[0].content, "updated A");
let names: Vec<&str> = loaded.sections.iter().map(|s| s.name.as_str()).collect();
assert_eq!(names, vec!["A"]);
}
#[test]
fn test_all_rules_adapters_registered() {
let adapters = all_rules_adapters();
assert_eq!(adapters.len(), 7);
let names: Vec<&str> = adapters.iter().map(|a| a.name()).collect();
assert!(names.contains(&"claude-code"));
assert!(names.contains(&"cursor"));
assert!(names.contains(&"windsurf"));
assert!(names.contains(&"codex"));
assert!(names.contains(&"kimi"));
assert!(names.contains(&"opencode"));
assert!(names.contains(&"trae"));
}
#[test]
fn test_find_rules_adapter_by_name() {
assert!(find_rules_adapter("claude-code").is_some());
assert!(find_rules_adapter("cursor").is_some());
assert!(find_rules_adapter("windsurf").is_some());
assert!(find_rules_adapter("codex").is_some());
assert!(find_rules_adapter("kimi").is_some());
assert!(find_rules_adapter("opencode").is_some());
assert!(find_rules_adapter("trae").is_some());
assert!(find_rules_adapter("nonexistent").is_none());
}
#[test]
fn test_rules_file_roundtrip_via_sync() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
let original = RulesFile {
preamble: "# My Rules".to_owned(),
sections: vec![
RulesSection {
name: "Style".to_owned(),
content: "Use 4 spaces.".to_owned(),
},
RulesSection {
name: "Testing".to_owned(),
content: "Run `cargo test`.".to_owned(),
},
],
};
rules::save_rules(root, &original).unwrap();
let content = rules::render_rules(&original);
let results = sync::sync_rules_all(root, &content);
assert_eq!(results.len(), 7); for r in &results {
assert!(r.success, "sync failed for {}: {:?}", r.tool, r.error);
}
for adapter in all_rules_adapters() {
let read = adapter
.read_rules(root)
.unwrap()
.unwrap_or_else(|| panic!("{} should have rules file", adapter.name()));
assert_eq!(
read,
content,
"content mismatch for adapter {}",
adapter.name()
);
}
}