archetype flows behave like the built-ins: switch off per archetype or per agent, board cards, duplicate
The document's disabled list takes archetype:<archetype>/<flow> (off for every member) and the same with @<agent> (off for one member); a key naming nothing is ignored. GET /api/notify/flows lists the archetype flows with their members, the board draws a card for each with a switch, a checkbox per agent and duplicate, and the dry run counts their hits. The flows keep following the archetype.
This commit is contained in:
parent
1efbb72fdd
commit
68c24643f6
2 changed files with 908 additions and 102 deletions
|
|
@ -130,6 +130,24 @@ pub(crate) async fn of_agents(
|
|||
Ok(found)
|
||||
}
|
||||
|
||||
/// The archetype of every agent that has one, by agent name.
|
||||
pub(crate) async fn all(
|
||||
client: &async_nats::Client,
|
||||
) -> anyhow::Result<std::collections::BTreeMap<String, ArchetypeRef>> {
|
||||
use futures_util::TryStreamExt as _;
|
||||
|
||||
let store = agent_archetype::open_or_create(client).await?;
|
||||
let mut keys = store.keys().await?;
|
||||
let mut found = std::collections::BTreeMap::new();
|
||||
while let Some(agent) = keys.try_next().await? {
|
||||
let (archetype, _, _) = read_or_default::<Option<ArchetypeRef>>(&store, &agent).await?;
|
||||
if let Some(archetype) = archetype {
|
||||
found.insert(agent, archetype);
|
||||
}
|
||||
}
|
||||
Ok(found)
|
||||
}
|
||||
|
||||
/// Delete a destroyed agent's key, so the next agent of that name is not
|
||||
/// built from this one's archetype. A failure is logged and does not stop the
|
||||
/// destroy, as in `agent_permissions::clear_on_destroy`.
|
||||
|
|
|
|||
|
|
@ -20,11 +20,14 @@ use axum::extract::rejection::JsonRejection;
|
|||
use axum::http::StatusCode;
|
||||
use problem_details::ProblemDetails;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use swarm_queue_client::archetypes::{self, SourceRecord, flows_of};
|
||||
use swarm_wire::notify_flows::{self, DecodeError, Document, Flow};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::routes::{AppState, error_problem, wanted_error_status};
|
||||
use crate::schedules::{queue, read_with, refused, write_revision};
|
||||
use crate::schedules::{queue, read_or_default, read_with, refused, write_revision};
|
||||
|
||||
/// How many times a losing writer re-reads and re-applies, as in
|
||||
/// `wanted::MAX_ATTEMPTS`.
|
||||
|
|
@ -36,17 +39,38 @@ pub(crate) struct FlowsView {
|
|||
/// The revision of the stored document, to send back in a `PUT`; `0` when
|
||||
/// none is stored.
|
||||
revision: u64,
|
||||
/// The ids of the built-in flows that are switched off.
|
||||
/// The ids of the built-in flows that are switched off, and the keys of the
|
||||
/// archetype flows that are.
|
||||
disabled_builtins: Vec<String>,
|
||||
/// The built-in flows this build ships, all of them, switched off or not.
|
||||
builtins: Vec<Flow>,
|
||||
/// The custom flows.
|
||||
flows: Vec<Flow>,
|
||||
/// The flows archetypes carry, each with the agents it runs for. They follow
|
||||
/// their archetype, so they are not in the document; a `PUT` switches one off
|
||||
/// with its `key` in `disabled_builtins`. An older controller's answer has
|
||||
/// none.
|
||||
archetype_flows: Vec<ArchetypeFlowView>,
|
||||
/// What this build accepts: source events, fields, node kinds and limits,
|
||||
/// for an editor to offer. An older controller's answer has none.
|
||||
schema: notify_flows::Schema,
|
||||
}
|
||||
|
||||
/// One flow an archetype carries.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, ToSchema)]
|
||||
pub(crate) struct ArchetypeFlowView {
|
||||
/// The archetype's name.
|
||||
archetype: String,
|
||||
/// What names the flow in `disabled_builtins` (off for every member), in the
|
||||
/// router's log and in a todo's source. `<key>@<agent>` in
|
||||
/// `disabled_builtins` switches it off for one member.
|
||||
key: String,
|
||||
/// The flow as the archetype declares it, under its own `id`.
|
||||
flow: Flow,
|
||||
/// The agents of the archetype, by name.
|
||||
members: Vec<String>,
|
||||
}
|
||||
|
||||
/// The body of a `PUT`: the whole document.
|
||||
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
|
|
@ -64,12 +88,13 @@ pub(crate) struct PutFlows {
|
|||
}
|
||||
|
||||
impl FlowsView {
|
||||
fn of(doc: Document, revision: u64) -> Self {
|
||||
fn of(doc: Document, revision: u64, archetype_flows: Vec<ArchetypeFlowView>) -> Self {
|
||||
Self {
|
||||
revision,
|
||||
disabled_builtins: doc.disabled_builtins,
|
||||
builtins: notify_flows::builtins(),
|
||||
flows: doc.flows,
|
||||
archetype_flows,
|
||||
schema: notify_flows::schema(),
|
||||
}
|
||||
}
|
||||
|
|
@ -152,12 +177,53 @@ pub(crate) async fn get_flows(
|
|||
Stored::Nothing => Ok(Json(FlowsView::of(
|
||||
Document::default(),
|
||||
revision.unwrap_or(0),
|
||||
archetype_flows(&client).await,
|
||||
))),
|
||||
Stored::Document(doc) => Ok(Json(FlowsView::of(
|
||||
doc,
|
||||
revision.unwrap_or(0),
|
||||
archetype_flows(&client).await,
|
||||
))),
|
||||
Stored::Document(doc) => Ok(Json(FlowsView::of(doc, revision.unwrap_or(0)))),
|
||||
Stored::Unreadable(e) => Err(unreadable(&e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The flows the archetypes carry, with their agents. A read that fails is
|
||||
/// logged and leaves the list empty: the document is still served.
|
||||
async fn archetype_flows(client: &async_nats::Client) -> Vec<ArchetypeFlowView> {
|
||||
let found = async {
|
||||
let refs = crate::agent_archetype::all(client).await?;
|
||||
let mut records = BTreeMap::new();
|
||||
if let Some(store) = archetypes::open(client).await? {
|
||||
let sources: BTreeSet<&str> = refs.values().map(|r| r.source.as_str()).collect();
|
||||
for source in sources {
|
||||
let (record, _, _) = read_or_default::<SourceRecord>(&store, source).await?;
|
||||
records.insert(source.to_owned(), record);
|
||||
}
|
||||
}
|
||||
anyhow::Ok(flows_of(&refs, &records).0)
|
||||
}
|
||||
.await;
|
||||
match found {
|
||||
Ok(found) => found
|
||||
.into_iter()
|
||||
.map(|f| ArchetypeFlowView {
|
||||
key: f.key(),
|
||||
archetype: f.archetype,
|
||||
flow: f.flow,
|
||||
members: f.members,
|
||||
})
|
||||
.collect(),
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
error = %format!("{e:#}"),
|
||||
"notify flows: reading the archetype flows failed; none shown"
|
||||
);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn unreadable(why: &DecodeError) -> ProblemDetails {
|
||||
error_problem(
|
||||
StatusCode::CONFLICT,
|
||||
|
|
@ -239,7 +305,11 @@ pub(crate) async fn put_flows(
|
|||
custom = doc.flows.len(),
|
||||
"set the notification router's flows"
|
||||
);
|
||||
return Ok(Json(FlowsView::of(doc, written)));
|
||||
return Ok(Json(FlowsView::of(
|
||||
doc,
|
||||
written,
|
||||
archetype_flows(&client).await,
|
||||
)));
|
||||
}
|
||||
}
|
||||
Err(queue_problem(&anyhow::anyhow!(
|
||||
|
|
@ -465,4 +535,105 @@ mod tests {
|
|||
.await
|
||||
.expect("{number} is a field");
|
||||
}
|
||||
|
||||
/// The archetypes' flows are listed with their agents; their keys go in the
|
||||
/// disabled list, a key for a flow that is gone is kept, and a key of no
|
||||
/// valid shape is refused.
|
||||
#[tokio::test]
|
||||
async fn archetype_flows_are_listed_and_their_keys_are_saved() {
|
||||
use swarm_queue_client::agent_archetype::ArchetypeRef;
|
||||
use swarm_queue_client::archetypes::{ArchetypeMeta, BUILTIN_SOURCE, SourceRecord};
|
||||
|
||||
let nats = TestNats::start();
|
||||
let client = nats.client().await;
|
||||
let state = crate::agent_limits::tests::state(&client);
|
||||
|
||||
let mut members = custom("new-issues");
|
||||
members.nodes[1].kind = NodeKind::Todo(TodoSink {
|
||||
to: Recipients::Members,
|
||||
op: Op::Upsert,
|
||||
key: "forge:{{ repo }}#{{ number }}:triage".to_owned(),
|
||||
prefix: false,
|
||||
text: "triage {{ repo }}#{{ number }}".to_owned(),
|
||||
});
|
||||
let record = SourceRecord {
|
||||
source: BUILTIN_SOURCE.to_owned(),
|
||||
archetypes: [
|
||||
(
|
||||
"triage".to_owned(),
|
||||
ArchetypeMeta {
|
||||
description: "d".to_owned(),
|
||||
flows: vec![members.clone()],
|
||||
..ArchetypeMeta::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
"reviewer".to_owned(),
|
||||
ArchetypeMeta {
|
||||
description: "d".to_owned(),
|
||||
..ArchetypeMeta::default()
|
||||
},
|
||||
),
|
||||
]
|
||||
.into(),
|
||||
refused: BTreeMap::default(),
|
||||
};
|
||||
let records = swarm_queue_client::archetypes::open_or_create(&client)
|
||||
.await
|
||||
.expect("records");
|
||||
records
|
||||
.put(BUILTIN_SOURCE, serde_json::to_vec(&record).unwrap().into())
|
||||
.await
|
||||
.expect("put the record");
|
||||
let choices = swarm_queue_client::agent_archetype::open_or_create(&client)
|
||||
.await
|
||||
.expect("choices");
|
||||
for (agent, archetype) in [
|
||||
("alice", "triage"),
|
||||
("bob", "triage"),
|
||||
("carol", "reviewer"),
|
||||
] {
|
||||
let choice = ArchetypeRef {
|
||||
source: BUILTIN_SOURCE.to_owned(),
|
||||
name: archetype.to_owned(),
|
||||
};
|
||||
choices
|
||||
.put(agent, serde_json::to_vec(&choice).unwrap().into())
|
||||
.await
|
||||
.expect("put the choice");
|
||||
}
|
||||
|
||||
let Json(view) = get_flows(State(state.clone())).await.expect("get");
|
||||
assert_eq!(view.archetype_flows.len(), 1, "{:?}", view.archetype_flows);
|
||||
let listed = &view.archetype_flows[0];
|
||||
assert_eq!(listed.key, "archetype:triage/new-issues");
|
||||
assert_eq!(listed.archetype, "triage");
|
||||
assert_eq!(listed.flow, members);
|
||||
assert_eq!(listed.members, ["alice", "bob"]);
|
||||
|
||||
// Off for everyone, off for one agent, and a key of a flow that is gone.
|
||||
let keys = [
|
||||
"archetype:triage/new-issues",
|
||||
"archetype:triage/new-issues@alice",
|
||||
"archetype:gone/flow",
|
||||
];
|
||||
let Json(saved) = put_flows(State(state.clone()), Ok(body(0, &keys, vec![])))
|
||||
.await
|
||||
.expect("the keys are saved");
|
||||
assert_eq!(saved.disabled_builtins, keys);
|
||||
assert_eq!(
|
||||
saved.archetype_flows.len(),
|
||||
1,
|
||||
"a PUT answers with them too"
|
||||
);
|
||||
for bad in ["archetype:triage", "archetype:/x", "archetype:a/b@", "nope"] {
|
||||
let refused = put_flows(
|
||||
State(state.clone()),
|
||||
Ok(body(saved.revision, &[bad], vec![])),
|
||||
)
|
||||
.await
|
||||
.expect_err(bad);
|
||||
assert_eq!(refused.status, Some(StatusCode::BAD_REQUEST), "{bad}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue