Watch
0
0
Fork
You've already forked hyperhive
0

notify flows: refuse unknown placeholders and require the revision read on every save

This commit is contained in:
atlas 2026-10-24 04:38:17 +02:00 • committed by mara
commit ada364a482
7 changed files with 203 additions and 57 deletions

View file

@ -67,14 +67,25 @@ fn emit(flow: &Flow, node: &Node, sink: &TodoSink, fields: &Fields) -> Vec<Actio
None => Vec::new(),
},
};
let rendered = expand(&sink.key, fields).and_then(|key| {
let summary = match sink.op {
Op::Upsert => expand(&sink.text, fields)?,
Op::Clear => String::new(),
};
Ok((key, summary))
});
let (key, summary) = match rendered {
Ok(done) => done,
Err(why) => {
tracing::warn!(flow = %flow.id, node = %node.id, %why, "notify router: a todo node names a placeholder the event lacks; nothing sent");
return Vec::new();
}
};
let op = RouterTodoOp {
op: sink.op,
key: expand(&sink.key, fields),
key,
prefix: sink.op == Op::Clear && sink.prefix,
summary: match sink.op {
Op::Upsert => expand(&sink.text, fields),
Op::Clear => String::new(),
},
summary,
source: format!("router:{}", flow.id),
};
recipients
@ -96,9 +107,9 @@ fn holds(condition: &Condition, fields: &Fields) -> bool {
}
}
/// `template` with each `{field}` replaced by that text field's value. A list
/// field is joined with `, `; an unknown name is left as written.
pub fn expand(template: &str, fields: &Fields) -> String {
/// `template` with each `{field}` replaced by that field's value, a list
/// joined with `, `. A name that is no field is an error naming it.
pub fn expand(template: &str, fields: &Fields) -> Result<String, String> {
let mut out = String::with_capacity(template.len());
let mut rest = template;
while let Some(open) = rest.find('{') {
@ -106,22 +117,18 @@ pub fn expand(template: &str, fields: &Fields) -> String {
let after = &rest[open + 1..];
let Some(close) = after.find('}') else {
out.push_str(&rest[open..]);
return out;
return Ok(out);
};
let name = &after[..close];
match fields.get(name) {
Some(Value::Text(v)) => out.push_str(v),
Some(Value::List(items)) => out.push_str(&items.join(", ")),
None => {
out.push('{');
out.push_str(name);
out.push('}');
}
None => return Err(format!("{{{name}}} is no event field")),
}
rest = &after[close + 1..];
}
out.push_str(rest);
out
Ok(out)
}
#[cfg(test)]
@ -288,11 +295,36 @@ mod tests {
}
#[test]
fn a_placeholder_expands_to_its_field_and_an_unknown_one_stays() {
fn a_placeholder_expands_to_its_field_and_an_unknown_one_is_an_error() {
let fields = event("assigned", &["a", "b"], &[], &[]);
assert_eq!(
expand("{repo}#{number} {assignees_added} {nope} {", &fields),
"hyperhive/hyperhive#7 a, b {nope} {"
expand("{repo}#{number} {assignees_added} {", &fields),
Ok("hyperhive/hyperhive#7 a, b {".to_owned())
);
assert_eq!(
expand("x {nope}", &fields),
Err("{nope} is no event field".to_owned())
);
}
/// A sink naming a placeholder the event lacks sends nothing, rather
/// than a todo under a literal `{name}` key.
#[test]
fn a_sink_with_an_unknown_placeholder_emits_nothing() {
let mut f = flow("assigned");
f.nodes[2].kind = NodeKind::Todo(TodoSink {
to: Recipients::Agent("triage".to_owned()),
op: Op::Upsert,
key: "forge:{repo}#{numbr}:triage".to_owned(),
prefix: false,
text: "t".to_owned(),
});
let fields = event("assigned", &["atlas"], &[], &[]);
assert!(evaluate(&f, "issue_assign", &fields).is_empty());
// Control: spelled right, the same flow emits.
if let NodeKind::Todo(s) = &mut f.nodes[2].kind {
s.key = "forge:{repo}#{number}:triage".to_owned();
}
assert_eq!(evaluate(&f, "issue_assign", &fields).len(), 1);
}
}