Watch
0
0
Fork
You've already forked hyperhive
0

an agent gets one todo when its pr starts conflicting with the target branch

This commit is contained in:
atlas 2026-11-11 04:36:28 +01:00 • committed by mara
commit 11eb7dac9c

View file

@ -13,6 +13,8 @@ use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use serde_json::Value as Json;
use swarm_wire::forge_events::MERGEABILITY_EVENT;
use crate::flow::{Fields, Value};
/// The parts of an `issues` or `pull_request` family delivery the router
@ -165,6 +167,41 @@ pub struct Snapshot {
/// and tells nobody anything.
#[serde(default)]
pub head: Option<String>,
/// Whether the last mergeability event for this PR found it conflicting
/// with its target branch, or `None` before the first.
#[serde(default)]
pub conflicting: Option<bool>,
}
/// Whether a mergeability event moves the PR into or out of conflict: a
/// `conflicting` finding for a PR not already conflicting, or a `mergeable`
/// one for a PR that was.
fn mergeability_changed(event_type: &str, action: &str, snapshot: &Snapshot) -> bool {
if event_type != MERGEABILITY_EVENT {
return false;
}
match action {
"conflicting" => snapshot.conflicting != Some(true),
"mergeable" => snapshot.conflicting == Some(true),
_ => false,
}
}
/// The PR's conflict state after `event_type`, or `snapshot`'s when the event
/// says nothing about it. A closed PR forgets it: the `closed` flow clears the
/// conflict todo, so a reopened PR that still conflicts is told again.
pub fn conflicting_after(event_type: &str, action: &str, snapshot: &Snapshot) -> Option<bool> {
if event_type == "pull_request" && action == "closed" {
return None;
}
if event_type != MERGEABILITY_EVENT {
return snapshot.conflicting;
}
match action {
"conflicting" => Some(true),
"mergeable" => Some(false),
_ => snapshot.conflicting,
}
}
/// Whether `event_type` is an assign delivery, whose assignee list is the
@ -199,6 +236,14 @@ pub fn fields(event_type: &str, delivery: &Delivery, snapshot: &Snapshot) -> Fie
("author", text(&delivery.author)),
("draft", text(bool_text(delivery.draft))),
("undrafted", text(bool_text(delivery.undrafted))),
(
"mergeability_changed",
text(bool_text(mergeability_changed(
event_type,
&delivery.action,
snapshot,
))),
),
(
"assignees",
Value::List(delivery.assignees.iter().cloned().collect()),
@ -428,6 +473,60 @@ mod tests {
assert_eq!(f.get("undrafted"), Some(&Value::Text("true".to_owned())));
}
/// The body swarm-controller publishes for a mergeability finding reads as
/// a PR, and `mergeability_changed` is true only for a change.
#[test]
fn a_mergeability_event_reads_as_a_pull_request_and_flags_only_a_change() {
let body = |action: &str| {
serde_json::to_vec(&serde_json::json!({
"action": action,
"number": 9,
"pull_request": {
"number": 9,
"title": "t",
"html_url": "https://forge/pulls/9",
"user": { "login": "atlas" },
"assignees": [{ "login": "iris" }],
"head": { "sha": "abc" },
"base": { "ref": "main" },
"draft": false,
},
"repository": { "id": 42, "full_name": "o/r" },
}))
.expect("encodes")
};
let d = Delivery::from_event(MERGEABILITY_EVENT, &body("conflicting")).expect("parses");
assert!(d.is_pull);
assert_eq!(
(d.author.as_str(), d.number, d.action.as_str()),
("atlas", 9, "conflicting")
);
assert_eq!(d.assignees, BTreeSet::from(["iris".to_owned()]));
let changed = |action: &str, before: Option<bool>, event: &str| {
let d = Delivery::from_event(event, &body(action)).expect("parses");
let snapshot = Snapshot {
conflicting: before,
..Snapshot::default()
};
fields(event, &d, &snapshot)
.get("mergeability_changed")
.cloned()
};
let yes = Some(Value::Text("true".to_owned()));
let no = Some(Value::Text("false".to_owned()));
assert_eq!(changed("conflicting", None, MERGEABILITY_EVENT), yes);
assert_eq!(changed("conflicting", Some(false), MERGEABILITY_EVENT), yes);
assert_eq!(changed("conflicting", Some(true), MERGEABILITY_EVENT), no);
assert_eq!(changed("mergeable", Some(true), MERGEABILITY_EVENT), yes);
assert_eq!(changed("mergeable", Some(false), MERGEABILITY_EVENT), no);
assert_eq!(changed("mergeable", None, MERGEABILITY_EVENT), no);
assert_eq!(
changed("conflicting", None, "pull_request"),
no,
"another event type never changes it"
);
}
#[test]
fn the_snapshot_turns_the_after_list_into_added_and_removed() {
let d = Delivery::parse(&issue_assign("unassigned", &["iris"])).expect("parses");