huskies: merge 1011

This commit is contained in:
dave
2026-05-13 21:27:49 +00:00
parent 2758f744f2
commit eb48ef19e7
+83 -27
View File
@@ -477,13 +477,15 @@ pub(crate) async fn tool_run_lint(args: &Value, ctx: &AppContext) -> Result<Stri
// ── run_check ──────────────────────────────────────────────────────────────── // ── run_check ────────────────────────────────────────────────────────────────
/// Fast compile-only check (`script/check`). /// Fast quality gate (`script/check`).
/// ///
/// Runs `script/check` (expected to be `cargo check --tests --workspace`) in /// Delegates to `script/check` verbatim — whatever checks that script runs
/// the agent's worktree and returns the **full, untruncated** stdout + stderr /// (fmt, clippy, doc-coverage, etc.) are what this tool runs. Cargo
/// so that every compiler diagnostic is visible to the caller. Unlike /// diagnostics are returned in structured `errors`/`warnings` arrays; the raw
/// `run_build` and `run_lint`, output is never truncated — compile errors must /// `output` field (truncated to the last `MAX_OUTPUT_LINES` lines) is always
/// be readable in their entirety for fast iteration feedback. /// included so that non-cargo failures (e.g. `source-map-check`) surface even
/// when they produce no cargo-format diagnostics. Pass `verbose: true` for the
/// full untruncated output.
pub(crate) async fn tool_run_check(args: &Value, ctx: &AppContext) -> Result<String, String> { pub(crate) async fn tool_run_check(args: &Value, ctx: &AppContext) -> Result<String, String> {
let project_root = ctx.services.agents.get_project_root(&ctx.state)?; let project_root = ctx.services.agents.get_project_root(&ctx.state)?;
@@ -534,11 +536,14 @@ pub(crate) async fn tool_run_check(args: &Value, ctx: &AppContext) -> Result<Str
.map_err(|e| format!("Serialization error: {e}")) .map_err(|e| format!("Serialization error: {e}"))
} }
/// Shared response builder for tools that wrap cargo / rustc output. By /// Shared response builder for tools that wrap cargo / rustc output.
/// default returns parsed `errors` + `warnings` arrays plus a one-line ///
/// summary; the raw `output` is only included when `verbose` is true. This /// Returns parsed `errors` + `warnings` arrays (cargo format) plus a one-line
/// keeps the MCP response under the token cap for runs with many errors /// summary. The raw `output` is always included: truncated to the last
/// (bug 886). /// `MAX_OUTPUT_LINES` lines by default (keeps the token budget sane for large
/// cargo runs — bug 886), or full and untruncated when `verbose` is true. The
/// always-present `output` field means non-cargo failures (e.g.
/// `source-map-check`) surface even when they produce no parseable diagnostics.
fn build_diagnostic_response( fn build_diagnostic_response(
passed: bool, passed: bool,
exit_code: i32, exit_code: i32,
@@ -550,7 +555,12 @@ fn build_diagnostic_response(
let summary = summarise(&diags); let summary = summarise(&diags);
let errors: Vec<&_> = diags.iter().filter(|d| d.kind == "error").collect(); let errors: Vec<&_> = diags.iter().filter(|d| d.kind == "error").collect();
let warnings: Vec<&_> = diags.iter().filter(|d| d.kind == "warning").collect(); let warnings: Vec<&_> = diags.iter().filter(|d| d.kind == "warning").collect();
let mut payload = json!({ let output = if verbose {
raw_output.to_string()
} else {
truncate_output(raw_output, MAX_OUTPUT_LINES)
};
json!({
"passed": passed, "passed": passed,
"exit_code": exit_code, "exit_code": exit_code,
"errors": errors, "errors": errors,
@@ -559,11 +569,8 @@ fn build_diagnostic_response(
"{} error(s), {} warning(s)", "{} error(s), {} warning(s)",
summary.error_count, summary.warning_count summary.error_count, summary.warning_count
), ),
}); "output": output,
if verbose { })
payload["output"] = json!(raw_output);
}
payload
} }
#[cfg(test)] #[cfg(test)]
@@ -815,10 +822,9 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn tool_run_check_returns_parsed_errors_on_nonzero_exit() { async fn tool_run_check_returns_parsed_errors_on_nonzero_exit() {
// Bug 886: rather than dumping the entire cargo log into `output` // Cargo diagnostics are parsed into structured `errors`/`warnings`
// (which routinely exceeds the MCP token cap), tool_run_check now // arrays. The raw `output` is always present (truncated at
// parses errors / warnings into structured arrays. Raw output is // MAX_OUTPUT_LINES in default mode, full in verbose mode).
// available behind `verbose: true`.
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let script_dir = tmp.path().join("script"); let script_dir = tmp.path().join("script");
std::fs::create_dir_all(&script_dir).unwrap(); std::fs::create_dir_all(&script_dir).unwrap();
@@ -836,14 +842,14 @@ mod tests {
let ctx = test_ctx(tmp.path()); let ctx = test_ctx(tmp.path());
// Default mode: no `output` field, structured `errors` array, summary. // Default mode: truncated `output` is present, structured `errors` array, summary.
let result = tool_run_check(&json!({}), &ctx).await.unwrap(); let result = tool_run_check(&json!({}), &ctx).await.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["passed"], false); assert_eq!(parsed["passed"], false);
assert_eq!(parsed["exit_code"], 1); assert_eq!(parsed["exit_code"], 1);
assert!( assert!(
parsed.get("output").map(|v| v.is_null()).unwrap_or(true), parsed["output"].as_str().is_some(),
"default mode must not include raw `output`" "default mode must include `output`"
); );
let errors = parsed["errors"].as_array().expect("errors array"); let errors = parsed["errors"].as_array().expect("errors array");
assert_eq!(errors.len(), 150, "all 150 errors should be parsed"); assert_eq!(errors.len(), 150, "all 150 errors should be parsed");
@@ -862,14 +868,14 @@ mod tests {
summary.contains("150 error"), summary.contains("150 error"),
"summary mentions error count: {summary}" "summary mentions error count: {summary}"
); );
// Default response should be small even with 150 errors. // Response should stay compact even with 150 errors + truncated output.
assert!( assert!(
result.len() < 50_000, result.len() < 50_000,
"default response should be compact (was {} bytes)", "response should be compact (was {} bytes)",
result.len() result.len()
); );
// Verbose mode: raw output is included. // Verbose mode: full untruncated output.
let result_v = tool_run_check(&json!({"verbose": true}), &ctx) let result_v = tool_run_check(&json!({"verbose": true}), &ctx)
.await .await
.unwrap(); .unwrap();
@@ -881,6 +887,56 @@ mod tests {
assert!(output.contains("error[E150]"), "verbose contains last line"); assert!(output.contains("error[E150]"), "verbose contains last line");
} }
#[tokio::test]
async fn tool_run_check_surfaces_doc_coverage_error() {
// Parity test: when script/check fails because source-map-check finds
// missing doc comments, the failure message must be visible in the
// response even though it produces no cargo-format diagnostics.
let tmp = tempfile::tempdir().unwrap();
let script_dir = tmp.path().join("script");
std::fs::create_dir_all(&script_dir).unwrap();
let script_path = script_dir.join("check");
std::fs::write(
&script_path,
"#!/usr/bin/env bash\n\
echo '=== Checking doc coverage on changed files ==='\n\
echo 'Doc coverage check failed. Add doc comments to the following items before committing:' >&2\n\
echo ' Add a `///` doc comment above `pub fn new` in server/src/foo.rs:10' >&2\n\
exit 1\n",
)
.unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&script_path, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let ctx = test_ctx(tmp.path());
let result = tool_run_check(&json!({}), &ctx).await.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["passed"], false, "check must report failure");
assert_eq!(parsed["exit_code"], 1);
// No cargo-format diagnostics — errors array is empty.
let errors = parsed["errors"].as_array().expect("errors array");
assert!(
errors.is_empty(),
"no cargo diagnostics expected for a source-map-check failure"
);
// But the raw failure text must surface in `output`.
let output = parsed["output"].as_str().expect("output must be present");
assert!(
output.contains("Doc coverage check failed"),
"doc-coverage failure must be visible in output: {output}"
);
assert!(
output.contains("pub fn new"),
"specific missing-doc item must be visible: {output}"
);
}
#[tokio::test] #[tokio::test]
async fn tool_run_check_passes_when_script_exits_zero() { async fn tool_run_check_passes_when_script_exits_zero() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();