use std::path::PathBuf; use domain::{ IssueActor, ProjectPath, Sprint, SprintId, SprintOrder, SprintStatus, SprintStore, SprintStoreError, }; use infrastructure::FsSprintStore; use uuid::Uuid; struct TempDir(PathBuf); impl TempDir { fn new() -> Self { let path = std::env::temp_dir().join(format!("idea-sprints-{}", Uuid::new_v4())); std::fs::create_dir_all(&path).unwrap(); Self(path) } fn root(&self) -> ProjectPath { ProjectPath::new(self.0.to_string_lossy().to_string()).unwrap() } fn child(&self, rel: &str) -> PathBuf { self.0.join(rel) } } impl Drop for TempDir { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.0); } } fn sid(n: u128) -> SprintId { SprintId::from_uuid(Uuid::from_u128(n)) } fn sprint(id: SprintId, order: u32, name: &str) -> Sprint { Sprint::new( id, SprintOrder::new(order).unwrap(), name, None, IssueActor::User, 1_000, ) .unwrap() } #[tokio::test] async fn sprint_store_writes_markdown_doc_and_index() { let tmp = TempDir::new(); let root = tmp.root(); let store = FsSprintStore::new(); let sprint = sprint(sid(1), 1, "Delivery"); store.create(&root, &sprint).await.unwrap(); let sprint_md = std::fs::read_to_string(tmp.child(&format!(".ideai/sprints/{}/sprint.md", sprint.id))) .unwrap(); let index = std::fs::read_to_string(tmp.child(".ideai/sprints/index.json")).unwrap(); assert!(sprint_md.starts_with("---\n")); assert!(sprint_md.contains("name: \"Delivery\"")); assert!(index.contains("\"name\": \"Delivery\"")); let loaded = store.get(&root, sprint.id).await.unwrap(); assert_eq!(loaded.name, "Delivery"); assert_eq!(loaded.order.get(), 1); assert_eq!(loaded.status, SprintStatus::Planned); } #[tokio::test] async fn sprint_store_update_checks_expected_version() { let tmp = TempDir::new(); let root = tmp.root(); let store = FsSprintStore::new(); let original = sprint(sid(2), 1, "Plan"); store.create(&root, &original).await.unwrap(); let updated = original .clone() .mutate(IssueActor::System, 2_000, |sprint| { sprint.name = "Build".to_owned(); }) .unwrap(); let err = store .update(&root, &updated, updated.version) .await .unwrap_err(); assert!(matches!(err, SprintStoreError::VersionConflict { .. })); store .update(&root, &updated, original.version) .await .unwrap(); let loaded = store.get(&root, updated.id).await.unwrap(); assert_eq!(loaded.name, "Build"); assert_eq!(loaded.version.get(), 2); } #[tokio::test] async fn sprint_store_lists_by_order_and_deletes() { let tmp = TempDir::new(); let root = tmp.root(); let store = FsSprintStore::new(); let first = sprint(sid(10), 2, "Second"); let second = sprint(sid(11), 1, "First"); store.create(&root, &first).await.unwrap(); store.create(&root, &second).await.unwrap(); let rows = store.list(&root).await.unwrap(); assert_eq!( rows.iter().map(|row| row.name.as_str()).collect::>(), vec!["First", "Second"] ); store.delete(&root, first.id).await.unwrap(); assert!(matches!( store.get(&root, first.id).await.unwrap_err(), SprintStoreError::NotFound )); let rows = store.list(&root).await.unwrap(); assert_eq!(rows.len(), 1); assert_eq!(rows[0].id, second.id); }