//! Tests for basic coder selection and front-matter agent preference in //! `AgentPool::start_agent`. use super::super::AgentPool; use crate::agents::{AgentEvent, AgentStatus}; #[tokio::test] async fn start_agent_auto_selects_second_coder_when_first_busy() { let tmp = tempfile::tempdir().unwrap(); let sk = tmp.path().join(".huskies"); std::fs::create_dir_all(&sk).unwrap(); std::fs::write( sk.join("project.toml"), r#" [[agent]] name = "supervisor" stage = "other" [[agent]] name = "coder-1" stage = "coder" [[agent]] name = "coder-2" stage = "coder" "#, ) .unwrap(); let pool = AgentPool::new_test(3001); pool.inject_test_agent("other-story", "coder-1", AgentStatus::Running); let result = pool .start_agent(tmp.path(), "42_my_story", None, None, None) .await; match result { Ok(info) => { assert_eq!(info.agent_name, "coder-2"); } Err(err) => { assert!( !err.contains("All coder agents are busy"), "should have selected coder-2 but got: {err}" ); assert!( !err.contains("No coder agent configured"), "should not fail on agent selection, got: {err}" ); } } } #[tokio::test] async fn start_agent_returns_busy_when_all_coders_occupied() { let tmp = tempfile::tempdir().unwrap(); let sk = tmp.path().join(".huskies"); std::fs::create_dir_all(&sk).unwrap(); std::fs::write( sk.join("project.toml"), r#" [[agent]] name = "coder-1" stage = "coder" [[agent]] name = "coder-2" stage = "coder" "#, ) .unwrap(); let pool = AgentPool::new_test(3001); pool.inject_test_agent("story-1", "coder-1", AgentStatus::Running); pool.inject_test_agent("story-2", "coder-2", AgentStatus::Pending); let result = pool .start_agent(tmp.path(), "story-3", None, None, None) .await; assert!(result.is_err()); let err = result.unwrap_err(); assert!( err.contains("All coder agents are busy"), "expected busy error, got: {err}" ); } #[tokio::test] async fn start_agent_moves_story_to_current_when_coders_busy() { let tmp = tempfile::tempdir().unwrap(); let sk = tmp.path().join(".huskies"); let backlog = sk.join("work/1_backlog"); std::fs::create_dir_all(&backlog).unwrap(); std::fs::write( sk.join("project.toml"), r#" [[agent]] name = "coder-1" stage = "coder" "#, ) .unwrap(); let story_content = "---\nname: Story 3\n---\n"; std::fs::write(backlog.join("story-3.md"), story_content).unwrap(); crate::db::ensure_content_store(); crate::db::write_content(crate::db::ContentKey::Story("story-3"), story_content); let pool = AgentPool::new_test(3001); pool.inject_test_agent("story-1", "coder-1", AgentStatus::Running); let result = pool .start_agent(tmp.path(), "story-3", None, None, None) .await; assert!(result.is_err()); let err = result.unwrap_err(); assert!( err.contains("All coder agents are busy"), "expected busy error, got: {err}" ); assert!( err.contains("queued in work/2_current/"), "expected story-to-current message, got: {err}" ); // The lifecycle function updates the content store (not the filesystem), // so verify the move via the DB. let content = crate::db::read_content(crate::db::ContentKey::Story("story-3")) .expect("story-3 should be in content store after move to current"); assert!( content.contains("name: Story 3"), "story-3 content should be preserved after move" ); } #[tokio::test] async fn start_agent_story_already_in_current_is_noop() { let tmp = tempfile::tempdir().unwrap(); let sk = tmp.path().join(".huskies"); let current = sk.join("work/2_current"); std::fs::create_dir_all(¤t).unwrap(); std::fs::write( sk.join("project.toml"), "[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n", ) .unwrap(); std::fs::write(current.join("story-5.md"), "---\nname: Story 5\n---\n").unwrap(); let pool = AgentPool::new_test(3001); let result = pool .start_agent(tmp.path(), "story-5", None, None, None) .await; match result { Ok(_) => {} Err(e) => { assert!( !e.contains("Failed to move"), "should not fail on idempotent move, got: {e}" ); } } } #[tokio::test] async fn start_agent_explicit_name_unchanged_when_busy() { let tmp = tempfile::tempdir().unwrap(); let sk = tmp.path().join(".huskies"); std::fs::create_dir_all(&sk).unwrap(); std::fs::write( sk.join("project.toml"), r#" [[agent]] name = "coder-1" stage = "coder" [[agent]] name = "coder-2" stage = "coder" "#, ) .unwrap(); let pool = AgentPool::new_test(3001); pool.inject_test_agent("story-1", "coder-1", AgentStatus::Running); let result = pool .start_agent(tmp.path(), "story-2", Some("coder-1"), None, None) .await; assert!(result.is_err()); let err = result.unwrap_err(); assert!( err.contains("coder-1") && err.contains("already running"), "expected explicit busy error, got: {err}" ); } // ── front matter agent preference (bug 379) ────────────────────────────── #[tokio::test] async fn start_agent_honours_front_matter_agent_when_idle() { let tmp = tempfile::tempdir().unwrap(); let sk = tmp.path().join(".huskies"); let backlog = sk.join("work/1_backlog"); std::fs::create_dir_all(&backlog).unwrap(); std::fs::write( sk.join("project.toml"), r#" [[agent]] name = "coder-sonnet" stage = "coder" [[agent]] name = "coder-opus" stage = "coder" "#, ) .unwrap(); // Story file with agent preference in front matter. std::fs::write( backlog.join("368_story_test.md"), "---\nname: Test Story\nagent: coder-opus\n---\n# Story 368\n", ) .unwrap(); let pool = AgentPool::new_test(3010); // coder-sonnet is busy so without front matter the auto-selection // would skip coder-opus and try something else. pool.inject_test_agent("other-story", "coder-sonnet", AgentStatus::Running); let result = pool .start_agent(tmp.path(), "368_story_test", None, None, None) .await; match result { Ok(info) => { assert_eq!( info.agent_name, "coder-opus", "should pick the front-matter preferred agent" ); } Err(err) => { // Allowed to fail for infrastructure reasons (no git repo), // but NOT due to agent selection ignoring the preference. assert!( !err.contains("All coder agents are busy"), "should not report busy when coder-opus is idle: {err}" ); assert!( !err.contains("coder-sonnet"), "should not have picked coder-sonnet: {err}" ); } } } #[tokio::test] async fn start_agent_returns_error_when_front_matter_agent_busy() { let tmp = tempfile::tempdir().unwrap(); let sk = tmp.path().join(".huskies"); let backlog = sk.join("work/1_backlog"); let current = sk.join("work/2_current"); std::fs::create_dir_all(&backlog).unwrap(); std::fs::create_dir_all(¤t).unwrap(); std::fs::write( sk.join("project.toml"), r#" [[agent]] name = "coder-sonnet" stage = "coder" [[agent]] name = "coder-opus" stage = "coder" "#, ) .unwrap(); let story_content = "---\nname: Test Story\nagent: coder-opus\n---\n# Story 368\n"; std::fs::write(backlog.join("368_story_test.md"), story_content).unwrap(); // Also write to the filesystem current dir and content store so that // start_agent reads the correct front matter even when another test has // left a stale entry for "368_story_test" in the global CRDT. std::fs::write(current.join("368_story_test.md"), story_content).unwrap(); crate::db::ensure_content_store(); crate::db::write_content( crate::db::ContentKey::Story("368_story_test"), story_content, ); // Story 929: agent pin comes from the CRDT register, not YAML. Seed it. crate::crdt_state::init_for_test(); crate::crdt_state::write_item_str( "368_story_test", "2_current", Some("Test Story"), Some("coder-opus"), None, None, None, None, None, ); let pool = AgentPool::new_test(3011); // Preferred agent is busy — should NOT fall back to coder-sonnet. pool.inject_test_agent("other-story", "coder-opus", AgentStatus::Running); let result = pool .start_agent(tmp.path(), "368_story_test", None, None, None) .await; assert!( result.is_err(), "expected error when preferred agent is busy" ); let err = result.unwrap_err(); assert!( err.contains("coder-opus"), "error should mention the preferred agent: {err}" ); assert!( err.contains("busy") || err.contains("queued"), "error should say agent is busy or story is queued: {err}" ); }