story-kit: merge 69_story_test_coverage_qa_gate

This commit is contained in:
Dave
2026-02-23 13:40:12 +00:00
parent 61383caa01
commit 16989a12fc
2 changed files with 276 additions and 15 deletions

View File

@@ -537,7 +537,8 @@ impl AgentPool {
///
/// - **Coder** + gates passed → move story to `work/3_qa/`, start `qa` agent.
/// - **Coder** + gates failed → restart the same coder agent with failure context.
/// - **QA** + gates passed → move story to `work/4_merge/`, start `mergemaster` agent.
/// - **QA** + gates passed + coverage passed → move story to `work/4_merge/`, start `mergemaster` agent.
/// - **QA** + gates passed + coverage failed → restart `qa` with coverage failure context.
/// - **QA** + gates failed → restart `qa` with failure context.
/// - **Mergemaster** → run `script/test` on master; if pass: archive + cleanup worktree;
/// if fail: restart `mergemaster` with failure context.
@@ -545,7 +546,7 @@ impl AgentPool {
async fn run_pipeline_advance_for_completed_agent(&self, story_id: &str, agent_name: &str) {
let key = composite_key(story_id, agent_name);
let (completion, project_root) = {
let (completion, project_root, worktree_path) = {
let agents = match self.agents.lock() {
Ok(a) => a,
Err(e) => {
@@ -557,7 +558,11 @@ impl AgentPool {
Some(a) => a,
None => return,
};
(agent.completion.clone(), agent.project_root.clone())
let wt_path = agent
.worktree_info
.as_ref()
.map(|wt| wt.path.clone());
(agent.completion.clone(), agent.project_root.clone(), wt_path)
};
let completion = match completion {
@@ -618,18 +623,51 @@ impl AgentPool {
}
PipelineStage::Qa => {
if completion.gates_passed {
eprintln!(
"[pipeline] QA passed gates for '{story_id}'. Moving to merge."
);
if let Err(e) = move_story_to_merge(&project_root, story_id) {
eprintln!("[pipeline] Failed to move '{story_id}' to 4_merge/: {e}");
return;
}
if let Err(e) = self
.start_agent(&project_root, story_id, Some("mergemaster"), None)
.await
{
eprintln!("[pipeline] Failed to start mergemaster for '{story_id}': {e}");
// Run coverage gate in the QA worktree before advancing to merge.
let coverage_path = worktree_path.clone().unwrap_or_else(|| project_root.clone());
let cp = coverage_path.clone();
let coverage_result =
tokio::task::spawn_blocking(move || run_coverage_gate(&cp))
.await
.unwrap_or_else(|e| {
eprintln!("[pipeline] Coverage gate task panicked: {e}");
Ok((false, format!("Coverage gate task panicked: {e}")))
});
let (coverage_passed, coverage_output) = match coverage_result {
Ok(pair) => pair,
Err(e) => (false, e),
};
if coverage_passed {
eprintln!(
"[pipeline] QA passed gates and coverage for '{story_id}'. Moving to merge."
);
if let Err(e) = move_story_to_merge(&project_root, story_id) {
eprintln!("[pipeline] Failed to move '{story_id}' to 4_merge/: {e}");
return;
}
if let Err(e) = self
.start_agent(&project_root, story_id, Some("mergemaster"), None)
.await
{
eprintln!("[pipeline] Failed to start mergemaster for '{story_id}': {e}");
}
} else {
eprintln!(
"[pipeline] QA coverage gate failed for '{story_id}'. Restarting QA."
);
let context = format!(
"\n\n---\n## Coverage Gate Failed\n\
The coverage gate (script/test_coverage) failed with the following output:\n{}\n\n\
Please improve test coverage until the coverage gate passes.",
coverage_output
);
if let Err(e) = self
.start_agent(&project_root, story_id, Some("qa"), Some(&context))
.await
{
eprintln!("[pipeline] Failed to restart qa for '{story_id}': {e}");
}
}
} else {
eprintln!(
@@ -1353,6 +1391,36 @@ fn run_acceptance_gates(path: &Path) -> Result<(bool, String), String> {
Ok((all_passed, all_output))
}
/// Run `script/test_coverage` in the given directory if the script exists.
///
/// Used as a QA gate before advancing a story from `3_qa/` to `4_merge/`.
/// Returns `(passed, output)`. If the script does not exist, returns `(true, …)`.
fn run_coverage_gate(path: &Path) -> Result<(bool, String), String> {
let script = path.join("script").join("test_coverage");
if !script.exists() {
return Ok((
true,
"script/test_coverage not found; coverage gate skipped.\n".to_string(),
));
}
let mut output = String::from("=== script/test_coverage ===\n");
let result = Command::new(&script)
.current_dir(path)
.output()
.map_err(|e| format!("Failed to run script/test_coverage: {e}"))?;
let combined = format!(
"{}{}",
String::from_utf8_lossy(&result.stdout),
String::from_utf8_lossy(&result.stderr)
);
output.push_str(&combined);
output.push('\n');
Ok((result.status.success(), output))
}
// ── Mergemaster helpers ───────────────────────────────────────────────────────
/// Squash-merge a feature branch into the current branch in the project root.
@@ -2351,4 +2419,77 @@ mod tests {
assert!(!passed, "script/test exiting 1 should fail");
assert!(output.contains("script/test"), "output should mention script/test");
}
// ── run_coverage_gate tests ───────────────────────────────────────────────
#[cfg(unix)]
#[test]
fn coverage_gate_passes_when_script_absent() {
use tempfile::tempdir;
let tmp = tempdir().unwrap();
let (passed, output) = run_coverage_gate(tmp.path()).unwrap();
assert!(passed, "coverage gate should pass when script is absent");
assert!(
output.contains("not found"),
"output should mention script not found"
);
}
#[cfg(unix)]
#[test]
fn coverage_gate_passes_when_script_exits_zero() {
use std::fs;
use std::os::unix::fs::PermissionsExt;
use tempfile::tempdir;
let tmp = tempdir().unwrap();
let path = tmp.path();
let script_dir = path.join("script");
fs::create_dir_all(&script_dir).unwrap();
let script = script_dir.join("test_coverage");
fs::write(
&script,
"#!/usr/bin/env bash\necho 'Rust line coverage: 85%'\necho 'PASS: Coverage 85% meets threshold 0%'\nexit 0\n",
)
.unwrap();
let mut perms = fs::metadata(&script).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&script, perms).unwrap();
let (passed, output) = run_coverage_gate(path).unwrap();
assert!(passed, "coverage gate should pass when script exits 0");
assert!(
output.contains("script/test_coverage"),
"output should mention script/test_coverage"
);
}
#[cfg(unix)]
#[test]
fn coverage_gate_fails_when_script_exits_nonzero() {
use std::fs;
use std::os::unix::fs::PermissionsExt;
use tempfile::tempdir;
let tmp = tempdir().unwrap();
let path = tmp.path();
let script_dir = path.join("script");
fs::create_dir_all(&script_dir).unwrap();
let script = script_dir.join("test_coverage");
fs::write(
&script,
"#!/usr/bin/env bash\necho 'FAIL: Coverage 40% is below threshold 80%'\nexit 1\n",
)
.unwrap();
let mut perms = fs::metadata(&script).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&script, perms).unwrap();
let (passed, output) = run_coverage_gate(path).unwrap();
assert!(!passed, "coverage gate should fail when script exits 1");
assert!(
output.contains("script/test_coverage"),
"output should mention script/test_coverage"
);
}
}