story-kit: merge 173_bug_pipeline_board_lozenges_dont_update_on_agent_state_changes
This commit is contained in:
@@ -587,6 +587,7 @@ impl AgentPool {
|
|||||||
&sid,
|
&sid,
|
||||||
&aname,
|
&aname,
|
||||||
session_id,
|
session_id,
|
||||||
|
watcher_tx_clone.clone(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
Self::notify_agent_state_changed(&watcher_tx_clone);
|
Self::notify_agent_state_changed(&watcher_tx_clone);
|
||||||
@@ -2002,6 +2003,7 @@ async fn run_server_owned_completion(
|
|||||||
story_id: &str,
|
story_id: &str,
|
||||||
agent_name: &str,
|
agent_name: &str,
|
||||||
session_id: Option<String>,
|
session_id: Option<String>,
|
||||||
|
watcher_tx: broadcast::Sender<WatcherEvent>,
|
||||||
) {
|
) {
|
||||||
let key = composite_key(story_id, agent_name);
|
let key = composite_key(story_id, agent_name);
|
||||||
|
|
||||||
@@ -2094,7 +2096,7 @@ async fn run_server_owned_completion(
|
|||||||
|
|
||||||
// Advance the pipeline state machine in a background task.
|
// Advance the pipeline state machine in a background task.
|
||||||
// Uses a non-async helper to break the opaque type cycle.
|
// Uses a non-async helper to break the opaque type cycle.
|
||||||
spawn_pipeline_advance(Arc::clone(agents), port, story_id, agent_name);
|
spawn_pipeline_advance(Arc::clone(agents), port, story_id, agent_name, watcher_tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn pipeline advancement as a background task.
|
/// Spawn pipeline advancement as a background task.
|
||||||
@@ -2106,11 +2108,11 @@ fn spawn_pipeline_advance(
|
|||||||
port: u16,
|
port: u16,
|
||||||
story_id: &str,
|
story_id: &str,
|
||||||
agent_name: &str,
|
agent_name: &str,
|
||||||
|
watcher_tx: broadcast::Sender<WatcherEvent>,
|
||||||
) {
|
) {
|
||||||
let sid = story_id.to_string();
|
let sid = story_id.to_string();
|
||||||
let aname = agent_name.to_string();
|
let aname = agent_name.to_string();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let (watcher_tx, _) = broadcast::channel(16);
|
|
||||||
let pool = AgentPool {
|
let pool = AgentPool {
|
||||||
agents,
|
agents,
|
||||||
port,
|
port,
|
||||||
@@ -3667,7 +3669,7 @@ mod tests {
|
|||||||
// Subscribe before calling so we can check if Done event was emitted.
|
// Subscribe before calling so we can check if Done event was emitted.
|
||||||
let mut rx = pool.subscribe("s10", "coder-1").unwrap();
|
let mut rx = pool.subscribe("s10", "coder-1").unwrap();
|
||||||
|
|
||||||
run_server_owned_completion(&pool.agents, pool.port, "s10", "coder-1", Some("sess-1".to_string()))
|
run_server_owned_completion(&pool.agents, pool.port, "s10", "coder-1", Some("sess-1".to_string()), pool.watcher_tx.clone())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Status should remain Completed (unchanged) — no gate re-run.
|
// Status should remain Completed (unchanged) — no gate re-run.
|
||||||
@@ -3707,7 +3709,7 @@ mod tests {
|
|||||||
|
|
||||||
let mut rx = pool.subscribe("s11", "coder-1").unwrap();
|
let mut rx = pool.subscribe("s11", "coder-1").unwrap();
|
||||||
|
|
||||||
run_server_owned_completion(&pool.agents, pool.port, "s11", "coder-1", Some("sess-2".to_string()))
|
run_server_owned_completion(&pool.agents, pool.port, "s11", "coder-1", Some("sess-2".to_string()), pool.watcher_tx.clone())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Completion report should exist (gates were run, though they may fail
|
// Completion report should exist (gates were run, though they may fail
|
||||||
@@ -3760,7 +3762,7 @@ mod tests {
|
|||||||
repo.to_path_buf(),
|
repo.to_path_buf(),
|
||||||
);
|
);
|
||||||
|
|
||||||
run_server_owned_completion(&pool.agents, pool.port, "s12", "coder-1", None)
|
run_server_owned_completion(&pool.agents, pool.port, "s12", "coder-1", None, pool.watcher_tx.clone())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let agents = pool.agents.lock().unwrap();
|
let agents = pool.agents.lock().unwrap();
|
||||||
@@ -3784,7 +3786,7 @@ mod tests {
|
|||||||
async fn server_owned_completion_nonexistent_agent_is_noop() {
|
async fn server_owned_completion_nonexistent_agent_is_noop() {
|
||||||
let pool = AgentPool::new_test(3001);
|
let pool = AgentPool::new_test(3001);
|
||||||
// Should not panic or error — just silently return.
|
// Should not panic or error — just silently return.
|
||||||
run_server_owned_completion(&pool.agents, pool.port, "nonexistent", "bot", None)
|
run_server_owned_completion(&pool.agents, pool.port, "nonexistent", "bot", None, pool.watcher_tx.clone())
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6383,4 +6385,87 @@ theirs
|
|||||||
"child_killers should be cleared after kill_all_children"
|
"child_killers should be cleared after kill_all_children"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Bug 173: pipeline advance sends AgentStateChanged via real watcher_tx ─
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn pipeline_advance_sends_agent_state_changed_to_watcher_tx() {
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let root = tmp.path();
|
||||||
|
|
||||||
|
// Set up story in 2_current/
|
||||||
|
let current = root.join(".story_kit/work/2_current");
|
||||||
|
fs::create_dir_all(¤t).unwrap();
|
||||||
|
fs::write(current.join("173_story_test.md"), "test").unwrap();
|
||||||
|
// Ensure 3_qa/ exists for the move target
|
||||||
|
fs::create_dir_all(root.join(".story_kit/work/3_qa")).unwrap();
|
||||||
|
// Ensure 1_upcoming/ exists (start_agent calls move_story_to_current)
|
||||||
|
fs::create_dir_all(root.join(".story_kit/work/1_upcoming")).unwrap();
|
||||||
|
|
||||||
|
// Write a project.toml with a qa agent so start_agent can resolve it.
|
||||||
|
fs::create_dir_all(root.join(".story_kit")).unwrap();
|
||||||
|
fs::write(
|
||||||
|
root.join(".story_kit/project.toml"),
|
||||||
|
r#"
|
||||||
|
[[agent]]
|
||||||
|
name = "coder-1"
|
||||||
|
role = "Coder"
|
||||||
|
command = "echo"
|
||||||
|
args = ["noop"]
|
||||||
|
prompt = "test"
|
||||||
|
stage = "coder"
|
||||||
|
|
||||||
|
[[agent]]
|
||||||
|
name = "qa"
|
||||||
|
role = "QA"
|
||||||
|
command = "echo"
|
||||||
|
args = ["noop"]
|
||||||
|
prompt = "test"
|
||||||
|
stage = "qa"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let pool = AgentPool::new_test(3001);
|
||||||
|
pool.inject_test_agent_with_completion(
|
||||||
|
"173_story_test",
|
||||||
|
"coder-1",
|
||||||
|
AgentStatus::Completed,
|
||||||
|
root.to_path_buf(),
|
||||||
|
CompletionReport {
|
||||||
|
summary: "done".to_string(),
|
||||||
|
gates_passed: true,
|
||||||
|
gate_output: String::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Subscribe to the watcher channel BEFORE the pipeline advance.
|
||||||
|
let mut rx = pool.watcher_tx.subscribe();
|
||||||
|
|
||||||
|
// Call pipeline advance directly. This will:
|
||||||
|
// 1. Move the story to 3_qa/
|
||||||
|
// 2. Start the QA agent (which calls notify_agent_state_changed)
|
||||||
|
// Note: the actual agent process will fail (no real worktree), but the
|
||||||
|
// agent insertion and notification happen before the background spawn.
|
||||||
|
pool.run_pipeline_advance_for_completed_agent("173_story_test", "coder-1")
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// The pipeline advance should have sent AgentStateChanged events via
|
||||||
|
// the pool's watcher_tx (not a dummy channel). Collect all events.
|
||||||
|
let mut got_agent_state_changed = false;
|
||||||
|
while let Ok(evt) = rx.try_recv() {
|
||||||
|
if matches!(evt, WatcherEvent::AgentStateChanged) {
|
||||||
|
got_agent_state_changed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
got_agent_state_changed,
|
||||||
|
"pipeline advance should send AgentStateChanged through the real watcher_tx \
|
||||||
|
(bug 173: lozenges must update when agents are assigned during pipeline advance)"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user