use super::*; // ── is_config_file ──────────────────────────────────────────────────────── #[test] fn is_config_file_identifies_root_project_toml() { let git_root = PathBuf::from("/proj"); let config = git_root.join(".huskies").join("project.toml"); assert!(is_config_file(&config, &git_root)); } #[test] fn is_config_file_identifies_root_agents_toml() { let git_root = PathBuf::from("/proj"); let agents = git_root.join(".huskies").join("agents.toml"); assert!(is_config_file(&agents, &git_root)); } #[test] fn is_config_file_rejects_worktree_copies() { let git_root = PathBuf::from("/proj"); // project.toml inside a worktree must NOT be treated as the root config. let worktree_config = PathBuf::from("/proj/.huskies/worktrees/42_story_foo/.huskies/project.toml"); assert!(!is_config_file(&worktree_config, &git_root)); } #[test] fn is_config_file_rejects_other_files() { let git_root = PathBuf::from("/proj"); // Random files must not match. assert!(!is_config_file( &PathBuf::from("/proj/.huskies/work/2_current/42_story_foo.md"), &git_root )); assert!(!is_config_file( &PathBuf::from("/proj/.huskies/README.md"), &git_root )); } #[test] fn is_config_file_rejects_wrong_root() { let git_root = PathBuf::from("/proj"); let other_root_config = PathBuf::from("/other/.huskies/project.toml"); assert!(!is_config_file(&other_root_config, &git_root)); } // ── stage_metadata ──────────────────────────────────────────────────────── #[test] fn stage_metadata_returns_correct_actions() { let (action, msg) = stage_metadata("2_current", "42_story_foo").unwrap(); assert_eq!(action, "start"); assert_eq!(msg, "huskies: start 42_story_foo"); let (action, msg) = stage_metadata("5_done", "42_story_foo").unwrap(); assert_eq!(action, "done"); assert_eq!(msg, "huskies: done 42_story_foo"); let (action, msg) = stage_metadata("6_archived", "42_story_foo").unwrap(); assert_eq!(action, "accept"); assert_eq!(msg, "huskies: accept 42_story_foo"); assert!(stage_metadata("unknown", "id").is_none()); } // ── sweep_done_to_archived (CRDT-based) ───────────────────────────────── // // The sweep function reads from `read_all_typed()` and checks // `Stage::Done { merged_at, .. }`. Items created via // `write_item_with_content("5_done")` project `merged_at = Utc::now()`, // so we test with Duration::ZERO to sweep immediately and with a long // retention to verify items are kept. No filesystem access is involved. #[test] fn sweep_moves_old_items_to_archived() { crate::db::ensure_content_store(); crate::db::write_item_with_content("9880_story_sweep_old", "5_done", "---\nname: old\n---\n"); // With ZERO retention, any Done item should be swept. sweep_done_to_archived(Duration::ZERO); // Verify the item was moved to 6_archived in the CRDT. let items = crate::pipeline_state::read_all_typed(); let item = items .iter() .find(|i| i.story_id.0 == "9880_story_sweep_old"); assert!( item.is_some_and(|i| matches!(i.stage, crate::pipeline_state::Stage::Archived { .. })), "item should be archived after sweep" ); } #[test] fn sweep_keeps_recent_items_in_done() { crate::db::ensure_content_store(); crate::db::write_item_with_content("9881_story_sweep_new", "5_done", "---\nname: new\n---\n"); // With a very long retention, the item (merged_at ≈ now) should stay. sweep_done_to_archived(Duration::from_secs(999_999)); let items = crate::pipeline_state::read_all_typed(); let item = items .iter() .find(|i| i.story_id.0 == "9881_story_sweep_new"); assert!( item.is_some_and(|i| matches!(i.stage, crate::pipeline_state::Stage::Done { .. })), "item should remain in Done with long retention" ); } #[test] fn sweep_respects_custom_retention() { crate::db::ensure_content_store(); crate::db::write_item_with_content( "9882_story_sweep_custom", "5_done", "---\nname: custom\n---\n", ); // With ZERO retention, sweep should promote. sweep_done_to_archived(Duration::ZERO); let items = crate::pipeline_state::read_all_typed(); let item = items .iter() .find(|i| i.story_id.0 == "9882_story_sweep_custom"); assert!( item.is_some_and(|i| matches!(i.stage, crate::pipeline_state::Stage::Archived { .. })), "item should be archived with zero retention" ); } /// Prove that the sweep reads `merged_at` from the CRDT (not `Utc::now()`). /// /// This test sets `merged_at` to 10 seconds in the past and uses a 5-second /// retention. If the sweep were still using `Utc::now()` as the start time /// (the original bug), the elapsed time would be ~0 and the item would NOT /// be swept. With the fix, the item is swept because 10s > 5s retention. #[test] fn sweep_uses_crdt_merged_at_not_utc_now() { crate::db::ensure_content_store(); let ten_seconds_ago = (chrono::Utc::now() - chrono::Duration::seconds(10)).timestamp() as f64; // Write item in 5_done with an explicit past merged_at timestamp. crate::crdt_state::write_item( "9883_story_sweep_merged_at", "5_done", Some("merged_at test"), None, None, None, None, None, None, Some(ten_seconds_ago), ); // 5-second retention: item is 10s old → should be swept. sweep_done_to_archived(Duration::from_secs(5)); let items = crate::pipeline_state::read_all_typed(); let item = items .iter() .find(|i| i.story_id.0 == "9883_story_sweep_merged_at"); assert!( item.is_some_and(|i| matches!(i.stage, crate::pipeline_state::Stage::Archived { .. })), "item with merged_at 10s ago should be archived with 5s retention" ); } /// Prove that an item with merged_at NEWER than done_retention is NOT swept. #[test] fn sweep_keeps_item_newer_than_retention() { crate::db::ensure_content_store(); let one_second_ago = (chrono::Utc::now() - chrono::Duration::seconds(1)).timestamp() as f64; crate::crdt_state::write_item( "9884_story_sweep_recent", "5_done", Some("recent merged_at test"), None, None, None, None, None, None, Some(one_second_ago), ); // 1-hour retention: item is only 1s old → should NOT be swept. sweep_done_to_archived(Duration::from_secs(3600)); let items = crate::pipeline_state::read_all_typed(); let item = items .iter() .find(|i| i.story_id.0 == "9884_story_sweep_recent"); assert!( item.is_some_and(|i| matches!(i.stage, crate::pipeline_state::Stage::Done { .. })), "item with merged_at 1s ago should stay in Done with 1-hour retention" ); }