huskies: merge 953

This commit is contained in:
dave
2026-05-13 08:53:03 +00:00
parent 6bd11d41f9
commit 6a015d6202
@@ -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::<Vec<_>>()
);
// 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"
);
}