use chrono::{Duration, Utc};
use serde_json::json;
use spool::context::SpoolContext;
use std::fs;
use std::io::Write;
use tempfile::TempDir;
fn setup_spool_dir(temp_dir: &TempDir) -> std::path::PathBuf {
let spool_dir = temp_dir.path().join(".spool");
fs::create_dir_all(spool_dir.join("events")).unwrap();
fs::create_dir_all(spool_dir.join("archive")).unwrap();
spool_dir
}
fn create_test_context(spool_dir: &std::path::Path) -> SpoolContext {
SpoolContext::new(spool_dir.to_path_buf())
}
fn write_events(dir: &std::path::Path, filename: &str, events: &[serde_json::Value]) {
let path = dir.join(filename);
let mut file = fs::File::create(path).unwrap();
for event in events {
writeln!(file, "{}", serde_json::to_string(event).unwrap()).unwrap();
}
}
#[test]
fn test_archive_no_tasks_to_archive() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let events = vec![json!({
"v": 1, "op": "create", "id": "open-task",
"ts": "2024-01-15T10:00:00Z", "by": "tester", "branch": "main",
"d": {"title": "Open Task"}
})];
write_events(&spool_dir.join("events"), "2024-01-15.jsonl", &events);
let ctx = create_test_context(&spool_dir);
let archived = spool::archive::archive_tasks(&ctx, 30, false).unwrap();
assert!(archived.is_empty());
}
#[test]
fn test_archive_recently_completed_not_archived() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let now = Utc::now();
let events = vec![
json!({
"v": 1, "op": "create", "id": "recent-complete",
"ts": now.to_rfc3339(), "by": "tester", "branch": "main",
"d": {"title": "Recently Completed"}
}),
json!({
"v": 1, "op": "complete", "id": "recent-complete",
"ts": now.to_rfc3339(), "by": "tester", "branch": "main",
"d": {}
}),
];
write_events(
&spool_dir.join("events"),
&format!("{}.jsonl", now.format("%Y-%m-%d")),
&events,
);
let ctx = create_test_context(&spool_dir);
let archived = spool::archive::archive_tasks(&ctx, 30, false).unwrap();
assert!(archived.is_empty());
}
#[test]
fn test_archive_old_completed_archived() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let old_date = Utc::now() - Duration::days(60);
let events = vec![
json!({
"v": 1, "op": "create", "id": "old-complete",
"ts": old_date.to_rfc3339(), "by": "tester", "branch": "main",
"d": {"title": "Old Completed Task"}
}),
json!({
"v": 1, "op": "complete", "id": "old-complete",
"ts": old_date.to_rfc3339(), "by": "tester", "branch": "main",
"d": {}
}),
];
write_events(
&spool_dir.join("events"),
&format!("{}.jsonl", old_date.format("%Y-%m-%d")),
&events,
);
let ctx = create_test_context(&spool_dir);
let archived = spool::archive::archive_tasks(&ctx, 30, false).unwrap();
assert_eq!(archived.len(), 1);
assert!(archived.contains(&"old-complete".to_string()));
let archive_files: Vec<_> = fs::read_dir(spool_dir.join("archive"))
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map_or(false, |ext| ext == "jsonl"))
.collect();
assert!(!archive_files.is_empty());
}
#[test]
fn test_archive_dry_run() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let old_date = Utc::now() - Duration::days(60);
let events = vec![
json!({
"v": 1, "op": "create", "id": "dry-run-task",
"ts": old_date.to_rfc3339(), "by": "tester", "branch": "main",
"d": {"title": "Dry Run Task"}
}),
json!({
"v": 1, "op": "complete", "id": "dry-run-task",
"ts": old_date.to_rfc3339(), "by": "tester", "branch": "main",
"d": {}
}),
];
write_events(
&spool_dir.join("events"),
&format!("{}.jsonl", old_date.format("%Y-%m-%d")),
&events,
);
let ctx = create_test_context(&spool_dir);
let archived = spool::archive::archive_tasks(&ctx, 30, true).unwrap();
assert_eq!(archived.len(), 1);
let archive_files: Vec<_> = fs::read_dir(spool_dir.join("archive"))
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map_or(false, |ext| ext == "jsonl"))
.collect();
assert!(archive_files.is_empty());
}
#[test]
fn test_archive_custom_days_threshold() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ten_days_ago = Utc::now() - Duration::days(10);
let events = vec![
json!({
"v": 1, "op": "create", "id": "threshold-task",
"ts": ten_days_ago.to_rfc3339(), "by": "tester", "branch": "main",
"d": {"title": "Threshold Task"}
}),
json!({
"v": 1, "op": "complete", "id": "threshold-task",
"ts": ten_days_ago.to_rfc3339(), "by": "tester", "branch": "main",
"d": {}
}),
];
write_events(
&spool_dir.join("events"),
&format!("{}.jsonl", ten_days_ago.format("%Y-%m-%d")),
&events,
);
let ctx = create_test_context(&spool_dir);
let archived_30 = spool::archive::archive_tasks(&ctx, 30, true).unwrap();
assert!(archived_30.is_empty());
let archived_7 = spool::archive::archive_tasks(&ctx, 7, true).unwrap();
assert_eq!(archived_7.len(), 1);
}
#[test]
fn test_archive_already_archived_not_rearchived() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let old_date = Utc::now() - Duration::days(60);
let events = vec![
json!({
"v": 1, "op": "create", "id": "already-archived",
"ts": old_date.to_rfc3339(), "by": "tester", "branch": "main",
"d": {"title": "Already Archived Task"}
}),
json!({
"v": 1, "op": "complete", "id": "already-archived",
"ts": old_date.to_rfc3339(), "by": "tester", "branch": "main",
"d": {}
}),
json!({
"v": 1, "op": "archive", "id": "already-archived",
"ts": (old_date + Duration::days(1)).to_rfc3339(), "by": "@spool", "branch": "main",
"d": {"ref": old_date.format("%Y-%m").to_string()}
}),
];
write_events(
&spool_dir.join("events"),
&format!("{}.jsonl", old_date.format("%Y-%m-%d")),
&events,
);
let ctx = create_test_context(&spool_dir);
let archived = spool::archive::archive_tasks(&ctx, 30, true).unwrap();
assert!(archived.is_empty());
}
#[test]
fn test_collect_all_events() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let events1 = vec![
json!({
"v": 1, "op": "create", "id": "task-a",
"ts": "2024-01-15T10:00:00Z", "by": "tester", "branch": "main",
"d": {"title": "Task A"}
}),
json!({
"v": 1, "op": "create", "id": "task-b",
"ts": "2024-01-15T11:00:00Z", "by": "tester", "branch": "main",
"d": {"title": "Task B"}
}),
];
let events2 = vec![json!({
"v": 1, "op": "update", "id": "task-a",
"ts": "2024-01-16T10:00:00Z", "by": "tester", "branch": "main",
"d": {"title": "Task A Updated"}
})];
write_events(&spool_dir.join("events"), "2024-01-15.jsonl", &events1);
write_events(&spool_dir.join("events"), "2024-01-16.jsonl", &events2);
let ctx = create_test_context(&spool_dir);
let all_events = spool::archive::collect_all_events(&ctx).unwrap();
assert_eq!(all_events.len(), 2);
assert_eq!(all_events.get("task-a").unwrap().len(), 2);
assert_eq!(all_events.get("task-b").unwrap().len(), 1);
}
#[test]
fn test_archive_multiple_tasks_grouped_by_month() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let month1 = Utc::now() - Duration::days(90);
let month2 = Utc::now() - Duration::days(60);
let events = vec![
json!({
"v": 1, "op": "create", "id": "task-month1",
"ts": month1.to_rfc3339(), "by": "tester", "branch": "main",
"d": {"title": "Month 1 Task"}
}),
json!({
"v": 1, "op": "complete", "id": "task-month1",
"ts": month1.to_rfc3339(), "by": "tester", "branch": "main",
"d": {}
}),
json!({
"v": 1, "op": "create", "id": "task-month2",
"ts": month2.to_rfc3339(), "by": "tester", "branch": "main",
"d": {"title": "Month 2 Task"}
}),
json!({
"v": 1, "op": "complete", "id": "task-month2",
"ts": month2.to_rfc3339(), "by": "tester", "branch": "main",
"d": {}
}),
];
write_events(
&spool_dir.join("events"),
&format!("{}.jsonl", month1.format("%Y-%m-%d")),
&events,
);
let ctx = create_test_context(&spool_dir);
let archived = spool::archive::archive_tasks(&ctx, 30, false).unwrap();
assert_eq!(archived.len(), 2);
assert!(archived.contains(&"task-month1".to_string()));
assert!(archived.contains(&"task-month2".to_string()));
}