huskies: merge 877
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
//! Assign-and-start: unified service for pinning an agent to a story and starting it.
|
||||
//!
|
||||
//! Both the Matrix `assign` command and the MCP `start_agent` tool delegate
|
||||
//! here when an explicit `agent_name` is provided, ensuring that the CRDT
|
||||
//! `agent` register is always written before the agent is spawned. This
|
||||
//! eliminates the asymmetry where the chat path wrote YAML + CRDT while the
|
||||
//! MCP path only called `start_agent`.
|
||||
|
||||
use crate::agents::{AgentInfo, AgentPool};
|
||||
use std::path::Path;
|
||||
|
||||
/// Pin an agent to a story in the CRDT and immediately start it.
|
||||
///
|
||||
/// Writes `agent_name` into the typed CRDT `agent` register for `story_id`,
|
||||
/// then delegates to [`AgentPool::start_agent`] with the explicit name.
|
||||
///
|
||||
/// Callers that need to stop an already-running agent first (e.g. the Matrix
|
||||
/// `assign` command replacing a running coder) must do so before calling this
|
||||
/// function; the pool's concurrency guard will reject a duplicate start.
|
||||
///
|
||||
/// Returns the [`AgentInfo`] for the newly started agent, or an error string.
|
||||
pub async fn assign_and_start(
|
||||
story_id: &str,
|
||||
agent_name: &str,
|
||||
project_root: &Path,
|
||||
agents: &AgentPool,
|
||||
) -> Result<AgentInfo, String> {
|
||||
crate::crdt_state::set_agent(story_id, Some(agent_name));
|
||||
agents
|
||||
.start_agent(project_root, story_id, Some(agent_name), None, None)
|
||||
.await
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::agents::AgentPool;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn make_agents() -> Arc<AgentPool> {
|
||||
Arc::new(AgentPool::new_test(3000))
|
||||
}
|
||||
|
||||
/// Regression test (story 877, AC4): assigning via the Matrix chat path
|
||||
/// and via the MCP `start_agent` path both call `assign_and_start`, which
|
||||
/// writes the typed CRDT `agent` register before spawning. This test
|
||||
/// verifies the CRDT write is unconditional even when the pool call fails
|
||||
/// (no git repo in the test environment).
|
||||
#[tokio::test]
|
||||
async fn assign_and_start_writes_crdt_agent_register_before_pool_call() {
|
||||
crate::crdt_state::init_for_test();
|
||||
let story_id = "8770_story_assign_regression_crdt";
|
||||
|
||||
// Seed the CRDT so set_agent can find the item.
|
||||
crate::crdt_state::write_item(
|
||||
story_id,
|
||||
"2_current",
|
||||
Some("Assign Regression"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let agents = make_agents();
|
||||
|
||||
// The pool call will fail (no git repo / project config), but the CRDT
|
||||
// write must succeed before it is attempted.
|
||||
let _ = assign_and_start(story_id, "coder-opus", tmp.path(), &agents).await;
|
||||
|
||||
let dump = crate::crdt_state::dump_crdt_state(Some(story_id));
|
||||
let item = dump
|
||||
.items
|
||||
.iter()
|
||||
.find(|i| i.story_id.as_deref() == Some(story_id))
|
||||
.expect("item must be in CRDT");
|
||||
assert_eq!(
|
||||
item.agent.as_deref(),
|
||||
Some("coder-opus"),
|
||||
"CRDT agent register must be set to coder-opus after assign_and_start"
|
||||
);
|
||||
}
|
||||
|
||||
/// Both paths (Matrix and MCP) that call `assign_and_start` leave the same
|
||||
/// CRDT register value, regardless of which path is used.
|
||||
#[tokio::test]
|
||||
async fn assign_and_start_same_crdt_state_from_both_paths() {
|
||||
crate::crdt_state::init_for_test();
|
||||
let story_id_a = "8771_story_assign_path_a";
|
||||
let story_id_b = "8772_story_assign_path_b";
|
||||
|
||||
for sid in &[story_id_a, story_id_b] {
|
||||
crate::crdt_state::write_item(
|
||||
sid,
|
||||
"2_current",
|
||||
Some("Path Test"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let agents = make_agents();
|
||||
|
||||
// Simulate Matrix path: call assign_and_start directly (as handle_assign does).
|
||||
let _ = assign_and_start(story_id_a, "coder-sonnet", tmp.path(), &agents).await;
|
||||
|
||||
// Simulate MCP path: call assign_and_start directly (as tool_start_agent does).
|
||||
let _ = assign_and_start(story_id_b, "coder-sonnet", tmp.path(), &agents).await;
|
||||
|
||||
// Both must leave the same CRDT agent register value.
|
||||
for sid in &[story_id_a, story_id_b] {
|
||||
let dump = crate::crdt_state::dump_crdt_state(Some(sid));
|
||||
let item = dump
|
||||
.items
|
||||
.iter()
|
||||
.find(|i| i.story_id.as_deref() == Some(*sid))
|
||||
.expect("item must be in CRDT");
|
||||
assert_eq!(
|
||||
item.agent.as_deref(),
|
||||
Some("coder-sonnet"),
|
||||
"CRDT agent register must match for story {sid}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
//! Work-item service — cross-cutting domain logic that applies to all pipeline
|
||||
//! work-item types (stories, bugs, spikes, refactors).
|
||||
|
||||
/// Assign-and-start: unified assign + spawn for both chat and MCP paths.
|
||||
pub mod assign;
|
||||
/// Canonical delete sequence for any work item type.
|
||||
pub mod delete;
|
||||
|
||||
pub use assign::assign_and_start;
|
||||
|
||||
Reference in New Issue
Block a user