huskies: merge 987

This commit is contained in:
dave
2026-05-13 16:26:09 +00:00
parent 430079ecbc
commit c3c9db3d8b
13 changed files with 662 additions and 311 deletions
+40 -28
View File
@@ -105,22 +105,35 @@ impl AgentPool {
return;
}
let success = matches!(&report, Ok(r) if r.success);
let success = matches!(
&report,
Ok(r) if matches!(r.result, crate::agents::merge::MergeResult::Success { .. })
);
let finished_at = unix_now();
// On any failure: record merge_failure in CRDT and emit notification.
if !success {
let kind = match &report {
Ok(r) if r.no_commits => crate::pipeline_state::MergeFailureKind::NoCommits,
Ok(r) if r.had_conflicts => {
crate::pipeline_state::MergeFailureKind::ConflictDetected(
r.conflict_details.clone(),
)
}
Ok(r) => {
crate::pipeline_state::MergeFailureKind::GatesFailed(r.gate_output.clone())
}
Ok(r) => match &r.result {
crate::agents::merge::MergeResult::NoCommits { .. } => {
crate::pipeline_state::MergeFailureKind::NoCommits
}
crate::agents::merge::MergeResult::Conflict { details, .. } => {
crate::pipeline_state::MergeFailureKind::ConflictDetected(
details.clone(),
)
}
crate::agents::merge::MergeResult::GateFailure { output, .. } => {
crate::pipeline_state::MergeFailureKind::GatesFailed(output.clone())
}
crate::agents::merge::MergeResult::Other { output, .. } => {
crate::pipeline_state::MergeFailureKind::Other(output.clone())
}
crate::agents::merge::MergeResult::Success { .. } => {
unreachable!("success branch is guarded by !success above")
}
},
Err(e) => crate::pipeline_state::MergeFailureKind::Other(e.clone()),
};
let is_no_commits =
@@ -131,7 +144,17 @@ impl AgentPool {
&& report
.as_ref()
.ok()
.and_then(|r| r.gate_failure_kind.as_ref())
.and_then(|r| {
if let crate::agents::merge::MergeResult::GateFailure {
failure_kind: Some(k),
..
} = &r.result
{
Some(k)
} else {
None
}
})
.map(|k| k.is_self_evident_fix())
.unwrap_or(false);
@@ -271,17 +294,13 @@ impl AgentPool {
.await
.map_err(|e| format!("Merge task panicked: {e}"))??;
if !merge_result.success {
if !matches!(
merge_result,
crate::agents::merge::MergeResult::Success { .. }
) {
return Ok(crate::agents::merge::MergeReport {
story_id: story_id.to_string(),
success: false,
had_conflicts: merge_result.had_conflicts,
conflicts_resolved: merge_result.conflicts_resolved,
conflict_details: merge_result.conflict_details,
gates_passed: merge_result.gates_passed,
gate_output: merge_result.output,
gate_failure_kind: merge_result.gate_failure_kind,
no_commits: merge_result.no_commits,
result: merge_result,
worktree_cleaned_up: false,
story_archived: false,
});
@@ -305,14 +324,7 @@ impl AgentPool {
Ok(crate::agents::merge::MergeReport {
story_id: story_id.to_string(),
success: true,
had_conflicts: merge_result.had_conflicts,
conflicts_resolved: merge_result.conflicts_resolved,
conflict_details: merge_result.conflict_details,
gates_passed: true,
gate_output: merge_result.output,
gate_failure_kind: None,
no_commits: false,
result: merge_result,
worktree_cleaned_up,
story_archived,
})
@@ -23,14 +23,10 @@ impl AgentPool {
.and_then(|e| serde_json::from_str::<crate::agents::merge::MergeReport>(e).ok())
.unwrap_or_else(|| crate::agents::merge::MergeReport {
story_id: story_id.to_string(),
success: false,
had_conflicts: false,
conflicts_resolved: false,
conflict_details: None,
gates_passed: false,
gate_output: String::new(),
gate_failure_kind: None,
no_commits: false,
result: crate::agents::merge::MergeResult::Other {
output: String::new(),
conflict_details: None,
},
worktree_cleaned_up: false,
story_archived: false,
});
+70 -26
View File
@@ -237,7 +237,13 @@ async fn merge_agent_work_returns_error_when_branch_not_found() {
let job = run_merge_to_completion(&pool, repo, "99_nonexistent").await;
match &job.status {
MergeJobStatus::Completed(report) => {
assert!(!report.success, "should fail when branch missing");
assert!(
!matches!(
report.result,
crate::agents::merge::MergeResult::Success { .. }
),
"should fail when branch missing"
);
}
MergeJobStatus::Failed(_) => {
// Also acceptable — the pipeline errored out
@@ -305,11 +311,23 @@ async fn merge_agent_work_succeeds_on_clean_branch() {
match &job.status {
MergeJobStatus::Completed(report) => {
assert!(!report.had_conflicts, "should have no conflicts");
assert!(
report.success
|| report.gate_output.contains("Failed to run")
|| !report.gates_passed,
!matches!(
report.result,
crate::agents::merge::MergeResult::Conflict { .. }
),
"should have no conflicts"
);
let is_success = matches!(
report.result,
crate::agents::merge::MergeResult::Success { .. }
);
let is_gate_failure = matches!(
report.result,
crate::agents::merge::MergeResult::GateFailure { .. }
);
assert!(
is_success || is_gate_failure || report.result.output().contains("Failed to run"),
"report should be coherent: {report:?}"
);
if report.story_archived {
@@ -418,8 +436,8 @@ fn quality_gates_run_before_fast_forward_to_master() {
// Gates must have failed (script/test exits 1) so master should be untouched.
assert!(
!result.success,
"run_squash_merge must report failure when gates fail"
!matches!(result, crate::agents::merge::MergeResult::Success { .. }),
"run_squash_merge must report failure when gates fail; got: {result:?}"
);
assert_eq!(
head_before, head_after,
@@ -531,7 +549,13 @@ async fn merge_agent_work_conflict_does_not_break_master() {
// The report should accurately reflect what happened.
match &job.status {
MergeJobStatus::Completed(report) => {
assert!(report.had_conflicts, "should report conflicts");
assert!(
matches!(
report.result,
crate::agents::merge::MergeResult::Conflict { .. }
),
"should report conflicts"
);
}
MergeJobStatus::Failed(_) => {
// Acceptable — merge aborted due to conflicts
@@ -596,17 +620,17 @@ async fn merge_agent_work_zero_commits_ahead_stays_in_merge_stage() {
match &job.status {
MergeJobStatus::Completed(report) => {
assert!(
!report.success,
"merge must not have succeeded when feature branch is empty"
matches!(
report.result,
crate::agents::merge::MergeResult::NoCommits { .. }
),
"merge must fail with NoCommits when feature branch is empty; got: {:?}",
report.result
);
assert!(
report.no_commits,
"report.no_commits must be true for a zero-ahead branch"
);
assert!(
report.gate_output.contains("no commits to merge"),
"gate_output must contain 'no commits to merge', got: {}",
report.gate_output
report.result.output().contains("no commits to merge"),
"output must contain 'no commits to merge', got: {}",
report.result.output()
);
}
MergeJobStatus::Failed(e) => {
@@ -701,10 +725,16 @@ async fn server_side_merge_happy_path_advances_to_done() {
match &job.status {
MergeJobStatus::Completed(report) => {
assert!(
!report.had_conflicts,
!matches!(
report.result,
crate::agents::merge::MergeResult::Conflict { .. }
),
"clean branch should have no conflicts"
);
if report.success {
if matches!(
report.result,
crate::agents::merge::MergeResult::Success { .. }
) {
// story_archived may or may not be true depending on gate env,
// but merge_failure must NOT be in the content store.
let content = crate::db::read_content(crate::db::ContentKey::Story("757a_happy"));
@@ -838,7 +868,8 @@ async fn server_side_merge_conflict_sets_merge_failure() {
// The merge must fail (conflict).
let failed = matches!(
&job.status,
MergeJobStatus::Completed(r) if !r.success
MergeJobStatus::Completed(r)
if !matches!(r.result, crate::agents::merge::MergeResult::Success { .. })
) || matches!(&job.status, MergeJobStatus::Failed(_));
assert!(
failed,
@@ -953,11 +984,17 @@ async fn server_side_merge_gate_failure_sets_merge_failure() {
match &job.status {
MergeJobStatus::Completed(report) => {
assert!(
!report.success,
!matches!(
report.result,
crate::agents::merge::MergeResult::Success { .. }
),
"gates should have failed; report: {report:?}"
);
assert!(
!report.had_conflicts,
!matches!(
report.result,
crate::agents::merge::MergeResult::Conflict { .. }
),
"should be a gate failure, not a conflict"
);
}
@@ -1052,11 +1089,18 @@ async fn merge_agent_work_one_commit_ahead_merges_successfully() {
MergeJobStatus::Completed(report) => {
// Success or gate failure — both acceptable; the key invariant is
// that we didn't fail with the zero-commits early-exit.
let is_success = matches!(
report.result,
crate::agents::merge::MergeResult::Success { .. }
);
let is_gate_failure = matches!(
report.result,
crate::agents::merge::MergeResult::GateFailure { .. }
);
assert!(
report.success || !report.gates_passed,
"unexpected state: success={} gates_passed={}",
report.success,
report.gates_passed
is_success || is_gate_failure,
"unexpected result variant: {:?}",
report.result
);
}
MergeJobStatus::Running => panic!("should not still be running"),