From 19a2ffde96b66b4f6fe12e5c396bc6317ab2b3af Mon Sep 17 00:00:00 2001 From: dave Date: Wed, 29 Apr 2026 10:48:06 +0000 Subject: [PATCH] huskies: merge 860 --- server/src/agents/gates.rs | 225 ++++++++++++++++++++++++++++++++++++- 1 file changed, 221 insertions(+), 4 deletions(-) diff --git a/server/src/agents/gates.rs b/server/src/agents/gates.rs index 1c358446..7d93ff93 100644 --- a/server/src/agents/gates.rs +++ b/server/src/agents/gates.rs @@ -212,6 +212,33 @@ fn run_command_with_timeout( } } +/// Run the documentation coverage gate for the given worktree. +/// +/// Invokes `cargo run --quiet -p source-map-gen --bin source-map-check` in +/// `path`, comparing the current branch against its detected base branch +/// (`master` or `main`). Returns `(passed, combined_output)`. +pub(crate) fn run_doc_coverage_gate(path: &Path) -> Result<(bool, String), String> { + let base = detect_worktree_base_branch(path); + let base_str = base.as_str(); + run_command_with_timeout( + "cargo", + &[ + "run", + "--quiet", + "-p", + "source-map-gen", + "--bin", + "source-map-check", + "--", + "--worktree", + ".", + "--base", + base_str, + ], + path, + ) +} + /// Run `cargo clippy` and the project test suite (via `script/test` if present, /// otherwise `cargo nextest run` / `cargo test`) in the given directory. /// Returns `(gates_passed, combined_output)`. @@ -234,12 +261,27 @@ pub(crate) fn run_acceptance_gates(path: &Path) -> Result<(bool, String), String return Ok((false, msg)); } - // Run script/test (or fallback to cargo test). This is the sole - // acceptance gate — project-specific linting and test commands belong - // in script/test, not hardcoded here. + // Run script/test (or fallback to cargo test). Project-specific linting + // and test commands belong in script/test. let (test_success, test_out) = run_project_tests(path)?; + if !test_success { + return Ok((false, test_out)); + } - Ok((test_success, test_out)) + // Doc coverage gate (defence in depth): verify all public items in files + // changed since the base branch have doc comments. This gate catches + // projects that do not include source-map-check in their script/test, and + // provides an actionable "Doc coverage gate failed:" preamble so the agent + // knows exactly what to fix on retry. + let (doc_ok, doc_out) = run_doc_coverage_gate(path)?; + if !doc_ok { + return Ok(( + false, + format!("{test_out}Doc coverage gate failed:\n{doc_out}"), + )); + } + + Ok((true, test_out)) } /// Scan `root` recursively for Rust source files where both `path/X.rs` and @@ -557,6 +599,181 @@ mod tests { ); } + // ── run_acceptance_gates doc-coverage tests ─────────────────────────────── + + /// Create a git worktree from the actual huskies workspace on a fresh + /// branch (``) rooted at `master`. Returns the path to the + /// worktree. Call `cleanup_worktree` when done. + #[cfg(unix)] + fn create_test_worktree(branch: &str) -> std::path::PathBuf { + let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .to_path_buf(); + let wt = std::env::temp_dir().join(branch.replace('/', "_")); + // Remove any stale worktree from a previous failed run. + let _ = Command::new("git") + .args(["worktree", "remove", "--force", wt.to_str().unwrap()]) + .current_dir(&workspace) + .output(); + let _ = Command::new("git") + .args(["branch", "-D", branch]) + .current_dir(&workspace) + .output(); + + let out = Command::new("git") + .args([ + "worktree", + "add", + wt.to_str().unwrap(), + "-b", + branch, + "master", + ]) + .current_dir(&workspace) + .output() + .expect("git worktree add"); + assert!( + out.status.success(), + "git worktree add failed: {}", + String::from_utf8_lossy(&out.stderr) + ); + wt + } + + /// Remove the test worktree and its branch from the workspace. + #[cfg(unix)] + fn cleanup_worktree(branch: &str, wt: &std::path::Path) { + let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .to_path_buf(); + let _ = Command::new("git") + .args(["worktree", "remove", "--force", wt.to_str().unwrap()]) + .current_dir(&workspace) + .output(); + let _ = Command::new("git") + .args(["branch", "-D", branch]) + .current_dir(&workspace) + .output(); + } + + /// Write a `.cargo/config.toml` inside `wt` that redirects the build + /// target directory to the shared workspace `target/` so `cargo run` + /// finds the already-compiled `source-map-check` binary without + /// recompiling. + #[cfg(unix)] + fn set_shared_target(wt: &std::path::Path) { + use std::fs; + let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .to_path_buf(); + let target_dir = workspace.join("target"); + let cargo_dir = wt.join(".cargo"); + fs::create_dir_all(&cargo_dir).unwrap(); + fs::write( + cargo_dir.join("config.toml"), + format!( + "[build]\ntarget-dir = \"{}\"\n", + target_dir.to_str().unwrap() + ), + ) + .unwrap(); + } + + /// Write and chmod a minimal `script/test` that exits 0 without running + /// the full test suite. The file is left uncommitted so `run_project_tests` + /// uses it but it does not appear in the doc-check diff. + #[cfg(unix)] + fn write_fast_script_test(wt: &std::path::Path) { + use std::fs; + use std::os::unix::fs::PermissionsExt; + let script = wt.join("script").join("test"); + fs::write(&script, "#!/usr/bin/env bash\nexit 0\n").unwrap(); + let mut perms = fs::metadata(&script).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(&script, perms).unwrap(); + } + + /// Commit a single Rust file in the worktree. + #[cfg(unix)] + fn commit_file(wt: &std::path::Path, rel_path: &str, content: &str) { + use std::fs; + let full = wt.join(rel_path); + if let Some(parent) = full.parent() { + fs::create_dir_all(parent).unwrap(); + } + fs::write(&full, content).unwrap(); + Command::new("git") + .args(["add", rel_path]) + .current_dir(wt) + .output() + .unwrap(); + Command::new("git") + .args([ + "-c", + "user.email=test@test.com", + "-c", + "user.name=Test", + "commit", + "-m", + "test: add file for doc gate test", + ]) + .current_dir(wt) + .output() + .unwrap(); + } + + #[cfg(unix)] + #[test] + fn run_acceptance_gates_fails_on_undocumented_pub_fn() { + let branch = format!("test/doc-gate-fail-story-860-{}", std::process::id()); + let wt = create_test_worktree(&branch); + + set_shared_target(&wt); + write_fast_script_test(&wt); + commit_file( + &wt, + "server/src/_doc_gate_test_undoc.rs", + "pub fn undocumented_gate_test_fn() {}\n", + ); + + let result = run_acceptance_gates(&wt); + cleanup_worktree(&branch, &wt); + + let (passed, output) = result.unwrap(); + assert!(!passed, "acceptance gates should fail; output:\n{output}"); + assert!( + output.contains("Doc coverage gate failed:"), + "output should contain 'Doc coverage gate failed:'; got:\n{output}" + ); + } + + #[cfg(unix)] + #[test] + fn run_acceptance_gates_passes_with_documented_pub_fn() { + let branch = format!("test/doc-gate-pass-story-860-{}", std::process::id()); + let wt = create_test_worktree(&branch); + + set_shared_target(&wt); + write_fast_script_test(&wt); + commit_file( + &wt, + "server/src/_doc_gate_test_doc.rs", + "//! Test module for the doc-coverage happy-path gate test.\n/// A test function that is properly documented.\npub fn documented_gate_test_fn() {}\n", + ); + + let result = run_acceptance_gates(&wt); + cleanup_worktree(&branch, &wt); + + let (passed, output) = result.unwrap(); + assert!( + passed, + "acceptance gates should pass for documented pub fn; output:\n{output}" + ); + } + // ── worktree_has_committed_work tests ───────────────────────────────────── #[test]