use anyhow::Result;
use ralph::contracts::TaskStatus;
mod test_support;
#[test]
fn queue_archive_moves_terminal_tasks_to_done() -> Result<()> {
let dir = test_support::temp_dir_outside_repo();
test_support::git_init(dir.path())?;
test_support::seed_ralph_dir(dir.path())?;
let t1 = test_support::make_test_task("RQ-0001", "Todo", TaskStatus::Todo);
let mut t2 = test_support::make_test_task("RQ-0002", "Done", TaskStatus::Done);
let mut t3 = test_support::make_test_task("RQ-0003", "Rejected", TaskStatus::Rejected);
let t4 = test_support::make_test_task("RQ-0004", "Doing", TaskStatus::Doing);
t2.completed_at = Some("2026-01-20T00:00:00Z".to_string());
t3.completed_at = Some("2026-01-21T00:00:00Z".to_string());
test_support::write_queue(
dir.path(),
&[t1.clone(), t2.clone(), t3.clone(), t4.clone()],
)?;
test_support::write_done(dir.path(), &[])?;
let (status, _stdout, stderr) = test_support::run_in_dir(dir.path(), &["queue", "archive"]);
anyhow::ensure!(status.success(), "archive failed\nstderr:\n{stderr}");
let queue = test_support::read_queue(dir.path())?;
let done = test_support::read_done(dir.path())?;
let queue_ids: Vec<_> = queue.tasks.iter().map(|t| t.id.as_str()).collect();
let done_ids: Vec<_> = done.tasks.iter().map(|t| t.id.as_str()).collect();
anyhow::ensure!(
queue_ids == vec!["RQ-0001", "RQ-0004"],
"unexpected queue: {queue_ids:?}"
);
anyhow::ensure!(
done_ids.contains(&"RQ-0002") && done_ids.contains(&"RQ-0003"),
"unexpected done: {done_ids:?}"
);
Ok(())
}
#[test]
fn queue_archive_is_noop_when_no_terminal_tasks() -> Result<()> {
let dir = test_support::temp_dir_outside_repo();
test_support::git_init(dir.path())?;
test_support::seed_ralph_dir(dir.path())?;
let t1 = test_support::make_test_task("RQ-0001", "Todo", TaskStatus::Todo);
let t2 = test_support::make_test_task("RQ-0002", "Doing", TaskStatus::Doing);
test_support::write_queue(dir.path(), &[t1, t2])?;
test_support::write_done(dir.path(), &[])?;
let before_queue = std::fs::read_to_string(dir.path().join(".ralph/queue.jsonc"))?;
let before_done = std::fs::read_to_string(dir.path().join(".ralph/done.jsonc"))?;
let (status, _stdout, stderr) = test_support::run_in_dir(dir.path(), &["queue", "archive"]);
anyhow::ensure!(status.success(), "archive failed\nstderr:\n{stderr}");
let after_queue = std::fs::read_to_string(dir.path().join(".ralph/queue.jsonc"))?;
let after_done = std::fs::read_to_string(dir.path().join(".ralph/done.jsonc"))?;
anyhow::ensure!(before_queue == after_queue, "queue changed on noop archive");
anyhow::ensure!(before_done == after_done, "done changed on noop archive");
Ok(())
}
#[test]
fn queue_archive_appends_to_existing_done_file() -> Result<()> {
let dir = test_support::temp_dir_outside_repo();
test_support::git_init(dir.path())?;
test_support::seed_ralph_dir(dir.path())?;
let mut existing_done =
test_support::make_test_task("RQ-0100", "Already archived", TaskStatus::Done);
existing_done.completed_at = Some("2026-01-10T00:00:00Z".to_string());
test_support::write_done(dir.path(), &[existing_done])?;
let mut t1 = test_support::make_test_task("RQ-0001", "Done task", TaskStatus::Done);
t1.completed_at = Some("2026-01-20T00:00:00Z".to_string());
test_support::write_queue(dir.path(), &[t1])?;
let (status, _stdout, stderr) = test_support::run_in_dir(dir.path(), &["queue", "archive"]);
anyhow::ensure!(status.success(), "archive failed\nstderr:\n{stderr}");
let done = test_support::read_done(dir.path())?;
anyhow::ensure!(
done.tasks.iter().any(|t| t.id == "RQ-0001"),
"archived task should be in done.jsonc"
);
anyhow::ensure!(
done.tasks.iter().any(|t| t.id == "RQ-0100"),
"existing done task should still be in done.jsonc"
);
Ok(())
}
#[test]
fn queue_archive_dry_run_does_not_modify_files() -> Result<()> {
let dir = test_support::temp_dir_outside_repo();
test_support::git_init(dir.path())?;
test_support::seed_ralph_dir(dir.path())?;
let t1 = test_support::make_test_task("RQ-0001", "Todo", TaskStatus::Todo);
let mut t2 = test_support::make_test_task("RQ-0002", "Done", TaskStatus::Done);
t2.completed_at = Some("2026-01-20T00:00:00Z".to_string());
test_support::write_queue(dir.path(), &[t1.clone(), t2.clone()])?;
test_support::write_done(dir.path(), &[])?;
let before_queue = std::fs::read_to_string(dir.path().join(".ralph/queue.jsonc"))?;
let before_done = std::fs::read_to_string(dir.path().join(".ralph/done.jsonc"))?;
let (status, stdout, stderr) =
test_support::run_in_dir(dir.path(), &["queue", "archive", "--dry-run"]);
anyhow::ensure!(
status.success(),
"archive --dry-run failed\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
anyhow::ensure!(
stderr.contains("Dry run") || stdout.contains("Dry run"),
"expected dry-run message, got stdout:\n{stdout}\nstderr:\n{stderr}"
);
let after_queue = std::fs::read_to_string(dir.path().join(".ralph/queue.jsonc"))?;
let after_done = std::fs::read_to_string(dir.path().join(".ralph/done.jsonc"))?;
anyhow::ensure!(before_queue == after_queue, "queue changed during dry-run");
anyhow::ensure!(before_done == after_done, "done changed during dry-run");
Ok(())
}
#[test]
fn queue_archive_dry_run_shows_what_would_be_archived() -> Result<()> {
let dir = test_support::temp_dir_outside_repo();
test_support::git_init(dir.path())?;
test_support::seed_ralph_dir(dir.path())?;
let t1 = test_support::make_test_task("RQ-0001", "Todo", TaskStatus::Todo);
let mut t2 = test_support::make_test_task("RQ-0002", "Done", TaskStatus::Done);
let mut t3 = test_support::make_test_task("RQ-0003", "Rejected", TaskStatus::Rejected);
t2.completed_at = Some("2026-01-20T00:00:00Z".to_string());
t3.completed_at = Some("2026-01-21T00:00:00Z".to_string());
test_support::write_queue(dir.path(), &[t1.clone(), t2.clone(), t3.clone()])?;
test_support::write_done(dir.path(), &[])?;
let (status, stdout, stderr) =
test_support::run_in_dir(dir.path(), &["queue", "archive", "--dry-run"]);
anyhow::ensure!(
status.success(),
"archive --dry-run failed\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let output = format!("{stdout}\n{stderr}");
anyhow::ensure!(
output.contains("RQ-0002") && output.contains("RQ-0003"),
"expected task IDs in output, got:\n{output}"
);
anyhow::ensure!(
output.contains("2") || output.contains("two"),
"expected count of tasks to archive, got:\n{output}"
);
Ok(())
}
#[test]
fn queue_archive_dry_run_no_terminal_tasks() -> Result<()> {
let dir = test_support::temp_dir_outside_repo();
test_support::git_init(dir.path())?;
test_support::seed_ralph_dir(dir.path())?;
let t1 = test_support::make_test_task("RQ-0001", "Todo", TaskStatus::Todo);
test_support::write_queue(dir.path(), &[t1])?;
test_support::write_done(dir.path(), &[])?;
let (status, stdout, stderr) =
test_support::run_in_dir(dir.path(), &["queue", "archive", "--dry-run"]);
anyhow::ensure!(
status.success(),
"archive --dry-run failed\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let output = format!("{stdout}\n{stderr}");
anyhow::ensure!(
output.contains("no terminal tasks") || output.contains("no tasks"),
"expected 'no terminal tasks' message, got:\n{output}"
);
Ok(())
}