//! Basic pipeline advance tests. use super::super::super::AgentPool; use crate::agents::CompletionReport; use crate::io::watcher::WatcherEvent; // ── pipeline advance tests ──────────────────────────────────────────────── #[tokio::test] async fn pipeline_advance_coder_gates_pass_server_qa_moves_to_merge() { use std::fs; let tmp = tempfile::tempdir().unwrap(); let root = tmp.path(); // Set up story in 2_current/ (no qa frontmatter → uses project default "server"). // Use a unique high-numbered ID to avoid collision with the agent_qa test. let current = root.join(".huskies/work/2_current"); fs::create_dir_all(¤t).unwrap(); fs::write(current.join("9908_story_server_qa.md"), "test").unwrap(); crate::db::ensure_content_store(); crate::db::write_content("9908_story_server_qa", "test"); let pool = AgentPool::new_test(3001); pool.run_pipeline_advance( "9908_story_server_qa", "coder-1", CompletionReport { summary: "done".to_string(), gates_passed: true, gate_output: String::new(), }, Some(root.to_path_buf()), None, false, None, ) .await; // With default qa: server, story skips QA and goes straight to 4_merge/ // Lifecycle moves now update the content store, not the filesystem. assert!( crate::db::read_content("9908_story_server_qa").is_some(), "story should still exist in content store after move to merge" ); } #[tokio::test] async fn pipeline_advance_coder_gates_pass_agent_qa_moves_to_qa() { use std::fs; let tmp = tempfile::tempdir().unwrap(); let root = tmp.path(); // Set up story in 2_current/ with qa: agent frontmatter. // Use a unique high-numbered ID to avoid collision with the server_qa test. let current = root.join(".huskies/work/2_current"); fs::create_dir_all(¤t).unwrap(); fs::write( current.join("9909_story_agent_qa.md"), "---\nname: Test\nqa: agent\n---\ntest", ) .unwrap(); crate::db::ensure_content_store(); crate::db::write_content( "9909_story_agent_qa", "---\nname: Test\nqa: agent\n---\ntest", ); let pool = AgentPool::new_test(3001); pool.run_pipeline_advance( "9909_story_agent_qa", "coder-1", CompletionReport { summary: "done".to_string(), gates_passed: true, gate_output: String::new(), }, Some(root.to_path_buf()), None, false, None, ) .await; // With qa: agent, story should move to 3_qa/ // Lifecycle moves now update the content store, not the filesystem. assert!( crate::db::read_content("9909_story_agent_qa").is_some(), "story should still exist in content store after move to qa" ); } #[tokio::test] async fn pipeline_advance_qa_gates_pass_moves_story_to_merge() { use std::fs; let tmp = tempfile::tempdir().unwrap(); let root = tmp.path(); // Set up story in 3_qa/ let qa_dir = root.join(".huskies/work/3_qa"); fs::create_dir_all(&qa_dir).unwrap(); // qa: server so the story skips human review and goes straight to merge. fs::write( qa_dir.join("51_story_test.md"), "---\nname: Test\nqa: server\n---\ntest", ) .unwrap(); crate::db::ensure_content_store(); crate::db::write_content("51_story_test", "---\nname: Test\nqa: server\n---\ntest"); let pool = AgentPool::new_test(3001); pool.run_pipeline_advance( "51_story_test", "qa", CompletionReport { summary: "QA done".to_string(), gates_passed: true, gate_output: String::new(), }, Some(root.to_path_buf()), None, false, None, ) .await; // Story should have moved to 4_merge/ // Lifecycle moves now update the content store, not the filesystem. assert!( crate::db::read_content("51_story_test").is_some(), "story should still exist in content store after move to merge" ); } #[tokio::test] async fn pipeline_advance_supervisor_does_not_advance() { use std::fs; let tmp = tempfile::tempdir().unwrap(); let root = tmp.path(); let current = root.join(".huskies/work/2_current"); fs::create_dir_all(¤t).unwrap(); fs::write(current.join("52_story_test.md"), "test").unwrap(); let pool = AgentPool::new_test(3001); pool.run_pipeline_advance( "52_story_test", "supervisor", CompletionReport { summary: "supervised".to_string(), gates_passed: true, gate_output: String::new(), }, Some(root.to_path_buf()), None, false, None, ) .await; // Story should NOT have moved (supervisors don't advance pipeline) assert!( current.join("52_story_test.md").exists(), "story should still be in 2_current/ for supervisor" ); } #[tokio::test] async fn pipeline_advance_sends_agent_state_changed_to_watcher_tx() { use std::fs; let tmp = tempfile::tempdir().unwrap(); let root = tmp.path(); // Seed story via CRDT (the only source of truth). crate::db::ensure_content_store(); crate::db::write_item_with_content("173_story_test", "2_current", "---\nname: test\n---\n"); // Write a project.toml with a qa agent so start_agent can resolve it. fs::create_dir_all(root.join(".huskies")).unwrap(); fs::write( root.join(".huskies/project.toml"), r#" default_qa = "agent" [[agent]] name = "coder-1" role = "Coder" command = "echo" args = ["noop"] prompt = "test" stage = "coder" [[agent]] name = "qa" role = "QA" command = "echo" args = ["noop"] prompt = "test" stage = "qa" "#, ) .unwrap(); let pool = AgentPool::new_test(3001); // Subscribe to the watcher channel BEFORE the pipeline advance. let mut rx = pool.watcher_tx.subscribe(); pool.run_pipeline_advance( "173_story_test", "coder-1", CompletionReport { summary: "done".to_string(), gates_passed: true, gate_output: String::new(), }, Some(root.to_path_buf()), None, false, None, ) .await; // The pipeline advance should have sent AgentStateChanged events via // the pool's watcher_tx (not a dummy channel). Collect all events. let mut got_agent_state_changed = false; while let Ok(evt) = rx.try_recv() { if matches!(evt, WatcherEvent::AgentStateChanged) { got_agent_state_changed = true; break; } } assert!( got_agent_state_changed, "pipeline advance should send AgentStateChanged through the real watcher_tx \ (bug 173: lozenges must update when agents are assigned during pipeline advance)" ); }