story-kit: merge 173_bug_pipeline_board_lozenges_dont_update_on_agent_state_changes

This commit is contained in:
Dave
2026-02-24 23:57:07 +00:00
parent d55d4765a9
commit 51fad34a6a

View File

@@ -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(&current).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)"
);
}
} }