From 6a015d6202ea9d1844cec7e3a85fb95bbc2d955a Mon Sep 17 00:00:00 2001 From: dave Date: Wed, 13 May 2026 08:53:03 +0000 Subject: [PATCH] huskies: merge 953 --- .../pool/pipeline/advance/tests_regression.rs | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/server/src/agents/pool/pipeline/advance/tests_regression.rs b/server/src/agents/pool/pipeline/advance/tests_regression.rs index a42d0101..db292143 100644 --- a/server/src/agents/pool/pipeline/advance/tests_regression.rs +++ b/server/src/agents/pool/pipeline/advance/tests_regression.rs @@ -872,3 +872,134 @@ stage = "coder" item.retry_count() ); } + +// ── bug 953: bug-645 path must not advance when feature branch has zero commits ── + +/// Regression test for bug 953: when a coder agent exits with gates_passed=false +/// but has captured passing test evidence AND the feature branch has ZERO commits +/// ahead of master, the bug-645 salvage path must NOT advance the story to +/// QA or Merge. Zero commits = no actual work was done; treat as no-progress. +#[tokio::test] +async fn coder_completion_with_test_evidence_and_zero_commits_does_not_advance() { + use std::fs; + use std::process::Command; + + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + + // Init a git repo with an initial commit on master. + Command::new("git") + .args(["init"]) + .current_dir(root) + .output() + .unwrap(); + Command::new("git") + .args(["config", "user.email", "test@test.com"]) + .current_dir(root) + .output() + .unwrap(); + Command::new("git") + .args(["config", "user.name", "Test"]) + .current_dir(root) + .output() + .unwrap(); + Command::new("git") + .args(["commit", "--allow-empty", "-m", "init"]) + .current_dir(root) + .output() + .unwrap(); + + // Create a worktree on a feature branch that has ZERO commits ahead of master. + let wt_path = tmp.path().join("wt"); + Command::new("git") + .args([ + "worktree", + "add", + &wt_path.to_string_lossy(), + "-b", + "feature/story-9953_story_zero_commits", + ]) + .current_dir(root) + .output() + .unwrap(); + + // Seed the story in CRDT. + crate::crdt_state::init_for_test(); + crate::db::ensure_content_store(); + crate::db::write_item_with_content( + "9953_story_zero_commits", + "2_current", + "---\nname: Zero Commits Test\n---\n", + crate::db::ItemMeta::named("Zero Commits Test"), + ); + + // Simulate the agent having called run_tests with a passing result (bug-645 + // evidence) — but the feature branch still has zero commits ahead of master. + crate::db::write_content("9953_story_zero_commits:run_tests_ok", "1"); + + // Write a project.toml with max_retries=1 so the story blocks immediately, + // giving us a clean assertion target (StoryBlocked event). + fs::create_dir_all(root.join(".huskies")).unwrap(); + fs::write( + root.join(".huskies/project.toml"), + "max_retries = 1\n\n[[agent]]\nname = \"coder-1\"\nstage = \"coder\"\n", + ) + .unwrap(); + + let pool = AgentPool::new_test(3001); + let mut rx = pool.watcher_tx.subscribe(); + + // Simulate coder completing with gates_passed=false (e.g. agent crashed). + pool.run_pipeline_advance( + "9953_story_zero_commits", + "coder-1", + crate::agents::CompletionReport { + summary: "Agent crashed mid-output".to_string(), + gates_passed: false, + gate_output: "PTY write assertion failed".to_string(), + }, + Some(root.to_path_buf()), + Some(wt_path), + false, + None, + ) + .await; + + // The story must NOT have advanced to QA or Merge — it should be blocked + // (zero commits = no progress, so the bug-645 salvage path must not fire). + let mut got_blocked = false; + while let Ok(evt) = rx.try_recv() { + if let WatcherEvent::StoryBlocked { story_id, .. } = &evt + && story_id == "9953_story_zero_commits" + { + got_blocked = true; + break; + } + } + assert!( + got_blocked, + "Story with zero commits ahead of master must be blocked even when \ + test evidence exists — the bug-645 salvage path must require commits_ahead > 0" + ); + + // No QA or merge agent should have been started. + let agents = pool.agents.lock().unwrap(); + let qa_or_merge_started = agents + .values() + .any(|a| a.agent_name.contains("qa") || a.agent_name.contains("merge")); + assert!( + !qa_or_merge_started, + "No QA or merge agent must be started when feature branch has zero commits. \ + Pool: {:?}", + agents + .iter() + .map(|(k, a)| format!("{k}: {}", a.agent_name)) + .collect::>() + ); + + // Test evidence must have been consumed (cleared) by the advance handler. + assert!( + crate::db::read_content("9953_story_zero_commits:run_tests_ok").is_none(), + "run_tests evidence must be cleared after pipeline advance consumes it" + ); +}