use std::fs;
use tempfile::TempDir;
use spool::context::SpoolContext;
use spool::event::{Event, Operation};
use spool::writer::{
complete_task, create_stream, create_task, delete_stream, get_current_branch, get_current_user,
reopen_task, set_stream, update_stream, update_task, write_event, CreateTaskParams,
};
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())
}
#[test]
fn test_write_event_creates_file() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ctx = create_test_context(&spool_dir);
let event = Event {
v: 1,
op: Operation::Create,
id: "test-001".to_string(),
ts: chrono::Utc::now(),
by: "@tester".to_string(),
branch: "main".to_string(),
d: serde_json::json!({"title": "Test task"}),
};
write_event(&ctx, &event).unwrap();
let event_files = ctx.get_event_files().unwrap();
assert_eq!(event_files.len(), 1);
let content = fs::read_to_string(&event_files[0]).unwrap();
assert!(content.contains("test-001"));
assert!(content.contains("Test task"));
}
#[test]
fn test_write_event_appends_to_existing_file() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ctx = create_test_context(&spool_dir);
let event1 = Event {
v: 1,
op: Operation::Create,
id: "test-001".to_string(),
ts: chrono::Utc::now(),
by: "@tester".to_string(),
branch: "main".to_string(),
d: serde_json::json!({"title": "First task"}),
};
let event2 = Event {
v: 1,
op: Operation::Create,
id: "test-002".to_string(),
ts: chrono::Utc::now(),
by: "@tester".to_string(),
branch: "main".to_string(),
d: serde_json::json!({"title": "Second task"}),
};
write_event(&ctx, &event1).unwrap();
write_event(&ctx, &event2).unwrap();
let event_files = ctx.get_event_files().unwrap();
assert_eq!(event_files.len(), 1);
let content = fs::read_to_string(&event_files[0]).unwrap();
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 2);
assert!(lines[0].contains("test-001"));
assert!(lines[1].contains("test-002"));
}
#[test]
fn test_create_task_returns_id() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ctx = create_test_context(&spool_dir);
let id = create_task(
&ctx,
CreateTaskParams {
title: "Test task",
..Default::default()
},
"@tester",
"main",
)
.unwrap();
assert!(!id.is_empty());
let event_files = ctx.get_event_files().unwrap();
assert_eq!(event_files.len(), 1);
let content = fs::read_to_string(&event_files[0]).unwrap();
assert!(content.contains(&id));
assert!(content.contains("Test task"));
}
#[test]
fn test_create_task_with_all_fields() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ctx = create_test_context(&spool_dir);
let id = create_task(
&ctx,
CreateTaskParams {
title: "Full task",
description: Some("Task description"),
priority: Some("p1"),
assignee: Some("@dev"),
tags: vec!["bug".to_string(), "urgent".to_string()],
stream: None,
},
"@tester",
"feature-branch",
)
.unwrap();
let event_files = ctx.get_event_files().unwrap();
let content = fs::read_to_string(&event_files[0]).unwrap();
assert!(content.contains(&id));
assert!(content.contains("Full task"));
assert!(content.contains("Task description"));
assert!(content.contains("p1"));
assert!(content.contains("@dev"));
assert!(content.contains("bug"));
assert!(content.contains("urgent"));
assert!(content.contains("feature-branch"));
}
#[test]
fn test_update_task_writes_event() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ctx = create_test_context(&spool_dir);
update_task(
&ctx,
"task-001",
Some("New title"),
Some("New description"),
Some("p0"),
"@tester",
"main",
)
.unwrap();
let event_files = ctx.get_event_files().unwrap();
let content = fs::read_to_string(&event_files[0]).unwrap();
assert!(content.contains("task-001"));
assert!(content.contains("update"));
assert!(content.contains("New title"));
assert!(content.contains("New description"));
assert!(content.contains("p0"));
}
#[test]
fn test_update_task_partial_fields() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ctx = create_test_context(&spool_dir);
update_task(
&ctx,
"task-001",
Some("Only title"),
None,
None,
"@tester",
"main",
)
.unwrap();
let event_files = ctx.get_event_files().unwrap();
let content = fs::read_to_string(&event_files[0]).unwrap();
assert!(content.contains("Only title"));
assert!(!content.contains("description"));
assert!(!content.contains("priority"));
}
#[test]
fn test_update_task_no_fields_errors() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ctx = create_test_context(&spool_dir);
let result = update_task(&ctx, "task-001", None, None, None, "@tester", "main");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("No fields to update"));
}
#[test]
fn test_complete_task_writes_event() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ctx = create_test_context(&spool_dir);
complete_task(&ctx, "task-001", Some("done"), "@tester", "main").unwrap();
let event_files = ctx.get_event_files().unwrap();
let content = fs::read_to_string(&event_files[0]).unwrap();
assert!(content.contains("task-001"));
assert!(content.contains("complete"));
assert!(content.contains("done"));
}
#[test]
fn test_complete_task_default_resolution() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ctx = create_test_context(&spool_dir);
complete_task(&ctx, "task-001", None, "@tester", "main").unwrap();
let event_files = ctx.get_event_files().unwrap();
let content = fs::read_to_string(&event_files[0]).unwrap();
assert!(content.contains("done")); }
#[test]
fn test_complete_task_wontfix_resolution() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ctx = create_test_context(&spool_dir);
complete_task(&ctx, "task-001", Some("wontfix"), "@tester", "main").unwrap();
let event_files = ctx.get_event_files().unwrap();
let content = fs::read_to_string(&event_files[0]).unwrap();
assert!(content.contains("wontfix"));
}
#[test]
fn test_reopen_task_writes_event() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ctx = create_test_context(&spool_dir);
reopen_task(&ctx, "task-001", "@tester", "main").unwrap();
let event_files = ctx.get_event_files().unwrap();
let content = fs::read_to_string(&event_files[0]).unwrap();
assert!(content.contains("task-001"));
assert!(content.contains("reopen"));
}
#[test]
fn test_get_current_user_returns_formatted_user() {
let user = get_current_user().unwrap();
assert!(user.starts_with('@'));
assert!(user.len() > 1);
}
#[test]
fn test_get_current_branch_returns_branch() {
let branch = get_current_branch().unwrap();
assert!(!branch.is_empty());
}
#[test]
fn test_create_task_generates_unique_ids() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ctx = create_test_context(&spool_dir);
let id1 = create_task(
&ctx,
CreateTaskParams {
title: "Task 1",
..Default::default()
},
"@tester",
"main",
)
.unwrap();
let id2 = create_task(
&ctx,
CreateTaskParams {
title: "Task 2",
..Default::default()
},
"@tester",
"main",
)
.unwrap();
let id3 = create_task(
&ctx,
CreateTaskParams {
title: "Task 3",
..Default::default()
},
"@tester",
"main",
)
.unwrap();
assert_ne!(id1, id2);
assert_ne!(id2, id3);
assert_ne!(id1, id3);
}
#[test]
fn test_event_json_format() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ctx = create_test_context(&spool_dir);
create_task(
&ctx,
CreateTaskParams {
title: "Test",
..Default::default()
},
"@tester",
"main",
)
.unwrap();
let event_files = ctx.get_event_files().unwrap();
let content = fs::read_to_string(&event_files[0]).unwrap();
let parsed: serde_json::Value = serde_json::from_str(content.trim()).unwrap();
assert_eq!(parsed["v"], 1);
assert_eq!(parsed["op"], "create");
assert!(parsed["id"].is_string());
assert!(parsed["ts"].is_string());
assert_eq!(parsed["by"], "@tester");
assert_eq!(parsed["branch"], "main");
assert!(parsed["d"].is_object());
}
#[test]
fn test_create_task_with_stream() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ctx = create_test_context(&spool_dir);
let _id = create_task(
&ctx,
CreateTaskParams {
title: "Task in stream",
stream: Some("my-stream"),
..Default::default()
},
"@tester",
"main",
)
.unwrap();
let event_files = ctx.get_event_files().unwrap();
let content = fs::read_to_string(&event_files[0]).unwrap();
assert!(content.contains("\"stream\":\"my-stream\""));
}
#[test]
fn test_set_stream_writes_event() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ctx = create_test_context(&spool_dir);
let id = create_task(
&ctx,
CreateTaskParams {
title: "Test task",
..Default::default()
},
"@tester",
"main",
)
.unwrap();
set_stream(&ctx, &id, Some("agent-work"), "@tester", "main").unwrap();
let event_files = ctx.get_event_files().unwrap();
let content = fs::read_to_string(&event_files[0]).unwrap();
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 2);
let set_stream_event: serde_json::Value = serde_json::from_str(lines[1]).unwrap();
assert_eq!(set_stream_event["op"], "set_stream");
assert_eq!(set_stream_event["id"], id);
assert_eq!(set_stream_event["d"]["stream"], "agent-work");
}
#[test]
fn test_set_stream_to_none() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ctx = create_test_context(&spool_dir);
let id = create_task(
&ctx,
CreateTaskParams {
title: "Test",
stream: Some("old-stream"),
..Default::default()
},
"@tester",
"main",
)
.unwrap();
set_stream(&ctx, &id, None, "@tester", "main").unwrap();
let event_files = ctx.get_event_files().unwrap();
let content = fs::read_to_string(&event_files[0]).unwrap();
let lines: Vec<&str> = content.lines().collect();
let set_stream_event: serde_json::Value = serde_json::from_str(lines[1]).unwrap();
assert_eq!(set_stream_event["op"], "set_stream");
assert!(set_stream_event["d"]["stream"].is_null());
}
#[test]
fn test_create_stream_returns_id() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ctx = create_test_context(&spool_dir);
let id = create_stream(&ctx, "My Project", None, "@tester", "main").unwrap();
assert!(!id.is_empty());
let event_files = ctx.get_event_files().unwrap();
assert_eq!(event_files.len(), 1);
let content = fs::read_to_string(&event_files[0]).unwrap();
let event: serde_json::Value = serde_json::from_str(content.trim()).unwrap();
assert_eq!(event["op"], "create_stream");
assert_eq!(event["id"], id);
assert_eq!(event["d"]["name"], "My Project");
}
#[test]
fn test_create_stream_with_description() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ctx = create_test_context(&spool_dir);
let id = create_stream(
&ctx,
"Backend",
Some("Backend development tasks"),
"@tester",
"main",
)
.unwrap();
let event_files = ctx.get_event_files().unwrap();
let content = fs::read_to_string(&event_files[0]).unwrap();
let event: serde_json::Value = serde_json::from_str(content.trim()).unwrap();
assert_eq!(event["op"], "create_stream");
assert_eq!(event["id"], id);
assert_eq!(event["d"]["name"], "Backend");
assert_eq!(event["d"]["description"], "Backend development tasks");
}
#[test]
fn test_update_stream_writes_event() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ctx = create_test_context(&spool_dir);
let id = create_stream(&ctx, "Original Name", None, "@tester", "main").unwrap();
update_stream(
&ctx,
&id,
Some("New Name"),
Some("Updated description"),
"@tester",
"main",
)
.unwrap();
let event_files = ctx.get_event_files().unwrap();
let content = fs::read_to_string(&event_files[0]).unwrap();
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 2);
let update_event: serde_json::Value = serde_json::from_str(lines[1]).unwrap();
assert_eq!(update_event["op"], "update_stream");
assert_eq!(update_event["id"], id);
assert_eq!(update_event["d"]["name"], "New Name");
assert_eq!(update_event["d"]["description"], "Updated description");
}
#[test]
fn test_update_stream_no_fields_errors() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ctx = create_test_context(&spool_dir);
let result = update_stream(&ctx, "stream-001", None, None, "@tester", "main");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("No fields to update"));
}
#[test]
fn test_delete_stream_writes_event() {
let temp_dir = TempDir::new().unwrap();
let spool_dir = setup_spool_dir(&temp_dir);
let ctx = create_test_context(&spool_dir);
let id = create_stream(&ctx, "To Delete", None, "@tester", "main").unwrap();
delete_stream(&ctx, &id, "@tester", "main").unwrap();
let event_files = ctx.get_event_files().unwrap();
let content = fs::read_to_string(&event_files[0]).unwrap();
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 2);
let delete_event: serde_json::Value = serde_json::from_str(lines[1]).unwrap();
assert_eq!(delete_event["op"], "delete_stream");
assert_eq!(delete_event["id"], id);
}