Watch
0
0
Fork
You've already forked hyperhive
0

closed flow also queues a closed todo on each agent that held one, after the clear

This commit is contained in:
atlas 2026-10-24 13:58:08 +02:00 • committed by mara
commit 0a1e4db0f4
4 changed files with 99 additions and 24 deletions

View file

@ -4,8 +4,9 @@
//! `swarm_wire::notify_flows`; the built-in and the custom flows are the same
//! type and this walk runs both. A `match` node passes when every one of its
//! conditions holds, and then each of its children runs, so one event can
//! reach several branches. A `todo` node emits one op per recipient. The
//! router runs only flows that pass [`Flow::validate`].
//! reach several branches, in edge order, so a clear listed before an upsert
//! is queued before it. A `todo` node emits one op per recipient. The router
//! runs only flows that pass [`Flow::validate`].
use std::collections::BTreeMap;
@ -46,10 +47,10 @@ pub fn evaluate(flow: &Flow, event_type: &str, fields: &Fields) -> Vec<Action> {
while let Some(id) = stack.pop() {
let Some(node) = flow.node(id) else { continue };
match &node.kind {
NodeKind::Source { .. } => stack.extend(flow.children(id)),
NodeKind::Source { .. } => stack.extend(flow.children(id).into_iter().rev()),
NodeKind::Match { when } => {
if when.iter().all(|c| holds(c, fields)) {
stack.extend(flow.children(id));
stack.extend(flow.children(id).into_iter().rev());
}
}
NodeKind::Todo(sink) => actions.extend(emit(flow, node, sink, fields)),
@ -229,6 +230,7 @@ mod tests {
"issues",
&event("closed", &[], &[], &["atlas", "argus"]),
);
let got: Vec<_> = got.into_iter().filter(|a| a.node == "sink").collect();
assert_eq!(got.len(), 2);
for a in &got {
assert_eq!(a.op.op, Op::Clear);
@ -239,6 +241,43 @@ mod tests {
assert!(evaluate(&closed, "issues", &event("reopened", &[], &[], &["atlas"])).is_empty());
}
/// The clear runs before the `closed` todo, so its prefix does not take the
/// todo with it, and both read the one holders set.
#[test]
fn closing_clears_then_tells_each_holder() {
let closed = flow("closed");
let got = evaluate(
&closed,
"pull_request",
&event("closed", &[], &[], &["atlas", "argus"]),
);
let seen: Vec<(&str, &str, Op)> = got
.iter()
.map(|a| (a.node.as_str(), a.agent.as_str(), a.op.op))
.collect();
assert_eq!(
seen,
[
("sink", "atlas", Op::Clear),
("sink", "argus", Op::Clear),
("inform", "atlas", Op::Upsert),
("inform", "argus", Op::Upsert),
]
);
let tell = &got[2].op;
assert_eq!(tell.key, "forge:hyperhive/hyperhive#7:closed");
assert!(!tell.prefix);
assert_eq!(
tell.summary,
"issue hyperhive/hyperhive#7 was closed: router (https://forge/7)"
);
assert_eq!(tell.source, "router:closed");
// The clear's prefix covers the todo's key: the order is what saves it.
assert!(tell.key.starts_with(&got[0].op.key));
// No holders, no todos.
assert!(evaluate(&closed, "issues", &event("closed", &[], &[], &[])).is_empty());
}
/// A match node with two children runs both: the branching the tree
/// allows.
#[test]

View file

@ -137,9 +137,18 @@ mod tests {
&flows,
is_agent,
);
// Per agent the clear comes before the closed todo that its prefix would take.
let mut got = agents(&closed);
got.sort_unstable_by_key(|(a, _)| *a);
assert_eq!(got, [("atlas", Op::Clear), ("iris", Op::Clear)]);
got.sort_by_key(|(a, _)| *a);
assert_eq!(
got,
[
("atlas", Op::Clear),
("atlas", Op::Upsert),
("iris", Op::Clear),
("iris", Op::Upsert)
]
);
assert!(closed.next.holders.is_empty());
// A close leaves the assignee snapshot as the last assign set it.
assert_eq!(closed.next.assignees, three.next.assignees);