story-kit: merge 203_story_move_story_to_current_before_checking_agent_availability_in_start_agent
This commit is contained in:
@@ -352,6 +352,13 @@ impl AgentPool {
|
|||||||
let event_log: Arc<Mutex<Vec<AgentEvent>>> = Arc::new(Mutex::new(Vec::new()));
|
let event_log: Arc<Mutex<Vec<AgentEvent>>> = Arc::new(Mutex::new(Vec::new()));
|
||||||
let log_session_id = uuid::Uuid::new_v4().to_string();
|
let log_session_id = uuid::Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
// Move story from upcoming/ to current/ before checking agent
|
||||||
|
// availability so that auto_assign_available_work can pick it up even
|
||||||
|
// when all coders are currently busy (story 203). This is idempotent:
|
||||||
|
// if the story is already in 2_current/ or a later stage, the call is
|
||||||
|
// a no-op.
|
||||||
|
move_story_to_current(project_root, story_id)?;
|
||||||
|
|
||||||
// Atomically resolve agent name, check availability, and register as
|
// Atomically resolve agent name, check availability, and register as
|
||||||
// Pending. When `agent_name` is `None` the first idle coder is
|
// Pending. When `agent_name` is `None` the first idle coder is
|
||||||
// selected inside the lock so no TOCTOU race can occur between the
|
// selected inside the lock so no TOCTOU race can occur between the
|
||||||
@@ -376,8 +383,9 @@ impl AgentPool {
|
|||||||
.any(|a| agent_config_stage(a) == PipelineStage::Coder)
|
.any(|a| agent_config_stage(a) == PipelineStage::Coder)
|
||||||
{
|
{
|
||||||
format!(
|
format!(
|
||||||
"All coder agents are busy; story '{story_id}' will be \
|
"All coder agents are busy; story '{story_id}' has been \
|
||||||
picked up when one becomes available"
|
queued in work/2_current/ and will be auto-assigned when \
|
||||||
|
one becomes available"
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
"No coder agent configured. Specify an agent_name explicitly."
|
"No coder agent configured. Specify an agent_name explicitly."
|
||||||
@@ -467,9 +475,6 @@ impl AgentPool {
|
|||||||
status: "pending".to_string(),
|
status: "pending".to_string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Move story from upcoming/ to current/ and auto-commit before creating the worktree.
|
|
||||||
move_story_to_current(project_root, story_id)?;
|
|
||||||
|
|
||||||
// Extract inactivity timeout from the agent config before cloning config.
|
// Extract inactivity timeout from the agent config before cloning config.
|
||||||
let inactivity_timeout_secs = config
|
let inactivity_timeout_secs = config
|
||||||
.find_agent(&resolved_name)
|
.find_agent(&resolved_name)
|
||||||
@@ -6503,6 +6508,142 @@ stage = "coder"
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Story 203: when all coders are busy the story file must be moved from
|
||||||
|
/// 1_upcoming/ to 2_current/ so that auto_assign_available_work can pick
|
||||||
|
/// it up once a coder finishes.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn start_agent_moves_story_to_current_when_coders_busy() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let sk = tmp.path().join(".story_kit");
|
||||||
|
let upcoming = sk.join("work/1_upcoming");
|
||||||
|
std::fs::create_dir_all(&upcoming).unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
sk.join("project.toml"),
|
||||||
|
r#"
|
||||||
|
[[agent]]
|
||||||
|
name = "coder-1"
|
||||||
|
stage = "coder"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
// Place the story in 1_upcoming/.
|
||||||
|
std::fs::write(
|
||||||
|
upcoming.join("story-3.md"),
|
||||||
|
"---\nname: Story 3\n---\n",
|
||||||
|
)
|
||||||
|
.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-3", None, None)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Should fail because all coders are busy.
|
||||||
|
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}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Story must have been moved to 2_current/.
|
||||||
|
let current_path = sk.join("work/2_current/story-3.md");
|
||||||
|
assert!(
|
||||||
|
current_path.exists(),
|
||||||
|
"story should be in 2_current/ after busy error, but was not"
|
||||||
|
);
|
||||||
|
let upcoming_path = upcoming.join("story-3.md");
|
||||||
|
assert!(
|
||||||
|
!upcoming_path.exists(),
|
||||||
|
"story should no longer be in 1_upcoming/"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Story 203: auto_assign_available_work must detect a story in 2_current/
|
||||||
|
/// with no active agent and start an agent for it.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn auto_assign_picks_up_story_queued_in_current() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let sk = tmp.path().join(".story_kit");
|
||||||
|
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();
|
||||||
|
// Place the story in 2_current/ (simulating the "queued" state).
|
||||||
|
std::fs::write(
|
||||||
|
current.join("story-3.md"),
|
||||||
|
"---\nname: Story 3\n---\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let pool = AgentPool::new_test(3001);
|
||||||
|
// No agents are running — coder-1 is free.
|
||||||
|
|
||||||
|
// auto_assign will try to call start_agent, which will attempt to create
|
||||||
|
// a worktree (will fail without a git repo) — that is fine. We only need
|
||||||
|
// to verify the agent is registered as Pending before the background
|
||||||
|
// task eventually fails.
|
||||||
|
pool.auto_assign_available_work(tmp.path()).await;
|
||||||
|
|
||||||
|
let agents = pool.agents.lock().unwrap();
|
||||||
|
let has_pending = agents.values().any(|a| {
|
||||||
|
a.agent_name == "coder-1"
|
||||||
|
&& matches!(a.status, AgentStatus::Pending | AgentStatus::Running)
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
has_pending,
|
||||||
|
"auto_assign should have started coder-1 for story-3, but pool is empty"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Story 203: if a story is already in 2_current/ or later, start_agent
|
||||||
|
/// must not fail — the move is a no-op.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn start_agent_story_already_in_current_is_noop() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let sk = tmp.path().join(".story_kit");
|
||||||
|
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();
|
||||||
|
// Place the story directly in 2_current/.
|
||||||
|
std::fs::write(
|
||||||
|
current.join("story-5.md"),
|
||||||
|
"---\nname: Story 5\n---\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let pool = AgentPool::new_test(3001);
|
||||||
|
|
||||||
|
// start_agent should attempt to assign coder-1 (no infra, so it will
|
||||||
|
// fail for git reasons), but must NOT fail due to the story already
|
||||||
|
// being in 2_current/.
|
||||||
|
let result = pool
|
||||||
|
.start_agent(tmp.path(), "story-5", None, None)
|
||||||
|
.await;
|
||||||
|
match result {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
assert!(
|
||||||
|
!e.contains("Failed to move"),
|
||||||
|
"should not fail on idempotent move, got: {e}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn start_agent_explicit_name_unchanged_when_busy() {
|
async fn start_agent_explicit_name_unchanged_when_busy() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user