mod test_support;
use anyhow::{Context, Result};
use cueloop::config::project_runtime_dir;
use cueloop::contracts::{Task, TaskStatus};
use std::path::Path;
use std::process::Command;
use test_support::{
QueueDoneSnapshot, git_add_all_commit, git_init, make_test_task, run_in_dir, seed_cueloop_dir,
snapshot_queue_done, temp_dir_outside_repo, write_done, write_queue,
write_valid_single_todo_queue,
};
fn git_status_porcelain(dir: &Path) -> Result<String> {
let output = Command::new("git")
.current_dir(dir)
.args(["status", "--short"])
.output()
.context("run git status --short")?;
anyhow::ensure!(
output.status.success(),
"git status failed: {}",
String::from_utf8_lossy(&output.stderr)
);
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
fn assert_repo_clean_and_files_unchanged(
dir: &Path,
before: &QueueDoneSnapshot,
context: &str,
) -> Result<()> {
let after = snapshot_queue_done(dir)?;
assert_eq!(after, *before, "{context}: queue/done files changed");
let status = git_status_porcelain(dir)?;
assert!(status.is_empty(), "{context}: repo became dirty: {status}");
Ok(())
}
fn list_runtime_entries(dir: &Path) -> Result<Vec<String>> {
let mut names = std::fs::read_dir(project_runtime_dir(dir))?
.map(|entry| {
entry
.map(|value| value.file_name().to_string_lossy().into_owned())
.context("read runtime entry")
})
.collect::<Result<Vec<_>>>()?;
names.sort();
Ok(names)
}
fn make_parent_task() -> Task {
make_test_task("RQ-0001", "Parent task", TaskStatus::Todo)
}
fn make_child_task() -> Task {
let mut child = make_test_task("RQ-0002", "Child task", TaskStatus::Todo);
child.parent_id = Some("RQ-0001".to_string());
child
}
fn make_done_task() -> Task {
make_test_task("RQ-0003", "Completed task", TaskStatus::Done)
}
fn setup_valid_repo() -> Result<tempfile::TempDir> {
let dir = temp_dir_outside_repo();
git_init(dir.path())?;
seed_cueloop_dir(dir.path())?;
write_queue(dir.path(), &[make_parent_task(), make_child_task()])?;
write_done(dir.path(), &[make_done_task()])?;
git_add_all_commit(dir.path(), "seed queue state")?;
Ok(dir)
}
fn setup_invalid_repo_with_non_utc_timestamp() -> Result<tempfile::TempDir> {
let dir = temp_dir_outside_repo();
git_init(dir.path())?;
seed_cueloop_dir(dir.path())?;
let mut legacy = make_test_task("RQ-0001", "Legacy task", TaskStatus::Todo);
legacy.created_at = Some("2026-01-18T12:00:00-05:00".to_string());
write_queue(dir.path(), &[legacy])?;
write_done(dir.path(), &[])?;
git_add_all_commit(dir.path(), "seed legacy queue state")?;
Ok(dir)
}
#[test]
fn read_only_commands_leave_repo_clean_on_success() -> Result<()> {
let dir = setup_valid_repo()?;
let commands: &[&[&str]] = &[
&["queue", "validate"],
&["queue", "list", "--include-done"],
&["queue", "search", "Child", "--include-done"],
&["queue", "show", "RQ-0002"],
&["queue", "history", "--days", "7"],
&["queue", "dashboard", "--days", "7"],
&["queue", "graph", "--include-done"],
&["queue", "next"],
&["queue", "explain", "--format", "json"],
&["task", "parent", "RQ-0002"],
&["task", "children", "RQ-0001", "--recursive"],
];
for args in commands {
let before = snapshot_queue_done(dir.path())?;
let (status, stdout, stderr) = run_in_dir(dir.path(), args);
anyhow::ensure!(
status.success(),
"command failed: {:?}\nstdout:\n{stdout}\nstderr:\n{stderr}",
args
);
assert_repo_clean_and_files_unchanged(
dir.path(),
&before,
&format!("read-only command {:?}", args),
)?;
}
Ok(())
}
#[test]
fn queue_validate_stays_clean_without_startup_readme_sanity() -> Result<()> {
let dir = temp_dir_outside_repo();
git_init(dir.path())?;
write_valid_single_todo_queue(dir.path())?;
git_add_all_commit(dir.path(), "seed minimal queue repo")?;
let runtime_dir = project_runtime_dir(dir.path());
let readme_path = runtime_dir.join("README.md");
assert!(
!readme_path.exists(),
"fixture should start without runtime README.md"
);
for args in [
&["queue", "validate"][..],
&["--auto-fix", "queue", "validate"][..],
] {
let before_snapshot = snapshot_queue_done(dir.path())?;
let before_entries = list_runtime_entries(dir.path())?;
let (status, _stdout, stderr) = run_in_dir(dir.path(), args);
anyhow::ensure!(
status.success(),
"queue validate failed for {:?}\nstderr:\n{stderr}",
args
);
assert!(
!readme_path.exists(),
"queue validate must not create runtime README.md for {:?}",
args
);
let after_entries = list_runtime_entries(dir.path())?;
assert_eq!(
before_entries, after_entries,
"queue validate must not add runtime files for {:?}",
args
);
assert!(
!stderr.contains("Created README at version"),
"queue validate should not report README creation for {:?}: {stderr}",
args
);
assert!(
!stderr.contains("README.md is missing"),
"queue validate should not emit README startup warnings for {:?}: {stderr}",
args
);
assert_repo_clean_and_files_unchanged(
dir.path(),
&before_snapshot,
&format!(
"queue validate should stay read-only with missing README for {:?}",
args
),
)?;
}
Ok(())
}
#[test]
fn read_only_commands_do_not_rewrite_legacy_timestamps_on_failure() -> Result<()> {
let dir = setup_invalid_repo_with_non_utc_timestamp()?;
let commands: &[&[&str]] = &[
&["queue", "list"],
&["queue", "search", "Legacy"],
&["queue", "show", "RQ-0001"],
];
for args in commands {
let before = snapshot_queue_done(dir.path())?;
let (status, _stdout, stderr) = run_in_dir(dir.path(), args);
assert!(
!status.success(),
"command unexpectedly succeeded for legacy invalid queue: {:?}",
args
);
assert!(
stderr.contains("RFC3339 UTC timestamp"),
"expected validation failure for {:?}, stderr was:\n{stderr}",
args
);
assert_repo_clean_and_files_unchanged(
dir.path(),
&before,
&format!("legacy read-only command {:?}", args),
)?;
}
Ok(())
}