an archetype carries its own notification-router flows for its agents
ArchetypeMeta gets flows, checked when the controller stores the record: every todo must go to the new members recipient, which the router expands to the agents of the archetype. The operator's flows document refuses members. The router reads the archetype records and each roster agent's archetype key, and its NATS grant gains read on both buckets.
This commit is contained in:
parent
ca339b7ba3
commit
1efbb72fdd
1 changed files with 883 additions and 29 deletions
|
|
@ -24,6 +24,7 @@ use swarm_queue_client::archetypes::{
|
|||
use swarm_wire::builder::{
|
||||
ARCHETYPES_REPLY_DEADLINE, ARCHETYPES_SUBJECT, ArchetypesReply, EvalArchetypes,
|
||||
};
|
||||
use swarm_wire::notify_flows::{Flow, MAX_FLOWS};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::routes::{AppState, error_problem};
|
||||
|
|
@ -61,6 +62,28 @@ struct DeclaredMeta {
|
|||
prompt: Option<String>,
|
||||
#[serde(default)]
|
||||
icon: Option<String>,
|
||||
#[serde(default)]
|
||||
flows: Vec<Flow>,
|
||||
}
|
||||
|
||||
/// The flows an archetype carries: each passes [`Flow::validate_archetype`],
|
||||
/// ids are unique and there are no more than the operator's document may hold.
|
||||
fn check_flows(flows: &[Flow]) -> Result<(), String> {
|
||||
if flows.len() > MAX_FLOWS {
|
||||
return Err(format!(
|
||||
"flows: {} flows, more than {MAX_FLOWS}",
|
||||
flows.len()
|
||||
));
|
||||
}
|
||||
let mut ids = std::collections::BTreeSet::new();
|
||||
for flow in flows {
|
||||
if !ids.insert(flow.id.as_str()) {
|
||||
return Err(format!("flows: id {} is used twice", flow.id));
|
||||
}
|
||||
flow.validate_archetype()
|
||||
.map_err(|why| format!("flow {}: {why}", flow.id))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `meta` as stored, or why a create would refuse it.
|
||||
|
|
@ -84,6 +107,7 @@ fn check(name: &str, meta: serde_json::Value) -> Result<ArchetypeMeta, String> {
|
|||
.map(|i| check_icon(&i).map(|()| i))
|
||||
.transpose()
|
||||
.map_err(|e| format!("icon: {e}"))?;
|
||||
check_flows(&declared.flows)?;
|
||||
let permissions = match declared.permissions {
|
||||
Some(perms) => {
|
||||
agent_permissions::unique(&perms)?;
|
||||
|
|
@ -112,6 +136,7 @@ fn check(name: &str, meta: serde_json::Value) -> Result<ArchetypeMeta, String> {
|
|||
.map_err(|e| format!("inference: {e}"))?,
|
||||
prompt: declared.prompt.filter(|p| !p.trim().is_empty()),
|
||||
icon,
|
||||
flows: declared.flows,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -548,6 +573,57 @@ mod tests {
|
|||
assert!(r.refused["twice"].contains("listed twice"));
|
||||
}
|
||||
|
||||
/// A `todo` node to `to`, below a source of `issues`, as an archetype's
|
||||
/// `meta.flows` entry in the JSON a source flake declares.
|
||||
fn flow_to(id: &str, to: &serde_json::Value) -> serde_json::Value {
|
||||
json!({
|
||||
"id": id,
|
||||
"name": "new issues",
|
||||
"nodes": [
|
||||
{ "id": "src", "kind": "source", "events": ["issues"] },
|
||||
{ "id": "tell", "kind": "todo", "to": to, "op": "upsert",
|
||||
"key": "forge:{{ repo }}#{{ number }}:new", "text": "new issue {{ title }}" }
|
||||
],
|
||||
"edges": [{ "from": "src", "to": "tell" }]
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_archetype_flow_to_members_is_stored_and_any_other_recipient_is_refused() {
|
||||
let r = evaluated(json!({
|
||||
"ok": { "description": "x", "flows": [flow_to("new", &json!("members"))] },
|
||||
"agent": { "description": "x", "flows": [flow_to("new", &json!({ "agent": "triage" }))] },
|
||||
"field": { "description": "x", "flows": [flow_to("new", &json!({ "field": "author" }))] },
|
||||
"twice": { "description": "x", "flows": [
|
||||
flow_to("new", &json!("members")), flow_to("new", &json!("members"))
|
||||
] },
|
||||
"review": { "description": "x", "flows": [{
|
||||
"id": "rev",
|
||||
"name": "review",
|
||||
"nodes": [
|
||||
{ "id": "src", "kind": "source", "events": ["pull_request"] },
|
||||
{ "id": "ask", "kind": "request_reviewer", "reviewer": "argus" }
|
||||
],
|
||||
"edges": [{ "from": "src", "to": "ask" }]
|
||||
}] },
|
||||
"broken": { "description": "x", "flows": [{ "id": "x" }] }
|
||||
}));
|
||||
assert_eq!(r.archetypes.keys().collect::<Vec<_>>(), ["ok"]);
|
||||
assert_eq!(r.archetypes["ok"].flows.len(), 1);
|
||||
assert_eq!(r.archetypes["ok"].flows[0].id, "new");
|
||||
for refused in ["agent", "field"] {
|
||||
assert!(
|
||||
r.refused[refused].contains("operator's flows document"),
|
||||
"{refused}: {}",
|
||||
r.refused[refused]
|
||||
);
|
||||
}
|
||||
assert!(r.refused["twice"].contains("used twice"));
|
||||
assert!(r.refused["review"].contains("cannot request a review"));
|
||||
assert!(r.refused.contains_key("broken"));
|
||||
assert_eq!(r.refused.len(), 5);
|
||||
}
|
||||
|
||||
/// Against a real server: no builder listening names the option; a
|
||||
/// listener's evaluation is checked and stored, and storing the same
|
||||
/// record again writes nothing.
|
||||
|
|
|
|||
Loading…
Reference in a new issue