huskies: merge 683_refactor_decompose_server_src_agents_pool_start_mod_rs_1329_lines
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
//! 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(¤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("368_story_test", story_content);
|
||||
|
||||
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}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user