use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeSet;
use uuid::Uuid;
use crate::comms::{
PeerId, PeerLifecycleKind, PeerName, PeerRoute, SUPERVISOR_BRIDGE_INTENT, TrustedPeerDescriptor,
};
use crate::types::{ContentBlock, HandlingMode, RenderMetadata};
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct InteractionId(#[cfg_attr(feature = "schema", schemars(with = "String"))] pub Uuid);
impl std::fmt::Display for InteractionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResponseStatus {
Accepted,
Completed,
Failed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum TerminalityClass {
Progress,
Terminal { disposition: TerminalDisposition },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum TerminalDisposition {
Completed,
Failed,
}
pub fn classify_response_terminality(status: ResponseStatus) -> TerminalityClass {
match status {
ResponseStatus::Accepted => TerminalityClass::Progress,
ResponseStatus::Completed => TerminalityClass::Terminal {
disposition: TerminalDisposition::Completed,
},
ResponseStatus::Failed => TerminalityClass::Terminal {
disposition: TerminalDisposition::Failed,
},
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InteractionContent {
Message {
body: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
blocks: Option<Vec<ContentBlock>>,
},
Request { intent: String, params: Value },
Response {
in_reply_to: InteractionId,
status: ResponseStatus,
result: Value,
},
}
#[derive(Debug, Clone)]
pub struct InboxInteraction {
pub id: InteractionId,
pub from_route: Option<PeerId>,
pub from: String,
pub content: InteractionContent,
pub rendered_text: String,
pub handling_mode: HandlingMode,
pub render_metadata: Option<RenderMetadata>,
}
pub fn format_external_event_projection(source_name: &str, body: Option<&str>) -> String {
let label = format!("[EVENT via {source_name}]");
let body = body.map(str::trim).filter(|body| !body.is_empty());
match body {
Some(body) => format!("{label} {body}"),
None => label,
}
}
pub fn format_peer_message_projection(from_peer: &str, body: &str) -> String {
format!("[COMMS MESSAGE from {from_peer}]\n{body}")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SendResponseCallProjection {
pub peer_id: PeerId,
pub display_name: Option<String>,
pub in_reply_to: String,
}
impl SendResponseCallProjection {
pub const TOOL_NAME: &'static str = "send_response";
pub const PEER_ID_FIELD: &'static str = "peer_id";
pub const DISPLAY_NAME_FIELD: &'static str = "display_name";
pub const IN_REPLY_TO_FIELD: &'static str = "in_reply_to";
pub const STATUS_FIELD: &'static str = "status";
pub const RESULT_FIELD: &'static str = "result";
pub fn new(
peer_id: PeerId,
display_name: Option<&str>,
in_reply_to: impl Into<String>,
) -> Self {
Self {
peer_id,
display_name: display_name
.map(str::trim)
.filter(|name| !name.is_empty())
.map(ToOwned::to_owned),
in_reply_to: in_reply_to.into(),
}
}
pub fn completed_example_args(&self) -> Value {
let mut args = serde_json::Map::new();
args.insert(
Self::PEER_ID_FIELD.to_string(),
Value::String(self.peer_id.to_string()),
);
if let Some(display_name) = &self.display_name {
args.insert(
Self::DISPLAY_NAME_FIELD.to_string(),
Value::String(display_name.clone()),
);
}
args.insert(
Self::IN_REPLY_TO_FIELD.to_string(),
Value::String(self.in_reply_to.clone()),
);
args.insert(
Self::STATUS_FIELD.to_string(),
Value::String("completed".to_string()),
);
Value::Object(args)
}
pub fn instruction_text(&self) -> String {
let args = serde_json::to_string(&self.completed_example_args())
.unwrap_or_else(|_| "{}".to_string());
format!(
"Reply with {} with arguments {args}. Use status=\"failed\" instead of \"completed\" when the request cannot be fulfilled, and include result only when the request contract provides a typed result payload.",
Self::TOOL_NAME
)
}
}
pub fn format_peer_request_projection(
from_peer_id: PeerId,
display_name: Option<&str>,
request_id: impl std::fmt::Display,
intent: &str,
params: &Value,
) -> String {
let params_str = if params.is_null() || matches!(params, Value::Object(map) if map.is_empty()) {
String::new()
} else {
format!(
"\nParams: {}",
serde_json::to_string_pretty(params).unwrap_or_default()
)
};
let request_id = request_id.to_string();
let display_suffix = display_name
.map(str::trim)
.filter(|name| !name.is_empty())
.map(|name| format!(" (display_name: {name})"))
.unwrap_or_default();
let response_call =
SendResponseCallProjection::new(from_peer_id, display_name, request_id.clone());
format!(
"[COMMS REQUEST from peer_id {from_peer_id}{display_suffix} (id: {request_id})]\n\
Intent: {intent}{params_str}\n\
\n\
This is a correlated peer request. {} \
Do not answer this request with send_message.",
response_call.instruction_text()
)
}
pub fn format_peer_response_projection(
from_peer: &str,
in_reply_to: impl std::fmt::Display,
status: ResponseStatus,
result: &Value,
) -> String {
let status_str = match status {
ResponseStatus::Accepted => "accepted",
ResponseStatus::Completed => "completed",
ResponseStatus::Failed => "failed",
};
let result_str = if result.is_null() || matches!(result, Value::Object(map) if map.is_empty()) {
String::new()
} else {
format!(
"\nResult: {}",
serde_json::to_string_pretty(result).unwrap_or_default()
)
};
format!(
"[COMMS RESPONSE from {from_peer} (to request: {in_reply_to})]\n\
Status: {status_str}{result_str}"
)
}
pub fn format_peer_ack_projection(from_peer: &str, in_reply_to: impl std::fmt::Display) -> String {
format!("[COMMS ACK from {from_peer} (to request: {in_reply_to})]")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PeerInputClass {
ActionableMessage,
ActionableRequest,
ResponseProgress,
ResponseTerminal,
PeerLifecycleAdded,
PeerLifecycleRetired,
PeerLifecycleUnwired,
PeerLifecycleKickoffFailed,
PeerLifecycleKickoffCancelled,
SilentRequest,
Ack,
PlainEvent,
}
impl PeerInputClass {
pub fn is_actionable(&self) -> bool {
matches!(
self,
Self::ActionableMessage
| Self::ActionableRequest
| Self::ResponseProgress
| Self::ResponseTerminal
| Self::PlainEvent
| Self::PeerLifecycleKickoffFailed
| Self::PeerLifecycleKickoffCancelled
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum PeerIngressAuthExemption {
SupervisorBridge,
}
impl PeerIngressAuthExemption {
pub const fn intent(self) -> &'static str {
match self {
Self::SupervisorBridge => SUPERVISOR_BRIDGE_INTENT,
}
}
pub fn matches_intent(self, intent: &str) -> bool {
self.intent() == intent
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PeerIngressAuthDecision {
Required,
Exempt(PeerIngressAuthExemption),
}
impl PeerIngressAuthDecision {
pub const fn is_exempt(self) -> bool {
matches!(self, Self::Exempt(_))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PeerIngressConvention {
Message,
Request {
request_id: String,
intent: String,
},
Response {
in_reply_to: InteractionId,
status: ResponseStatus,
},
Ack {
in_reply_to: InteractionId,
},
Lifecycle {
kind: PeerLifecycleKind,
peer: String,
},
PlainEvent {
source_name: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeerIngressFact {
pub interaction_id: InteractionId,
pub class: PeerInputClass,
pub kind: PeerIngressKind,
pub canonical_peer_id: Option<PeerId>,
pub display_name: Option<PeerName>,
pub signing_pubkey: Option<[u8; 32]>,
pub route: Option<PeerRoute>,
pub auth: Option<PeerIngressAuthDecision>,
pub convention: PeerIngressConvention,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeerIngressIdentity {
pub canonical_peer_id: PeerId,
pub display_label: String,
pub signing_pubkey: Option<[u8; 32]>,
pub convention: PeerIngressConvention,
}
impl PeerIngressIdentity {
pub fn new(
canonical_peer_id: PeerId,
display_label: impl Into<String>,
convention: PeerIngressConvention,
) -> Self {
Self {
canonical_peer_id,
display_label: display_label.into(),
signing_pubkey: None,
convention,
}
}
pub fn with_signing_pubkey(mut self, signing_pubkey: [u8; 32]) -> Self {
self.signing_pubkey = Some(signing_pubkey);
self
}
}
impl PeerIngressFact {
pub fn peer(
interaction_id: InteractionId,
class: PeerInputClass,
kind: PeerIngressKind,
auth: Option<PeerIngressAuthDecision>,
identity: PeerIngressIdentity,
) -> Self {
let PeerIngressIdentity {
canonical_peer_id,
display_label,
signing_pubkey,
convention,
} = identity;
let display_name = PeerName::new(display_label).ok();
let route = Some(match &display_name {
Some(name) => PeerRoute::with_display_name(canonical_peer_id, name.clone()),
None => PeerRoute::new(canonical_peer_id),
});
Self {
interaction_id,
class,
kind,
canonical_peer_id: Some(canonical_peer_id),
display_name,
signing_pubkey,
route,
auth,
convention,
}
}
pub fn plain_event(
interaction_id: InteractionId,
source_name: impl Into<String>,
class: PeerInputClass,
kind: PeerIngressKind,
) -> Self {
let source_name = source_name.into();
Self {
interaction_id,
class,
kind,
canonical_peer_id: None,
display_name: None,
signing_pubkey: None,
route: None,
auth: None,
convention: PeerIngressConvention::PlainEvent { source_name },
}
}
pub fn legacy_peer_label(
interaction_id: InteractionId,
label: impl Into<String>,
class: PeerInputClass,
kind: PeerIngressKind,
auth: Option<PeerIngressAuthDecision>,
convention: PeerIngressConvention,
) -> Self {
let label = label.into();
let canonical_peer_id = PeerId::parse(&label).ok();
let display_name = PeerName::new(label).ok();
let route = canonical_peer_id.map(|peer_id| match &display_name {
Some(name) => PeerRoute::with_display_name(peer_id, name.clone()),
None => PeerRoute::new(peer_id),
});
Self {
interaction_id,
class,
kind,
canonical_peer_id,
display_name,
signing_pubkey: None,
route,
auth,
convention,
}
}
pub fn canonical_peer_id_string(&self) -> Option<String> {
self.canonical_peer_id.map(|peer_id| peer_id.as_str())
}
pub fn display_label(&self) -> Option<String> {
self.display_name.as_ref().map(PeerName::as_string)
}
pub fn diagnostic_label(&self) -> String {
self.display_label()
.or_else(|| self.canonical_peer_id_string())
.unwrap_or_else(|| "<unknown-peer-ingress>".to_string())
}
pub fn plain_event_source_name(&self) -> Option<&str> {
match &self.convention {
PeerIngressConvention::PlainEvent { source_name } => Some(source_name.as_str()),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeerIngressClassification {
pub class: PeerInputClass,
pub kind: PeerIngressKind,
pub auth: PeerIngressAuthDecision,
pub lifecycle_kind: Option<PeerLifecycleKind>,
pub response_terminality: Option<TerminalityClass>,
}
impl PeerIngressClassification {
pub const fn required(class: PeerInputClass, kind: PeerIngressKind) -> Self {
Self {
class,
kind,
auth: PeerIngressAuthDecision::Required,
lifecycle_kind: None,
response_terminality: None,
}
}
pub const fn lifecycle(kind: PeerLifecycleKind) -> Self {
let class = match kind {
PeerLifecycleKind::PeerAdded => PeerInputClass::PeerLifecycleAdded,
PeerLifecycleKind::PeerRetired => PeerInputClass::PeerLifecycleRetired,
PeerLifecycleKind::PeerUnwired => PeerInputClass::PeerLifecycleUnwired,
};
Self {
class,
kind: PeerIngressKind::Request,
auth: PeerIngressAuthDecision::Required,
lifecycle_kind: Some(kind),
response_terminality: None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PeerIngressEnvelopeFacts {
pub item_id: String,
pub from_peer: String,
pub from_peer_id: PeerId,
pub kind: PeerIngressEnvelopeKind,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PeerIngressEnvelopeKind {
Message {
body: String,
},
Request {
intent: String,
params: Value,
},
Lifecycle {
kind: PeerLifecycleKind,
params: Value,
},
Response {
in_reply_to: String,
status: ResponseStatus,
result: Value,
},
Ack {
in_reply_to: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeerIngressPlainEventFacts {
pub source_name: String,
pub body: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeerIngressAdmission {
pub classification: PeerIngressClassification,
pub lifecycle_peer: Option<String>,
pub request_id: Option<String>,
pub rendered_text: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeerIngressMachinePolicy {
silent_request_intents: BTreeSet<String>,
auth_exemptions: BTreeSet<PeerIngressAuthExemption>,
}
impl Default for PeerIngressMachinePolicy {
fn default() -> Self {
Self::from_silent_intents(std::iter::empty::<String>())
}
}
impl PeerIngressMachinePolicy {
pub fn from_silent_intents<I, S>(silent_intents: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
silent_request_intents: silent_intents.into_iter().map(Into::into).collect(),
auth_exemptions: BTreeSet::from([PeerIngressAuthExemption::SupervisorBridge]),
}
}
pub fn classify_message(&self) -> PeerIngressClassification {
PeerIngressClassification::required(
PeerInputClass::ActionableMessage,
PeerIngressKind::Message,
)
}
pub fn classify_request_intent(&self, intent: &str) -> PeerIngressClassification {
let auth = self
.auth_exemptions
.iter()
.copied()
.find(|exemption| exemption.matches_intent(intent))
.map(PeerIngressAuthDecision::Exempt)
.unwrap_or(PeerIngressAuthDecision::Required);
let mut classification = if let Some(kind) = classify_lifecycle_intent(intent) {
PeerIngressClassification::lifecycle(kind)
} else if self.silent_request_intents.contains(intent) {
PeerIngressClassification::required(
PeerInputClass::SilentRequest,
PeerIngressKind::Request,
)
} else {
PeerIngressClassification::required(
PeerInputClass::ActionableRequest,
PeerIngressKind::Request,
)
};
classification.auth = auth;
classification
}
pub fn classify_lifecycle(&self, kind: PeerLifecycleKind) -> PeerIngressClassification {
PeerIngressClassification::lifecycle(kind)
}
pub fn classify_response(&self, status: ResponseStatus) -> PeerIngressClassification {
let terminality = classify_response_terminality(status);
let class = match terminality {
TerminalityClass::Progress => PeerInputClass::ResponseProgress,
TerminalityClass::Terminal { .. } => PeerInputClass::ResponseTerminal,
};
let mut classification =
PeerIngressClassification::required(class, PeerIngressKind::Response);
classification.response_terminality = Some(terminality);
classification
}
pub fn classify_ack(&self) -> PeerIngressClassification {
PeerIngressClassification::required(PeerInputClass::Ack, PeerIngressKind::Ack)
}
pub fn classify_plain_event(&self) -> PeerIngressClassification {
PeerIngressClassification::required(PeerInputClass::PlainEvent, PeerIngressKind::PlainEvent)
}
pub fn classify_external_envelope(
&self,
facts: &PeerIngressEnvelopeFacts,
) -> PeerIngressAdmission {
match &facts.kind {
PeerIngressEnvelopeKind::Message { .. } => {
let classification = self.classify_message();
PeerIngressAdmission {
rendered_text: render_peer_ingress_admitted_text(facts, &classification),
classification,
lifecycle_peer: None,
request_id: None,
}
}
PeerIngressEnvelopeKind::Request { intent, params } => {
let classification = self.classify_request_intent(intent);
let lifecycle_peer = classification
.lifecycle_kind
.map(|_| peer_lifecycle_subject(params, facts.from_peer.as_str()));
let rendered_text = render_peer_ingress_admitted_text(facts, &classification);
PeerIngressAdmission {
classification,
lifecycle_peer,
request_id: Some(facts.item_id.clone()),
rendered_text,
}
}
PeerIngressEnvelopeKind::Lifecycle { kind, params } => {
let classification = self.classify_lifecycle(*kind);
PeerIngressAdmission {
rendered_text: render_peer_ingress_admitted_text(facts, &classification),
classification,
lifecycle_peer: Some(peer_lifecycle_subject(params, facts.from_peer.as_str())),
request_id: None,
}
}
PeerIngressEnvelopeKind::Response {
in_reply_to,
status,
..
} => {
let classification = self.classify_response(*status);
PeerIngressAdmission {
rendered_text: render_peer_ingress_admitted_text(facts, &classification),
classification,
lifecycle_peer: None,
request_id: Some(in_reply_to.clone()),
}
}
PeerIngressEnvelopeKind::Ack { in_reply_to } => {
let classification = self.classify_ack();
PeerIngressAdmission {
rendered_text: render_peer_ingress_admitted_text(facts, &classification),
classification,
lifecycle_peer: None,
request_id: Some(in_reply_to.clone()),
}
}
}
}
pub fn classify_plain_event_facts(
&self,
facts: &PeerIngressPlainEventFacts,
) -> PeerIngressAdmission {
PeerIngressAdmission {
classification: self.classify_plain_event(),
lifecycle_peer: None,
request_id: None,
rendered_text: format_external_event_projection(&facts.source_name, Some(&facts.body)),
}
}
}
pub fn render_peer_ingress_admitted_text(
facts: &PeerIngressEnvelopeFacts,
classification: &PeerIngressClassification,
) -> String {
match &facts.kind {
PeerIngressEnvelopeKind::Message { body } => {
format_peer_message_projection(&facts.from_peer, body)
}
PeerIngressEnvelopeKind::Request { intent, params } => {
if classification.lifecycle_kind.is_some() {
String::new()
} else {
format_peer_request_projection(
facts.from_peer_id,
Some(&facts.from_peer),
facts.item_id.as_str(),
intent,
params,
)
}
}
PeerIngressEnvelopeKind::Lifecycle { .. } => String::new(),
PeerIngressEnvelopeKind::Response {
in_reply_to,
status,
result,
} => format_peer_response_projection(&facts.from_peer, in_reply_to, *status, result),
PeerIngressEnvelopeKind::Ack { in_reply_to } => {
format_peer_ack_projection(&facts.from_peer, in_reply_to)
}
}
}
pub fn peer_lifecycle_subject(params: &Value, fallback_peer: &str) -> String {
params
.get("peer")
.and_then(Value::as_str)
.filter(|peer| !peer.is_empty())
.unwrap_or(fallback_peer)
.to_string()
}
fn classify_lifecycle_intent(intent: &str) -> Option<PeerLifecycleKind> {
if intent == PeerLifecycleKind::PeerAdded.as_str() {
Some(PeerLifecycleKind::PeerAdded)
} else if intent == PeerLifecycleKind::PeerRetired.as_str() {
Some(PeerLifecycleKind::PeerRetired)
} else if intent == PeerLifecycleKind::PeerUnwired.as_str() {
Some(PeerLifecycleKind::PeerUnwired)
} else {
None
}
}
#[derive(Debug, Clone)]
pub struct PeerInputCandidate {
pub interaction: InboxInteraction,
pub ingress: PeerIngressFact,
pub lifecycle_peer: Option<String>,
pub response_terminality: Option<TerminalityClass>,
}
impl PeerInputCandidate {
pub fn new(
interaction: InboxInteraction,
ingress: PeerIngressFact,
lifecycle_peer: Option<String>,
) -> Self {
Self {
interaction,
ingress,
lifecycle_peer,
response_terminality: None,
}
}
pub fn class(&self) -> PeerInputClass {
self.ingress.class
}
pub fn kind(&self) -> PeerIngressKind {
self.ingress.kind
}
pub fn auth(&self) -> Option<PeerIngressAuthDecision> {
self.ingress.auth
}
}
pub type ClassifiedInboxInteraction = PeerInputCandidate;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PeerIngressKind {
Message,
Request,
Response,
Ack,
PlainEvent,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeerIngressDiagnosticDisplay(String);
impl PeerIngressDiagnosticDisplay {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for PeerIngressDiagnosticDisplay {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PeerIngressAdmissionDiagnostic {
TrustedAtAdmission,
UntrustedAtAdmission,
}
impl PeerIngressAdmissionDiagnostic {
pub const fn from_trusted(trusted: bool) -> Self {
if trusted {
Self::TrustedAtAdmission
} else {
Self::UntrustedAtAdmission
}
}
pub const fn trusted_at_admission(self) -> bool {
matches!(self, Self::TrustedAtAdmission)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeerIngressEntrySnapshot {
pub raw_item_id: InteractionId,
pub interaction_id: Option<InteractionId>,
pub class: PeerInputClass,
pub kind: PeerIngressKind,
pub from_peer_display: Option<PeerIngressDiagnosticDisplay>,
pub canonical_peer_id: Option<PeerId>,
pub display_name: Option<PeerName>,
pub signing_pubkey: Option<[u8; 32]>,
pub route: Option<PeerRoute>,
pub lifecycle_peer_display: Option<PeerIngressDiagnosticDisplay>,
pub request_correlation_id: Option<InteractionId>,
pub auth: Option<PeerIngressAuthDecision>,
pub admission_diagnostic: Option<PeerIngressAdmissionDiagnostic>,
pub response_terminality: Option<TerminalityClass>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PeerIngressQueueSnapshot {
pub total_count: usize,
pub actionable_count: usize,
pub response_count: usize,
pub lifecycle_count: usize,
pub silent_request_count: usize,
pub ack_count: usize,
pub plain_event_count: usize,
pub queued_entries: Vec<PeerIngressEntrySnapshot>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PeerIngressAuthorityPhase {
#[default]
Absent,
Received,
Dropped,
Delivered,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeerIngressRuntimeSnapshot {
pub self_peer_id: crate::comms::PeerId,
pub auth_required: bool,
pub authority_phase: PeerIngressAuthorityPhase,
pub trusted_peers: Vec<TrustedPeerDescriptor>,
pub submission_queue_len: usize,
pub queue: PeerIngressQueueSnapshot,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn interaction_id_json_roundtrip() {
let id = InteractionId(Uuid::new_v4());
let json = serde_json::to_string(&id).unwrap();
let parsed: InteractionId = serde_json::from_str(&json).unwrap();
assert_eq!(id, parsed);
}
#[test]
fn interaction_content_message_json_roundtrip() {
let content = InteractionContent::Message {
body: "hello".to_string(),
blocks: None,
};
let json = serde_json::to_value(&content).unwrap();
assert_eq!(json["type"], "message");
let parsed: InteractionContent = serde_json::from_value(json).unwrap();
assert_eq!(content, parsed);
}
#[test]
fn interaction_content_request_json_roundtrip() {
let content = InteractionContent::Request {
intent: "review".to_string(),
params: serde_json::json!({"pr": 42}),
};
let json = serde_json::to_value(&content).unwrap();
assert_eq!(json["type"], "request");
let parsed: InteractionContent = serde_json::from_value(json).unwrap();
assert_eq!(content, parsed);
}
#[test]
fn interaction_content_response_json_roundtrip() {
let id = InteractionId(Uuid::new_v4());
let content = InteractionContent::Response {
in_reply_to: id,
status: ResponseStatus::Completed,
result: serde_json::json!({"ok": true}),
};
let json = serde_json::to_value(&content).unwrap();
assert_eq!(json["type"], "response");
assert_eq!(json["status"], "completed");
let parsed: InteractionContent = serde_json::from_value(json).unwrap();
assert_eq!(content, parsed);
}
#[test]
fn response_status_json_roundtrip_all_variants() {
for (variant, expected_str) in [
(ResponseStatus::Accepted, "accepted"),
(ResponseStatus::Completed, "completed"),
(ResponseStatus::Failed, "failed"),
] {
let json = serde_json::to_value(variant).unwrap();
assert_eq!(json, expected_str);
let parsed: ResponseStatus = serde_json::from_value(json).unwrap();
assert_eq!(variant, parsed);
}
}
#[test]
fn classify_response_terminality_covers_all_variants() {
assert_eq!(
classify_response_terminality(ResponseStatus::Accepted),
TerminalityClass::Progress
);
assert_eq!(
classify_response_terminality(ResponseStatus::Completed),
TerminalityClass::Terminal {
disposition: TerminalDisposition::Completed
}
);
assert_eq!(
classify_response_terminality(ResponseStatus::Failed),
TerminalityClass::Terminal {
disposition: TerminalDisposition::Failed
}
);
}
#[test]
fn peer_ingress_policy_owns_response_terminal_classification() {
let policy = PeerIngressMachinePolicy::default();
assert_eq!(
policy.classify_response(ResponseStatus::Accepted).class,
PeerInputClass::ResponseProgress
);
assert_eq!(
policy.classify_response(ResponseStatus::Completed).class,
PeerInputClass::ResponseTerminal
);
assert_eq!(
policy.classify_response(ResponseStatus::Failed).class,
PeerInputClass::ResponseTerminal
);
}
#[test]
fn peer_ingress_policy_auth_exempts_supervisor_bridge() {
let policy = PeerIngressMachinePolicy::default();
let classification = policy.classify_request_intent(crate::SUPERVISOR_BRIDGE_INTENT);
assert_eq!(classification.class, PeerInputClass::ActionableRequest);
assert_eq!(
classification.auth,
PeerIngressAuthDecision::Exempt(PeerIngressAuthExemption::SupervisorBridge)
);
}
#[test]
fn peer_ingress_policy_owns_lifecycle_and_silent_routing() {
let policy = PeerIngressMachinePolicy::from_silent_intents(["probe.silent"]);
let lifecycle = policy.classify_request_intent(PeerLifecycleKind::PeerUnwired.as_str());
assert_eq!(lifecycle.class, PeerInputClass::PeerLifecycleUnwired);
assert_eq!(
lifecycle.lifecycle_kind,
Some(PeerLifecycleKind::PeerUnwired)
);
let silent = policy.classify_request_intent("probe.silent");
assert_eq!(silent.class, PeerInputClass::SilentRequest);
assert_eq!(silent.auth, PeerIngressAuthDecision::Required);
}
#[test]
fn interaction_message_with_blocks_roundtrip() {
let content = InteractionContent::Message {
body: "hello".to_string(),
blocks: Some(vec![
ContentBlock::Text {
text: "hello".to_string(),
},
ContentBlock::Image {
media_type: "image/png".to_string(),
data: "iVBORw0KGgo=".into(),
},
]),
};
let json = serde_json::to_value(&content).unwrap();
assert_eq!(json["type"], "message");
assert!(json["blocks"].is_array());
let parsed: InteractionContent = serde_json::from_value(json).unwrap();
assert_eq!(content, parsed);
}
#[test]
fn inbox_interaction_preserves_runtime_hints() {
let interaction = InboxInteraction {
id: InteractionId(Uuid::new_v4()),
from_route: None,
from: "event:webhook".into(),
content: InteractionContent::Message {
body: "hello".into(),
blocks: None,
},
rendered_text: "[EVENT via webhook] hello".into(),
handling_mode: HandlingMode::Steer,
render_metadata: Some(RenderMetadata {
class: crate::types::RenderClass::SystemNotice,
salience: crate::types::RenderSalience::Urgent,
}),
};
assert_eq!(interaction.handling_mode, HandlingMode::Steer);
assert!(interaction.render_metadata.is_some());
}
#[test]
fn interaction_message_without_blocks_compat() {
let old_json = r#"{"type":"message","body":"hello"}"#;
let parsed: InteractionContent = serde_json::from_str(old_json).unwrap();
match parsed {
InteractionContent::Message { body, blocks } => {
assert_eq!(body, "hello");
assert_eq!(blocks, None);
}
other => panic!("Expected Message, got {other:?}"),
}
let content = InteractionContent::Message {
body: "test".to_string(),
blocks: None,
};
let json = serde_json::to_string(&content).unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(
value.get("blocks").is_none(),
"blocks: None should not appear in JSON"
);
}
}