Files
huskies/server/src/agents/pool/start/tests_selection.rs
T

320 lines
9.1 KiB
Rust
Raw Normal View History

//! 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("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("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(&current).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(&current).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("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(
"368_story_test",
"2_current",
Some("Test Story"),
Some("coder-opus"),
None,
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}"
);
}