huskies: merge 1188 story Merge tool results return a summary, not the full gate log (35KB per call)

This commit is contained in:
Huskies Agent
2026-07-17 11:58:37 +00:00
parent ea8e6edc94
commit dac0278218
8 changed files with 545 additions and 14 deletions
+134 -12
View File
@@ -82,9 +82,15 @@ pub(super) fn tool_get_merge_status(args: &Value, ctx: &AppContext) -> Result<St
match &job.status {
crate::agents::merge::MergeJobStatus::Running => {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs_f64();
let elapsed_seconds = (now - job.started_at).max(0.0);
serde_json::to_string_pretty(&json!({
"story_id": story_id,
"status": "running",
"elapsed_seconds": elapsed_seconds,
"message": "Merge pipeline is still running."
}))
.map_err(|e| format!("Serialization error: {e}"))
@@ -92,24 +98,27 @@ pub(super) fn tool_get_merge_status(args: &Value, ctx: &AppContext) -> Result<St
crate::agents::merge::MergeJobStatus::Completed(report) => {
use crate::agents::merge::MergeResult;
let status_msg = crate::service::merge::format_merge_status_message(report);
let (success, had_conflicts, conflicts_resolved, conflict_details, gates_passed, gate_output) =
let (success, had_conflicts, conflicts_resolved, conflict_details, gates_passed) =
match &report.result {
MergeResult::Success { conflicts_resolved, conflict_details, gate_output } => {
(true, *conflicts_resolved, *conflicts_resolved, conflict_details.clone(), true, gate_output.clone())
MergeResult::Success { conflicts_resolved, conflict_details, .. } => {
(true, *conflicts_resolved, *conflicts_resolved, conflict_details.clone(), true)
}
MergeResult::Conflict { details, output } => {
(false, true, false, details.clone(), false, output.clone())
MergeResult::Conflict { details, .. } => {
(false, true, false, details.clone(), false)
}
MergeResult::GateFailure { output, .. } => {
(false, false, false, None, false, output.clone())
MergeResult::GateFailure { .. } => {
(false, false, false, None, false)
}
MergeResult::NoCommits { output } => {
(false, false, false, None, false, output.clone())
MergeResult::NoCommits { .. } => {
(false, false, false, None, false)
}
MergeResult::Other { output, conflict_details } => {
(false, false, false, conflict_details.clone(), false, output.clone())
MergeResult::Other { conflict_details, .. } => {
(false, false, false, conflict_details.clone(), false)
}
};
// AC1: condensed digest instead of the full (potentially 35KB+)
// gate_output — successful merges carry no summary at all.
let gate_summary = crate::service::merge::summarize_merge_result(&report.result);
serde_json::to_string_pretty(&json!({
"story_id": story_id,
@@ -119,7 +128,8 @@ pub(super) fn tool_get_merge_status(args: &Value, ctx: &AppContext) -> Result<St
"conflicts_resolved": conflicts_resolved,
"conflict_details": conflict_details,
"gates_passed": gates_passed,
"gate_output": gate_output,
"gate_summary": if gate_summary.is_empty() { serde_json::Value::Null } else { json!(gate_summary) },
"report_path": report.report_path,
"worktree_cleaned_up": report.worktree_cleaned_up,
"story_archived": report.story_archived,
"message": status_msg,
@@ -398,6 +408,118 @@ mod tests {
assert!(result.unwrap_err().contains("No merge job"));
}
/// AC3: a still-running merge job returns status and elapsed time only —
/// no gate data of any kind belongs on a Running response.
#[test]
fn tool_get_merge_status_running_includes_elapsed_seconds_only() {
crate::crdt_state::init_for_test();
crate::crdt_state::write_merge_job(
"50_story_running",
"running",
0.0,
None,
Some("{\"server_start\":0.0}"),
);
let tmp = tempfile::tempdir().unwrap();
let ctx = test_ctx(tmp.path());
let result = tool_get_merge_status(&json!({"story_id": "50_story_running"}), &ctx);
assert!(result.is_ok(), "expected Ok, got: {result:?}");
let v: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap();
assert_eq!(v["status"], "running");
assert!(
v["elapsed_seconds"].as_f64().unwrap() > 0.0,
"elapsed_seconds must reflect time since started_at=0.0: {v}"
);
assert!(
v.get("gate_output").is_none() && v.get("gate_summary").is_none(),
"a running job must carry no gate data: {v}"
);
}
/// AC1: a successful merge's response carries no gate summary at all.
#[test]
fn tool_get_merge_status_completed_success_has_no_gate_summary() {
crate::crdt_state::init_for_test();
let report = crate::agents::merge::MergeReport {
story_id: "51_story_ok".to_string(),
result: crate::agents::merge::MergeResult::Success {
conflicts_resolved: false,
conflict_details: None,
gate_output: "all tests passed".to_string(),
},
worktree_cleaned_up: true,
story_archived: true,
report_path: Some(".huskies/merge_reports/51_story_ok-1.log".to_string()),
};
let report_json = serde_json::to_string(&report).unwrap();
crate::crdt_state::write_merge_job(
"51_story_ok",
"completed",
1.0,
Some(2.0),
Some(&report_json),
);
let tmp = tempfile::tempdir().unwrap();
let ctx = test_ctx(tmp.path());
let result = tool_get_merge_status(&json!({"story_id": "51_story_ok"}), &ctx).unwrap();
let v: serde_json::Value = serde_json::from_str(&result).unwrap();
assert!(
v["gate_summary"].is_null(),
"success must carry no gate summary: {v}"
);
assert_eq!(v["report_path"], ".huskies/merge_reports/51_story_ok-1.log");
}
/// AC1/AC2: a gate-failure response carries a condensed summary (failing
/// gate + parsed test names + tail), plus the full-report path — not the
/// raw multi-KB output.
#[test]
fn tool_get_merge_status_completed_gate_failure_has_condensed_summary() {
crate::crdt_state::init_for_test();
let output = "running 1 test\ntest foo::bar ... FAILED\n\n\
failures:\n foo::bar\n\ntest result: FAILED. 0 passed; 1 failed\n"
.to_string();
let report = crate::agents::merge::MergeReport {
story_id: "52_story_fail".to_string(),
result: crate::agents::merge::MergeResult::GateFailure {
output,
failure_kind: Some(crate::agents::gates::GateFailureKind::Test),
},
worktree_cleaned_up: false,
story_archived: false,
report_path: Some(".huskies/merge_reports/52_story_fail-1.log".to_string()),
};
let report_json = serde_json::to_string(&report).unwrap();
crate::crdt_state::write_merge_job(
"52_story_fail",
"completed",
1.0,
Some(2.0),
Some(&report_json),
);
let tmp = tempfile::tempdir().unwrap();
let ctx = test_ctx(tmp.path());
let result = tool_get_merge_status(&json!({"story_id": "52_story_fail"}), &ctx).unwrap();
let v: serde_json::Value = serde_json::from_str(&result).unwrap();
let summary = v["gate_summary"]
.as_str()
.expect("gate_summary must be present on failure");
assert!(
summary.contains("Failing gate: tests"),
"summary: {summary}"
);
assert!(
summary.contains("Failing tests: foo::bar"),
"summary: {summary}"
);
assert_eq!(
v["report_path"],
".huskies/merge_reports/52_story_fail-1.log"
);
}
// tool_get_merge_status_returns_running removed: depends on
// tool_merge_agent_work which now blocks indefinitely in a poll loop.