use std::sync::Arc;
mod document;
pub use document::{bundle, bundle_json, document, prepare, DOCUMENT_PATH, DOCUMENT_VERSION};
use onemessagebus::{
Allowlist, Asker, Author, ConsumerName, Correlation, Layout, Message, OpWord, Operation,
Policy, Position, Predicate, Pushed, Queue, QueueError, QueueName, QueueSpec, Registry, Router,
SchemaId, Supersede, Transport,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
pub const PLANNER_CHANNEL: &str = "planner-channel";
pub const SURFACES: &str = "surfaces";
pub const REPLIES: &str = "replies";
pub const COMMANDS: &str = "commands";
pub const COMMAND_OUTCOMES: &str = "command-outcomes";
pub const PROJECTION: &str = "queue.json";
pub const FILES: [&str; 7] = [
"surfaces.jsonl",
PROJECTION,
"replies.jsonl",
"replies-cursor.json",
"commands.jsonl",
"commands-cursor.json",
"command-outcomes.jsonl",
];
pub const ASKER_ENV: &str = "ONEPIPELINE_CHANNEL_ASKER";
pub const REPLY_ENVELOPE_VERSION: u32 = 3;
pub const REPLY_ENVELOPE_VERSIONS_READ: &[u32] = &[REPLY_ENVELOPE_VERSION, 2];
pub const REPLY_ENVELOPE_FAMILY: &str = "agent.reply-envelope";
pub const SURFACE_SCHEMA: SchemaId = SchemaId::literal("agent", "planner-surface", 1);
pub const QUEUED_REPLY_SCHEMA: SchemaId = SchemaId::literal("agent", "queued-reply", 1);
pub const QUEUED_COMMANDS_SCHEMA: SchemaId = SchemaId::literal("agent", "queued-commands", 1);
pub const COMMAND_OUTCOME_SCHEMA: SchemaId = SchemaId::literal("agent", "command-outcome", 1);
pub const READ_REPLY_ENVELOPE_SCHEMA: SchemaId =
SchemaId::literal("agent", "planner-reply-envelope", 1);
const REPLY_ENVELOPE_V2: &str = include_str!("../../schemas/reply-envelope-v2.schema.json");
const REPLY_ENVELOPE_V3: &str = include_str!("../../schemas/reply-envelope-v3.schema.json");
pub mod source {
pub const CHECK_IN: &str = "check-in";
pub const PROPOSAL: &str = "proposal";
pub const RECONCILER: &str = "reconciler";
}
fn is_false(value: &bool) -> bool {
!*value
}
pub(super) fn recorded_asker<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Asker>, D::Error> {
Ok(Option::<String>::deserialize(deserializer)?
.and_then(|name| Asker::new(&name, "the recorded asker").ok()))
}
pub(super) fn recorded_correlation<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Correlation>, D::Error> {
Ok(Option::<String>::deserialize(deserializer)?.and_then(|text| text.parse().ok()))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct Surface {
pub id: u64,
pub kind: String,
pub message: String,
pub source: String,
pub blocking: bool,
pub queued_at: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workstream: Option<String>,
#[serde(default, skip_serializing_if = "is_false")]
pub abandoned: bool,
#[serde(
default,
deserialize_with = "recorded_asker",
skip_serializing_if = "Option::is_none"
)]
pub asker: Option<Asker>,
#[serde(
default,
deserialize_with = "recorded_correlation",
skip_serializing_if = "Option::is_none"
)]
pub correlation: Option<Correlation>,
}
impl Message for Surface {
const SCHEMA: SchemaId = SURFACE_SCHEMA;
}
fn planner() -> Author {
Author::from("planner")
}
fn is_planner(author: &Author) -> bool {
author.as_str() == "planner"
}
fn read_at_a_version_this_build_reads<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Option<u32>, D::Error> {
Ok(Option::<u32>::deserialize(deserializer)?.map(|version| {
if REPLY_ENVELOPE_VERSIONS_READ.contains(&version) {
REPLY_ENVELOPE_VERSION
} else {
version
}
}))
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ReplyEnvelope {
#[serde(
default,
deserialize_with = "read_at_a_version_this_build_reads",
skip_serializing_if = "Option::is_none"
)]
pub version: Option<u32>,
#[serde(default = "planner", skip_serializing_if = "is_planner")]
pub author: Author,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub completion: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub commands: Vec<Map<String, Value>>,
}
impl Message for ReplyEnvelope {
const SCHEMA: SchemaId = READ_REPLY_ENVELOPE_SCHEMA;
}
impl Default for ReplyEnvelope {
fn default() -> Self {
Self {
version: None,
author: planner(),
completion: None,
message: None,
reason: None,
commands: Vec::new(),
}
}
}
impl ReplyEnvelope {
#[must_use]
pub fn carries_verdict(&self) -> bool {
self.completion.is_some() || self.message.is_some() || self.reason.is_some()
}
#[must_use]
pub fn carries_edits_without_a_verdict(&self) -> bool {
!self.commands.is_empty() && !self.carries_verdict()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct QueuedReply {
pub id: u64,
pub reply: ReplyEnvelope,
pub at: u64,
#[serde(
default,
deserialize_with = "recorded_correlation",
skip_serializing_if = "Option::is_none"
)]
pub correlation: Option<Correlation>,
}
impl Message for QueuedReply {
const SCHEMA: SchemaId = QUEUED_REPLY_SCHEMA;
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct QueuedCommands {
pub id: u64,
#[serde(default = "planner")]
pub author: Author,
pub commands: Vec<Map<String, Value>>,
}
impl Message for QueuedCommands {
const SCHEMA: SchemaId = QUEUED_COMMANDS_SCHEMA;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum CommandVerdict {
Applied,
Validated,
Delivered,
Refused,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct CommandResult {
pub index: usize,
pub op: String,
pub outcome: CommandVerdict,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct CommandOutcome {
pub id: u64,
pub applied: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub results: Vec<CommandResult>,
}
impl Message for CommandOutcome {
const SCHEMA: SchemaId = COMMAND_OUTCOME_SCHEMA;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Op {
Add,
Drop,
Reparent,
Retry,
Cancel,
Requeue,
Complete,
Attest,
Finding,
Amend,
Note,
Settle,
}
impl Op {
pub const ALL: [Op; 12] = [
Op::Add,
Op::Drop,
Op::Reparent,
Op::Retry,
Op::Cancel,
Op::Requeue,
Op::Complete,
Op::Attest,
Op::Finding,
Op::Amend,
Op::Note,
Op::Settle,
];
#[must_use]
pub const fn word(self) -> &'static str {
match self {
Op::Add => "add",
Op::Drop => "drop",
Op::Reparent => "reparent",
Op::Retry => "retry",
Op::Cancel => "cancel",
Op::Requeue => "requeue",
Op::Complete => "complete",
Op::Attest => "attest",
Op::Finding => "finding",
Op::Amend => "amend",
Op::Note => "note",
Op::Settle => "settle",
}
}
#[must_use]
pub fn of_word(word: &str) -> Option<Op> {
Op::ALL.into_iter().find(|op| op.word() == word)
}
}
impl Operation for Op {
fn name(&self) -> &str {
self.word()
}
}
#[must_use]
pub fn allowlist() -> Allowlist<Op> {
let planner = planner();
let mut allowlist = Allowlist::new(Op::ALL);
for op in Op::ALL {
allowlist.grant(planner.clone(), op);
}
allowlist
}
pub fn allows<O: Operation>(
allowlist: &Allowlist<O>,
author: &Author,
word: &str,
) -> Result<(), String> {
let Some(op) = allowlist.vocabulary().iter().find(|op| op.name() == word) else {
return Err(format!(
"'{word}' is not an op of the planner channel; the ops are: {}",
Op::ALL.map(Op::word).join(", ")
));
};
allowlist.allows(author, op).map_err(|refusal| {
format!(
"'{}' is not an op the {} may issue: {}. Surface it to the planner instead",
refusal.op, refusal.author, refusal.reason
)
})
}
fn ensure_declared<O: Operation>(allowlist: &Allowlist<O>, author: &Author) -> Result<(), String> {
if allowlist.declares(author) {
return Ok(());
}
Err(format!(
"the envelope's author `{author}` is not declared; the declared authors are: {}",
allowlist
.authors()
.iter()
.map(Author::as_str)
.collect::<Vec<_>>()
.join(", ")
))
}
pub fn allows_completion<O: Operation>(
allowlist: &Allowlist<O>,
author: &Author,
completion: Option<bool>,
) -> Result<(), String> {
if completion != Some(true) {
return Ok(());
}
let Some(complete) = allowlist
.vocabulary()
.iter()
.find(|op| op.name() == Op::Complete.word())
else {
return Ok(());
};
allowlist.allows(author, complete).map_err(|refusal| {
format!(
"declaring the run complete is not something the {} may do: {}. Surface it to the planner instead",
refusal.author, refusal.reason
)
})
}
fn name(text: &str) -> QueueName {
QueueName::try_from(text).unwrap_or_else(|_| unreachable!("{text} is a queue name"))
}
fn field(text: &str) -> onemessagebus::FieldPath {
text.parse()
.unwrap_or_else(|_| unreachable!("{text} is a field path"))
}
fn claims_a_verdict() -> Predicate {
let verdict = Predicate::Any(
["reply.completion", "reply.message", "reply.reason"]
.into_iter()
.map(|path| Predicate::Present {
field: field(path),
present: true,
})
.collect(),
);
Predicate::Not(Box::new(Predicate::All(vec![
Predicate::NonEmpty {
field: field("reply.commands"),
non_empty: true,
},
Predicate::Not(Box::new(verdict)),
])))
}
#[must_use]
pub fn queues() -> Vec<QueueSpec> {
let mut surfaces = QueueSpec::new(
name(SURFACES),
Policy {
supersede_on: Some(Supersede {
key: field("source"),
when: Some(Predicate::equals(field("source"), source::CHECK_IN)),
}),
hold_pending: true,
blocking_first: true,
projection: Some(
PROJECTION
.parse()
.unwrap_or_else(|_| unreachable!("queue.json is a document name")),
),
..Policy::default()
},
);
surfaces.schema = Some(SURFACE_SCHEMA);
surfaces.answers = Some(name(REPLIES));
let mut replies = QueueSpec::new(name(REPLIES), Policy::default());
replies.schema = Some(QUEUED_REPLY_SCHEMA);
replies.claims = Some(claims_a_verdict());
replies.numbered = true;
let mut commands = QueueSpec::new(name(COMMANDS), Policy::default());
commands.schema = Some(QUEUED_COMMANDS_SCHEMA);
commands.numbered = true;
let mut outcomes = QueueSpec::new(name(COMMAND_OUTCOMES), Policy::default());
outcomes.schema = Some(COMMAND_OUTCOME_SCHEMA);
vec![surfaces, replies, commands, outcomes]
}
#[must_use]
pub fn registry() -> Registry {
let mut registry = Registry::new();
registry
.register::<Surface>()
.expect("the planner surface schema registers");
registry
.register::<QueuedReply>()
.expect("the queued reply schema registers");
registry
.register::<QueuedCommands>()
.expect("the queued commands schema registers");
registry
.register::<CommandOutcome>()
.expect("the command outcome schema registers");
registry
.register::<ReplyEnvelope>()
.expect("the read reply envelope schema registers");
for (version, document) in [(2, REPLY_ENVELOPE_V2), (3, REPLY_ENVELOPE_V3)] {
let schema: Value = serde_json::from_str(document).expect("a committed schema is JSON");
registry
.register_schema(
SchemaId::literal("agent", "reply-envelope", version),
schema,
)
.expect("the reply envelope schema registers");
}
let profile = onemessagebus_agent::registry();
for id in profile.ids() {
if registry.schema(&id).is_some() {
continue;
}
let document = profile
.schema(&id)
.cloned()
.unwrap_or_else(|| unreachable!("the profile holds every id it lists"));
registry
.register_schema(id, document)
.expect("a schema the profile registered registers again");
}
registry
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PlannerChannel;
impl PlannerChannel {
fn route_envelope<O: Operation>(
envelope: Value,
allowlist: &Allowlist<O>,
) -> Result<Vec<(QueueName, Value)>, String> {
if !envelope.is_object() {
return Err("the reply is malformed: a reply envelope is a JSON object".to_owned());
}
let envelope: ReplyEnvelope = serde_json::from_value(envelope)
.map_err(|failure| format!("the reply is malformed: {failure}"))?;
let author = envelope.author.clone();
ensure_declared(allowlist, &author)?;
allows_completion(allowlist, &author, envelope.completion)?;
let mut routed = Vec::new();
if !envelope.commands.is_empty() {
if envelope.version != Some(REPLY_ENVELOPE_VERSION) {
return Err(format!(
"an edit envelope requires version {REPLY_ENVELOPE_VERSION}"
));
}
for command in &envelope.commands {
let word = command
.get("op")
.and_then(Value::as_str)
.ok_or("a command names its `op`")?;
allows(allowlist, &author, word)?;
}
routed.push((
name(COMMANDS),
serde_json::json!({
"id": 0,
"author": envelope.author,
"commands": envelope.commands,
}),
));
}
if envelope.carries_verdict() || envelope.commands.is_empty() {
routed.push((
name(REPLIES),
serde_json::json!({ "id": 0, "reply": envelope, "at": crate::sys::now_millis() }),
));
}
Ok(routed)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ReplyRouter;
impl Router for ReplyRouter {
fn route(
&self,
queue: &QueueName,
record: Value,
allowlist: &Allowlist<OpWord>,
) -> Result<Vec<(QueueName, Value)>, String> {
match record.get("reply") {
Some(envelope) => {
PlannerChannel::route_envelope(envelope.clone(), allowlist)?;
Ok(vec![(queue.clone(), record)])
}
None => PlannerChannel::route_envelope(record, allowlist),
}
}
}
impl Layout for PlannerChannel {
fn name(&self) -> &str {
PLANNER_CHANNEL
}
fn queues(&self) -> Vec<QueueSpec> {
queues()
}
fn allowlist(&self) -> Allowlist<OpWord> {
allowlist().words()
}
fn registry(&self) -> Registry {
registry()
}
fn prepare(
&self,
queue: &QueueName,
record: Value,
allowlist: &Allowlist<OpWord>,
) -> Result<Vec<(QueueName, Value)>, String> {
match queue.as_str() {
SURFACES => {
let Value::Object(fields) = record else {
return Ok(vec![(queue.clone(), record)]);
};
let about = fields.get(onemessagebus::ask::ABOUT).cloned();
let mut fields: Map<String, Value> = fields
.into_iter()
.filter(|(key, _)| key != onemessagebus::ask::ABOUT)
.collect();
if let Some(about) = about {
fields.entry("workstream").or_insert(about);
}
fields
.entry("queued_at")
.or_insert_with(|| Value::from(crate::sys::now_millis()));
Ok(vec![(queue.clone(), Value::Object(fields))])
}
REPLIES => ReplyRouter.route(queue, record, allowlist),
COMMANDS => {
let author: Author = record
.get("author")
.map(|author| serde_json::from_value(author.clone()))
.transpose()
.map_err(|failure| format!("the envelope's author: {failure}"))?
.unwrap_or_else(planner);
ensure_declared(allowlist, &author)?;
for command in record
.get("commands")
.and_then(Value::as_array)
.ok_or("a command envelope carries `commands`")?
{
let word = command
.get("op")
.and_then(Value::as_str)
.ok_or("a command names its `op`")?;
allows(allowlist, &author, word)?;
}
Ok(vec![(queue.clone(), record)])
}
_ => Ok(vec![(queue.clone(), record)]),
}
}
}
#[derive(Debug, Clone)]
pub struct Channel {
surfaces: Queue<Surface>,
replies: Queue<QueuedReply>,
commands: Queue<QueuedCommands>,
outcomes: Queue<CommandOutcome>,
}
impl Channel {
pub fn open(transport: &Arc<dyn Transport>) -> Result<Self, QueueError> {
let mut specs = queues().into_iter();
let mut next = || specs.next().unwrap_or_else(|| unreachable!("four queues"));
Ok(Self {
surfaces: Queue::open(Arc::clone(transport), next())?,
replies: Queue::open(Arc::clone(transport), next())?,
commands: Queue::open(Arc::clone(transport), next())?,
outcomes: Queue::open(Arc::clone(transport), next())?,
})
}
#[must_use]
pub fn surfaces(&self) -> &Queue<Surface> {
&self.surfaces
}
#[must_use]
pub fn replies(&self) -> &Queue<QueuedReply> {
&self.replies
}
#[must_use]
pub fn commands(&self) -> &Queue<QueuedCommands> {
&self.commands
}
#[must_use]
pub fn outcomes(&self) -> &Queue<CommandOutcome> {
&self.outcomes
}
pub fn push(&self, surface: &Surface) -> Result<Pushed<Surface>, QueueError> {
self.surfaces.push(surface)
}
pub fn claim(&self) -> Result<Option<Surface>, QueueError> {
Ok(self
.surfaces
.claim(&ConsumerName::default_consumer())?
.map(|claimed| claimed.record))
}
pub fn pending(&self) -> Result<Option<Surface>, QueueError> {
Ok(self
.surfaces
.pending(&ConsumerName::default_consumer())?
.map(|claimed| claimed.record))
}
pub fn abandon(&self, raised: &[u64]) -> Result<Vec<Surface>, QueueError> {
self.typed_surfaces(self.surfaces.raw().abandon(raised)?)
}
pub fn attend(&self, asker: &Asker) -> Result<Vec<Surface>, QueueError> {
self.typed_surfaces(self.surfaces.raw().attend(asker)?)
}
fn typed_surfaces(&self, records: Vec<Value>) -> Result<Vec<Surface>, QueueError> {
records
.into_iter()
.map(|record| {
serde_json::from_value(record).map_err(|failure| QueueError::Shape {
queue: name(SURFACES),
why: failure.to_string(),
})
})
.collect()
}
pub fn answer(&self, reply: &ReplyEnvelope, at: u64) -> Result<u64, QueueError> {
self.surfaces.raw().answer_pending()?;
let pushed = self.replies.push(&QueuedReply {
id: 0,
reply: reply.clone(),
at,
correlation: None,
})?;
Ok(pushed.id.unwrap_or_default())
}
pub fn answer_if_verdict(
&self,
reply: &ReplyEnvelope,
at: u64,
) -> Result<Option<u64>, QueueError> {
if reply.carries_verdict() {
return self.answer(reply, at).map(Some);
}
Ok(None)
}
pub fn claim_reply(&self) -> Result<Option<QueuedReply>, QueueError> {
Ok(self
.replies
.claim(&ConsumerName::default_consumer())?
.map(|claimed| claimed.record))
}
pub fn submit(
&self,
author: Author,
commands: Vec<Map<String, Value>>,
) -> Result<u64, QueueError> {
let pushed = self.commands.push(&QueuedCommands {
id: 0,
author,
commands,
})?;
Ok(pushed.id.unwrap_or_default())
}
pub fn claim_commands(&self) -> Result<Vec<QueuedCommands>, QueueError> {
let mut claimed = Vec::new();
while let Some(envelope) = self.commands.claim(&ConsumerName::default_consumer())? {
claimed.push(envelope.record);
}
Ok(claimed)
}
pub fn answer_commands(&self, outcome: &CommandOutcome) -> Result<Position, QueueError> {
Ok(self.outcomes.push(outcome)?.position)
}
pub fn outcome_of(&self, id: u64) -> Result<Option<CommandOutcome>, QueueError> {
for (record, _) in self.outcomes.raw().log(None)? {
let outcome = serde_json::from_value::<CommandOutcome>(record).map_err(|failure| {
QueueError::Shape {
queue: name(COMMAND_OUTCOMES),
why: failure.to_string(),
}
})?;
if outcome.id == id {
return Ok(Some(outcome));
}
}
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_layout_registers_the_channels_ids_beside_the_profiles() {
let own = registry();
let profile = onemessagebus_agent::registry();
let channel_ids = [
SURFACE_SCHEMA,
QUEUED_REPLY_SCHEMA,
QUEUED_COMMANDS_SCHEMA,
COMMAND_OUTCOME_SCHEMA,
SchemaId::literal("agent", "reply-envelope", 2),
SchemaId::literal("agent", "reply-envelope", 3),
];
for id in &channel_ids {
assert!(own.schema(id).is_some(), "{id} is not registered");
}
for id in profile.ids() {
assert!(
own.schema(&id).is_some(),
"the profile's {id} is not registered beside the layout's"
);
}
let surface = serde_json::json!({
"id": 3, "kind": "finding", "message": "m", "source": "proposal",
"blocking": false, "queued_at": 1, "asker": "a"
});
own.check(&SURFACE_SCHEMA, &surface)
.expect("a surface validates against the layout's document");
if profile.schema(&SURFACE_SCHEMA).is_some() {
profile
.check(&SURFACE_SCHEMA, &surface)
.expect("the same surface validates against the profile's document");
}
}
#[test]
fn a_read_version_is_carried_to_the_written_one_and_an_unread_one_is_left() {
for version in REPLY_ENVELOPE_VERSIONS_READ {
let envelope: ReplyEnvelope =
serde_json::from_value(serde_json::json!({"version": version}))
.expect("an envelope at a read version parses");
assert_eq!(envelope.version, Some(REPLY_ENVELOPE_VERSION));
}
let envelope: ReplyEnvelope = serde_json::from_value(serde_json::json!({"version": 1}))
.expect("an envelope at an unread version still parses");
assert_eq!(envelope.version, Some(1));
assert!(!envelope.carries_verdict());
assert!(!envelope.carries_edits_without_a_verdict());
}
#[test]
fn the_reply_envelope_documents_are_what_the_envelope_types_read() {
let generated = [
schemars::schema_for!(ReplyEnvelope).to_value(),
schemars::schema_for!(crate::channel::Reply).to_value(),
];
let wire = |property: &Value| -> Value {
match &property["type"] {
Value::Array(types) => {
let kept: Vec<&Value> = types.iter().filter(|t| *t != "null").collect();
serde_json::json!(kept[0])
}
Value::Null => property["$ref"].clone(),
other => other.clone(),
}
};
let command = &generated[1]["$defs"]["Command"]["oneOf"];
let op_fields: std::collections::BTreeSet<&str> = command
.as_array()
.expect("the command schema is a union of its ops")
.iter()
.flat_map(|op| op["properties"].as_object().into_iter().flatten())
.map(|(field, _)| field.as_str())
.collect();
for (version, document) in [(2, REPLY_ENVELOPE_V2), (3, REPLY_ENVELOPE_V3)] {
let document: Value = serde_json::from_str(document).expect("a committed schema");
assert_eq!(document["properties"]["version"]["const"], version);
assert_eq!(document["additionalProperties"], false);
let properties = document["properties"].as_object().expect("properties");
for schema in &generated {
let theirs = schema["properties"].as_object().expect("properties");
assert_eq!(
properties.keys().collect::<Vec<_>>(),
theirs.keys().collect::<Vec<_>>(),
"version {version}'s document names other fields than {}",
schema["title"]
);
for (field, property) in properties {
if field == "author" || field == "version" {
continue;
}
let theirs = wire(&theirs[field]);
let ours = wire(property);
assert!(
ours == theirs,
"version {version}'s `{field}` is {ours}, and {} reads {theirs}",
schema["title"]
);
}
}
let fields = document["$defs"]["Command"]["properties"]
.as_object()
.expect("the command's fields");
for field in fields.keys() {
assert!(
op_fields.contains(field.as_str()),
"version {version}'s document names a command field `{field}` no op carries"
);
}
}
}
#[test]
fn the_declared_files_are_the_files_the_queues_write() {
let dir =
std::env::temp_dir().join(format!("onepipeline-layout-files-{}", crate::sys::pid()));
let _ = std::fs::remove_dir_all(&dir);
let transport: Arc<dyn Transport> =
Arc::new(onemessagebus::LocalTransport::open(&dir).expect("the transport opens"));
let channel = Channel::open(&transport).expect("the channel opens");
channel
.push(&Surface {
id: 0,
kind: "planner-question".to_owned(),
message: "m".to_owned(),
source: source::PROPOSAL.to_owned(),
blocking: true,
queued_at: 1,
workstream: None,
abandoned: false,
asker: None,
correlation: None,
})
.expect("queued");
channel.claim().expect("a claim").expect("the surface");
channel
.answer(
&ReplyEnvelope {
message: Some("go".to_owned()),
..ReplyEnvelope::default()
},
2,
)
.expect("answered");
channel.claim_reply().expect("a claim").expect("the reply");
let mut command = Map::new();
command.insert("op".to_owned(), Value::from("finding"));
command.insert("message".to_owned(), Value::from("m"));
channel.submit(planner(), vec![command]).expect("submitted");
assert_eq!(channel.claim_commands().expect("claimed").len(), 1);
channel
.answer_commands(&CommandOutcome {
id: 0,
applied: true,
reason: None,
results: Vec::new(),
})
.expect("answered");
let mut written: Vec<String> = std::fs::read_dir(&dir)
.expect("the channel directory")
.flatten()
.filter(|entry| entry.path().is_file())
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.collect();
let _ = std::fs::remove_dir_all(&dir);
written.sort();
let mut declared: Vec<String> = FILES.iter().map(|file| (*file).to_owned()).collect();
declared.sort();
assert_eq!(written, declared);
}
#[test]
fn the_layouts_ops_are_the_engines_command_ops() {
let schema = schemars::schema_for!(crate::channel::Command).to_value();
let mut commands: Vec<&str> = schema["oneOf"]
.as_array()
.expect("the command schema is a union of its ops")
.iter()
.map(|op| {
op["properties"]["op"]["const"]
.as_str()
.expect("each op names its word")
})
.collect();
commands.sort_unstable();
let mut ops: Vec<&str> = Op::ALL.iter().map(|op| op.word()).collect();
ops.sort_unstable();
assert_eq!(ops, commands);
}
}