test: add additional Rust integration tests for history and slash_commands

history.rs (+5): deleted file snapshot/diff, non-adjacent diff, duplicate label error, restore commit verification
slash_commands.rs (+8): remove_empty_dirs (3), create_default_commands (1), slash_command_save project scope (5: basic, frontmatter, namespace, empty name error, invalid scope error)

Total Rust tests: 114

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
delibae 2026-03-02 15:57:05 +09:00
parent 17dcb3f273
commit b6a31ebcc7
2 changed files with 260 additions and 0 deletions

View file

@ -848,4 +848,95 @@ mod tests {
let content = fs::read_to_string(&excludes_path).unwrap();
assert!(content.contains(".prism/"), "should migrate to include .prism/");
}
// ─── edge cases ───
#[test]
fn test_history_snapshot_deleted_file() {
let dir = setup_project(&[("a.tex", "aaa"), ("b.tex", "bbb")]);
let r = root(&dir);
history_init(r.clone()).unwrap();
// Delete a file
fs::remove_file(dir.path().join("b.tex")).unwrap();
let snap = history_snapshot(r.clone(), "delete b".into()).unwrap().unwrap();
assert!(!snap.changed_files.is_empty());
}
#[test]
fn test_history_diff_deleted_file() {
let dir = setup_project(&[("a.tex", "keep"), ("b.tex", "remove me")]);
let r = root(&dir);
history_init(r.clone()).unwrap();
let list = history_list(r.clone(), 1, 0).unwrap();
let init_id = list[0].id.clone();
fs::remove_file(dir.path().join("b.tex")).unwrap();
let snap = history_snapshot(r.clone(), "delete b".into()).unwrap().unwrap();
let diffs = history_diff(r, init_id, snap.id).unwrap();
let d = diffs.iter().find(|d| d.file_path == "b.tex").unwrap();
assert_eq!(d.status, "deleted");
assert_eq!(d.old_content.as_deref(), Some("remove me"));
assert!(d.new_content.is_none());
}
#[test]
fn test_history_diff_nonadjacent_snapshots() {
let dir = setup_project(&[("a.tex", "v1")]);
let r = root(&dir);
history_init(r.clone()).unwrap();
let list0 = history_list(r.clone(), 1, 0).unwrap();
let init_id = list0[0].id.clone();
fs::write(dir.path().join("a.tex"), "v2").unwrap();
history_snapshot(r.clone(), "s1".into()).unwrap();
fs::write(dir.path().join("a.tex"), "v3").unwrap();
let snap3 = history_snapshot(r.clone(), "s2".into()).unwrap().unwrap();
// Diff from init directly to s2 (skipping s1)
let diffs = history_diff(r, init_id, snap3.id).unwrap();
let d = diffs.iter().find(|d| d.file_path == "a.tex").unwrap();
assert_eq!(d.old_content.as_deref(), Some("v1"));
assert_eq!(d.new_content.as_deref(), Some("v3"));
}
#[test]
fn test_history_add_duplicate_label_errors() {
let dir = setup_project(&[("main.tex", "x")]);
let r = root(&dir);
history_init(r.clone()).unwrap();
let list = history_list(r.clone(), 1, 0).unwrap();
let id = list[0].id.clone();
history_add_label(r.clone(), id.clone(), "dup".into()).unwrap();
// Adding same label again should error
let result = history_add_label(r, id, "dup".into());
assert!(result.is_err());
}
#[test]
fn test_history_restore_creates_restore_commit() {
let dir = setup_project(&[("main.tex", "original")]);
let r = root(&dir);
history_init(r.clone()).unwrap();
let init_list = history_list(r.clone(), 1, 0).unwrap();
let init_id = init_list[0].id.clone();
fs::write(dir.path().join("main.tex"), "changed").unwrap();
history_snapshot(r.clone(), "change".into()).unwrap();
history_restore(r.clone(), init_id).unwrap();
// Should now have 4 entries: init, change, restore
let list = history_list(r, 10, 0).unwrap();
assert_eq!(list.len(), 3);
assert!(list.iter().any(|s| s.message.contains("[restore]")));
}
}

View file

@ -533,4 +533,173 @@ mod tests {
assert!(names.contains(&"top.md".to_string()));
assert!(names.contains(&"nested.md".to_string()));
}
// --- remove_empty_dirs integration tests ---
#[test]
fn test_remove_empty_dirs_removes_nested_empty() {
let dir = tempfile::tempdir().unwrap();
let deep = dir.path().join("a").join("b").join("c");
fs::create_dir_all(&deep).unwrap();
remove_empty_dirs(&deep);
assert!(!dir.path().join("a").exists(), "entire empty chain should be removed");
}
#[test]
fn test_remove_empty_dirs_stops_at_nonempty() {
let dir = tempfile::tempdir().unwrap();
let parent = dir.path().join("a");
let child = parent.join("b");
fs::create_dir_all(&child).unwrap();
fs::write(parent.join("keep.txt"), "x").unwrap();
remove_empty_dirs(&child);
assert!(!child.exists(), "empty child should be removed");
assert!(parent.exists(), "parent with file should be kept");
}
#[test]
fn test_remove_empty_dirs_nonexistent_is_noop() {
let dir = tempfile::tempdir().unwrap();
let fake = dir.path().join("nonexistent");
// Should not panic
remove_empty_dirs(&fake);
}
// --- create_default_commands ---
#[test]
fn test_create_default_commands_structure() {
let cmds = create_default_commands();
assert_eq!(cmds.len(), 3);
let names: Vec<&str> = cmds.iter().map(|c| c.name.as_str()).collect();
assert!(names.contains(&"add-dir"));
assert!(names.contains(&"init"));
assert!(names.contains(&"review"));
for cmd in &cmds {
assert_eq!(cmd.scope, "default");
assert!(cmd.full_command.starts_with('/'));
}
}
// --- slash_command_save integration tests ---
#[tokio::test]
async fn test_slash_command_save_project_scope() {
let dir = tempfile::tempdir().unwrap();
let project_path = dir.path().to_string_lossy().to_string();
let result = slash_command_save(
"project".into(),
"test-cmd".into(),
None,
"Do something".into(),
None,
vec![],
Some(project_path.clone()),
)
.await;
assert!(result.is_ok());
let cmd = result.unwrap();
assert_eq!(cmd.name, "test-cmd");
assert_eq!(cmd.full_command, "/test-cmd");
assert_eq!(cmd.content, "Do something");
// Verify file was created
let file = dir.path().join(".claude").join("commands").join("test-cmd.md");
assert!(file.exists());
assert_eq!(fs::read_to_string(&file).unwrap(), "Do something");
}
#[tokio::test]
async fn test_slash_command_save_with_frontmatter() {
let dir = tempfile::tempdir().unwrap();
let project_path = dir.path().to_string_lossy().to_string();
let cmd = slash_command_save(
"project".into(),
"lint".into(),
None,
"Run the linter".into(),
Some("Lint all files".into()),
vec!["Bash".into(), "Read".into()],
Some(project_path),
)
.await
.unwrap();
assert_eq!(cmd.description.unwrap(), "Lint all files");
assert_eq!(cmd.allowed_tools, vec!["Bash", "Read"]);
// Verify frontmatter in file
let file = dir.path().join(".claude").join("commands").join("lint.md");
let content = fs::read_to_string(&file).unwrap();
assert!(content.starts_with("---\n"));
assert!(content.contains("description: Lint all files"));
assert!(content.contains("- Bash"));
}
#[tokio::test]
async fn test_slash_command_save_with_namespace() {
let dir = tempfile::tempdir().unwrap();
let project_path = dir.path().to_string_lossy().to_string();
let cmd = slash_command_save(
"project".into(),
"clippy".into(),
Some("tools:rust".into()),
"Run clippy".into(),
None,
vec![],
Some(project_path),
)
.await
.unwrap();
assert_eq!(cmd.name, "clippy");
assert_eq!(cmd.namespace.unwrap(), "tools:rust");
assert_eq!(cmd.full_command, "/tools:rust:clippy");
// Verify nested directory structure
let file = dir.path().join(".claude").join("commands").join("tools").join("rust").join("clippy.md");
assert!(file.exists());
}
#[tokio::test]
async fn test_slash_command_save_empty_name_errors() {
let result = slash_command_save(
"project".into(),
"".into(),
None,
"content".into(),
None,
vec![],
Some("/tmp".into()),
)
.await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("empty"));
}
#[tokio::test]
async fn test_slash_command_save_invalid_scope_errors() {
let result = slash_command_save(
"global".into(),
"test".into(),
None,
"content".into(),
None,
vec![],
None,
)
.await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid scope"));
}
}