1use chrono::Utc;
7#[cfg(test)]
8use meerkat_core::comms::PeerId;
9use meerkat_core::interaction::{
10 InboxInteraction, InteractionContent, PeerIngressConvention, PeerIngressFact, PeerIngressKind,
11 PeerInputCandidate, PeerInputClass,
12};
13#[cfg(test)]
14use meerkat_core::interaction::{PeerIngressIdentity, ResponseStatus};
15use meerkat_core::lifecycle::InputId;
16
17use crate::identifiers::{CorrelationId, LogicalRuntimeId};
18use crate::input::{
19 ExternalEventInput, Input, InputDurability, InputHeader, InputOrigin, InputVisibility,
20 PeerConvention, PeerInput, ResponseProgressPhase, ResponseTerminalStatus,
21};
22
23#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
24pub enum PeerIngressProjectionError {
25 #[error(
26 "classified peer ingress {interaction_id} ({kind:?}) cannot project to a runtime PeerInput"
27 )]
28 UnsupportedPeerConvention {
29 interaction_id: meerkat_core::InteractionId,
30 kind: PeerIngressKind,
31 },
32 #[error("classified peer ingress {interaction_id} missing canonical peer id")]
33 MissingCanonicalPeerId {
34 interaction_id: meerkat_core::InteractionId,
35 },
36 #[error("classified peer response {interaction_id} missing machine response terminality")]
37 MissingResponseTerminality {
38 interaction_id: meerkat_core::InteractionId,
39 },
40 #[error(
41 "classified peer response {interaction_id} has unsupported machine response terminality"
42 )]
43 UnsupportedResponseTerminality {
44 interaction_id: meerkat_core::InteractionId,
45 },
46}
47
48pub fn classified_interaction_to_runtime_input(
51 classified: &PeerInputCandidate,
52 runtime_id: &LogicalRuntimeId,
53) -> Result<Input, PeerIngressProjectionError> {
54 let interaction = &classified.interaction;
55
56 if classified.class() == PeerInputClass::PlainEvent {
57 let source_name = classified
58 .ingress
59 .plain_event_source_name()
60 .unwrap_or("unknown");
61 let blocks = external_event_blocks(interaction);
62 return Ok(Input::ExternalEvent(ExternalEventInput {
63 header: InputHeader {
64 id: InputId::new(),
65 timestamp: Utc::now(),
66 source: InputOrigin::External {
67 source_name: source_name.to_string(),
68 },
69 durability: InputDurability::Durable,
70 visibility: InputVisibility {
71 transcript_eligible: true,
72 operator_eligible: true,
73 },
74 idempotency_key: None,
75 supersession_key: None,
76 correlation_id: Some(CorrelationId::from_uuid(interaction.id.0)),
77 },
78 event_type: source_name.to_string(),
79 payload: external_event_payload(interaction),
80 blocks,
81 handling_mode: interaction.handling_mode,
82 render_metadata: interaction.render_metadata.clone(),
83 objective_id: interaction.objective_id,
84 }));
85 }
86
87 peer_candidate_to_peer_input(classified, runtime_id)
88}
89
90fn peer_candidate_to_peer_input(
91 classified: &PeerInputCandidate,
92 runtime_id: &LogicalRuntimeId,
93) -> Result<Input, PeerIngressProjectionError> {
94 peer_input_from_ingress_fact(
95 &classified.interaction,
96 runtime_id,
97 &classified.ingress,
98 classified.response_terminality,
99 )
100}
101
102fn peer_input_from_ingress_fact(
103 interaction: &InboxInteraction,
104 runtime_id: &LogicalRuntimeId,
105 ingress: &PeerIngressFact,
106 response_terminality: Option<meerkat_core::interaction::TerminalityClass>,
107) -> Result<Input, PeerIngressProjectionError> {
108 let convention = map_ingress_convention(interaction.id, ingress, response_terminality)?;
109 let transcript_correlation_id = transcript_correlation_id(interaction, &convention);
110 let durability = map_durability(&convention);
111 let handling_mode = match &convention {
112 PeerConvention::ResponseProgress { .. } => None,
113 _ => Some(interaction.handling_mode),
114 };
115 let peer_id = ingress.canonical_peer_id_string().ok_or(
116 PeerIngressProjectionError::MissingCanonicalPeerId {
117 interaction_id: interaction.id,
118 },
119 )?;
120 let display_identity = ingress
121 .route
122 .as_ref()
123 .map(meerkat_core::PeerRoute::label)
124 .or_else(|| ingress.display_label());
125
126 Ok(Input::Peer(PeerInput {
127 directed_interaction_id: None,
128 injected_context: Vec::new(),
129 header: InputHeader {
130 id: InputId::new(),
131 timestamp: Utc::now(),
132 source: InputOrigin::Peer {
133 peer_id,
134 display_identity,
135 runtime_id: Some(runtime_id.clone()),
136 },
137 durability,
138 visibility: InputVisibility {
139 transcript_eligible: true,
140 operator_eligible: true,
141 },
142 idempotency_key: None,
143 supersession_key: None,
144 correlation_id: Some(CorrelationId::from_uuid(transcript_correlation_id.0)),
145 },
146 convention: Some(convention),
147 content: match peer_blocks(interaction) {
148 Some(blocks) => meerkat_core::types::ContentInput::Blocks(blocks),
149 None => meerkat_core::types::ContentInput::Text(peer_rendered_body(interaction)),
150 },
151 payload: peer_payload(interaction),
152 handling_mode,
153 sender_taint: interaction.sender_taint,
157 objective_id: interaction.objective_id,
158 }))
159}
160
161fn transcript_correlation_id(
162 interaction: &InboxInteraction,
163 convention: &PeerConvention,
164) -> meerkat_core::InteractionId {
165 match (convention, &interaction.content) {
166 (
167 PeerConvention::ResponseProgress { .. } | PeerConvention::ResponseTerminal { .. },
168 InteractionContent::Response { in_reply_to, .. },
169 ) => *in_reply_to,
170 _ => interaction.id,
171 }
172}
173
174fn map_ingress_convention(
175 interaction_id: meerkat_core::InteractionId,
176 ingress: &PeerIngressFact,
177 response_terminality: Option<meerkat_core::interaction::TerminalityClass>,
178) -> Result<PeerConvention, PeerIngressProjectionError> {
179 match &ingress.convention {
180 PeerIngressConvention::Message => Ok(PeerConvention::Message),
181 PeerIngressConvention::Request { request_id, intent } => Ok(PeerConvention::Request {
182 request_id: request_id.clone(),
183 intent: intent.clone(),
184 }),
185 PeerIngressConvention::Response {
186 in_reply_to,
187 status: _,
188 } => {
189 let terminality = response_terminality
190 .ok_or(PeerIngressProjectionError::MissingResponseTerminality { interaction_id })?;
191 map_response_convention(interaction_id, *in_reply_to, terminality)
192 }
193 PeerIngressConvention::Lifecycle { kind, .. } => Ok(PeerConvention::Request {
194 request_id: ingress.interaction_id.to_string(),
195 intent: kind.to_string(),
196 }),
197 PeerIngressConvention::Ack { .. } | PeerIngressConvention::PlainEvent { .. } => {
198 Err(PeerIngressProjectionError::UnsupportedPeerConvention {
199 interaction_id,
200 kind: ingress.kind,
201 })
202 }
203 }
204}
205
206fn map_response_convention(
207 interaction_id: meerkat_core::InteractionId,
208 in_reply_to: meerkat_core::InteractionId,
209 terminality: meerkat_core::interaction::TerminalityClass,
210) -> Result<PeerConvention, PeerIngressProjectionError> {
211 let request_id = in_reply_to.to_string();
212 Ok(match terminality {
213 meerkat_core::interaction::TerminalityClass::Progress => PeerConvention::ResponseProgress {
214 request_id,
215 phase: ResponseProgressPhase::Accepted,
216 },
217 meerkat_core::interaction::TerminalityClass::Terminal { disposition } => {
218 let term = match disposition {
219 meerkat_core::interaction::TerminalDisposition::Completed => {
220 ResponseTerminalStatus::Completed
221 }
222 meerkat_core::interaction::TerminalDisposition::Failed => {
223 ResponseTerminalStatus::Failed
224 }
225 _ => {
226 return Err(PeerIngressProjectionError::UnsupportedResponseTerminality {
227 interaction_id,
228 });
229 }
230 };
231 PeerConvention::ResponseTerminal {
232 request_id,
233 status: term,
234 }
235 }
236 _ => {
237 return Err(PeerIngressProjectionError::UnsupportedResponseTerminality {
238 interaction_id,
239 });
240 }
241 })
242}
243
244fn peer_rendered_body(interaction: &InboxInteraction) -> String {
245 if !interaction.rendered_text.trim().is_empty() {
246 return interaction.rendered_text.clone();
247 }
248 match &interaction.content {
249 InteractionContent::Message { body, .. }
250 | InteractionContent::IncarnationFencedMessage { body, .. } => body.clone(),
251 InteractionContent::Request { params, .. } => {
252 serde_json::to_string(params).unwrap_or_default()
253 }
254 InteractionContent::Response { result, .. } => {
255 serde_json::to_string(result).unwrap_or_default()
256 }
257 }
258}
259
260fn peer_blocks(interaction: &InboxInteraction) -> Option<Vec<meerkat_core::types::ContentBlock>> {
261 match &interaction.content {
262 InteractionContent::Message { blocks, .. }
263 | InteractionContent::IncarnationFencedMessage { blocks, .. } => blocks.clone(),
264 InteractionContent::Request { blocks, .. } => blocks.clone(),
265 InteractionContent::Response { blocks, .. } => blocks.clone(),
266 }
267}
268
269fn peer_payload(interaction: &InboxInteraction) -> Option<serde_json::Value> {
270 match &interaction.content {
271 InteractionContent::Message { .. }
272 | InteractionContent::IncarnationFencedMessage { .. } => None,
273 InteractionContent::Request { params, .. } => Some(params.clone()),
274 InteractionContent::Response { result, .. } => Some(result.clone()),
275 }
276}
277
278fn external_event_payload(interaction: &InboxInteraction) -> serde_json::Value {
279 match &interaction.content {
280 InteractionContent::Message { body, .. }
281 | InteractionContent::IncarnationFencedMessage { body, .. } => {
282 serde_json::json!({ "body": body })
283 }
284 InteractionContent::Request { intent, params, .. } => {
285 serde_json::json!({ "intent": intent, "params": params })
286 }
287 InteractionContent::Response {
288 in_reply_to,
289 status,
290 result,
291 blocks,
292 } => serde_json::json!({
293 "in_reply_to": in_reply_to,
294 "status": status,
295 "result": result,
296 "blocks": blocks,
297 }),
298 }
299}
300
301fn external_event_blocks(
302 interaction: &InboxInteraction,
303) -> Option<Vec<meerkat_core::types::ContentBlock>> {
304 match &interaction.content {
305 InteractionContent::Message { blocks, .. }
306 | InteractionContent::IncarnationFencedMessage { blocks, .. } => blocks.clone(),
307 InteractionContent::Request { blocks, .. } => blocks.clone(),
308 _ => None,
309 }
310}
311
312fn map_durability(convention: &PeerConvention) -> InputDurability {
313 match convention {
314 PeerConvention::ResponseProgress { .. } => InputDurability::Ephemeral,
315 _ => InputDurability::Durable,
316 }
317}
318
319#[cfg(test)]
320#[allow(clippy::unwrap_used, clippy::panic)]
321mod tests {
322 use super::*;
323 use meerkat_core::interaction::{PeerIngressIdentity, ResponseStatus};
324
325 fn make_interaction_id() -> meerkat_core::interaction::InteractionId {
326 meerkat_core::interaction::InteractionId(meerkat_core::time_compat::new_uuid_v7())
327 }
328
329 fn plain_event_ingress(
330 id: meerkat_core::interaction::InteractionId,
331 source_name: &str,
332 ) -> PeerIngressFact {
333 PeerIngressFact::plain_event(
334 id,
335 source_name,
336 PeerInputClass::PlainEvent,
337 meerkat_core::PeerIngressKind::PlainEvent,
338 )
339 }
340
341 fn test_peer_id() -> PeerId {
342 PeerId::parse("22222222-2222-4222-8222-222222222222").expect("canonical test peer id")
343 }
344
345 fn peer_kind_for_convention(
346 convention: &PeerIngressConvention,
347 ) -> meerkat_core::PeerIngressKind {
348 match convention {
349 PeerIngressConvention::Message => meerkat_core::PeerIngressKind::Message,
350 PeerIngressConvention::Request { .. } | PeerIngressConvention::Lifecycle { .. } => {
351 meerkat_core::PeerIngressKind::Request
352 }
353 PeerIngressConvention::Response { .. } => meerkat_core::PeerIngressKind::Response,
354 PeerIngressConvention::Ack { .. } => meerkat_core::PeerIngressKind::Ack,
355 PeerIngressConvention::PlainEvent { .. } => meerkat_core::PeerIngressKind::PlainEvent,
356 }
357 }
358
359 fn peer_ingress(
360 id: meerkat_core::interaction::InteractionId,
361 peer_id: PeerId,
362 label: &str,
363 class: PeerInputClass,
364 convention: PeerIngressConvention,
365 ) -> PeerIngressFact {
366 let kind = peer_kind_for_convention(&convention);
367 PeerIngressFact::peer(
368 id,
369 class,
370 kind,
371 Some(meerkat_core::PeerIngressAuthDecision::Required),
372 PeerIngressIdentity::new(peer_id, label, convention),
373 )
374 }
375
376 fn candidate_for_interaction(interaction: InboxInteraction) -> PeerInputCandidate {
377 let peer_id = interaction.from_route.unwrap_or_else(test_peer_id);
378 crate::test_peer_input_candidate_from_interaction(interaction, peer_id)
379 }
380
381 fn peer_input_for_test(interaction: &InboxInteraction, runtime_id: &LogicalRuntimeId) -> Input {
382 let candidate = candidate_for_interaction(interaction.clone());
383 classified_interaction_to_runtime_input(&candidate, runtime_id)
384 .expect("test candidate should project to runtime input")
385 }
386
387 #[test]
388 fn message_to_peer_input() {
389 let interaction = InboxInteraction {
390 objective_id: None,
391 sender_taint: None,
392 from_route: None,
393 from: "peer-1".into(),
394 content: InteractionContent::Message {
395 body: "hello".into(),
396 blocks: None,
397 },
398 id: make_interaction_id(),
399 rendered_text: String::new(),
400 handling_mode: meerkat_core::types::HandlingMode::Queue,
401 render_metadata: None,
402 };
403 let input = peer_input_for_test(&interaction, &LogicalRuntimeId::new("test"));
404 if let Input::Peer(p) = &input {
405 assert!(matches!(p.convention, Some(PeerConvention::Message)));
406 assert_eq!(p.content.text_content(), "hello");
407 assert_eq!(
408 p.header.correlation_id,
409 Some(CorrelationId::from_uuid(interaction.id.0)),
410 "plain peer messages must use the inbound interaction id as the live/history dedup key",
411 );
412 assert_eq!(p.header.durability, InputDurability::Durable);
413 assert_eq!(
414 p.handling_mode,
415 Some(meerkat_core::types::HandlingMode::Queue),
416 "explicit queue must survive comms -> runtime projection so it can suppress running-turn interruption"
417 );
418 } else {
419 panic!("Expected PeerInput");
420 }
421 }
422
423 #[test]
428 fn message_sender_taint_threads_to_peer_input() {
429 use meerkat_core::comms::SenderContentTaint;
430
431 for declared in [
432 Some(SenderContentTaint::Tainted),
433 Some(SenderContentTaint::Clean),
434 None,
435 ] {
436 let interaction = InboxInteraction {
437 objective_id: None,
438 sender_taint: declared,
439 from_route: None,
440 from: "peer-1".into(),
441 content: InteractionContent::Message {
442 body: "hello".into(),
443 blocks: None,
444 },
445 id: make_interaction_id(),
446 rendered_text: String::new(),
447 handling_mode: meerkat_core::types::HandlingMode::Queue,
448 render_metadata: None,
449 };
450 let input = peer_input_for_test(&interaction, &LogicalRuntimeId::new("test"));
451 let Input::Peer(p) = &input else {
452 panic!("Expected PeerInput");
453 };
454 assert_eq!(
455 p.sender_taint, declared,
456 "sender taint must ride the classified projection unchanged"
457 );
458 }
459 }
460
461 #[test]
462 fn request_to_peer_input() {
463 let interaction = InboxInteraction {
464 objective_id: None,
465 sender_taint: None,
466 from_route: None,
467 from: "peer-1".into(),
468 content: InteractionContent::Request {
469 intent: "mob.peer_added".into(),
470 params: serde_json::json!({"peer": "agent-1"}),
473 blocks: None,
474 },
475 id: make_interaction_id(),
476 rendered_text: String::new(),
477 handling_mode: meerkat_core::types::HandlingMode::Queue,
478 render_metadata: None,
479 };
480 let input = peer_input_for_test(&interaction, &LogicalRuntimeId::new("test"));
481 if let Input::Peer(p) = &input {
482 assert!(matches!(p.convention, Some(PeerConvention::Request { .. })));
483 match p.convention.as_ref() {
484 Some(PeerConvention::Request { request_id, .. }) => {
485 assert_eq!(request_id, &interaction.id.0.to_string());
486 }
487 other => panic!("Expected request convention, got {other:?}"),
488 }
489 assert_eq!(p.header.durability, InputDurability::Durable);
490 assert_eq!(
491 p.payload,
492 Some(serde_json::json!({"peer": "agent-1"})),
493 "request params must remain structured on PeerInput so runtime prompt projection does not depend on pre-rendered comms prose"
494 );
495 assert_eq!(
496 p.handling_mode,
497 Some(meerkat_core::types::HandlingMode::Queue),
498 "explicit queue request semantics must not collapse to default policy"
499 );
500 } else {
501 panic!("Expected PeerInput");
502 }
503 }
504
505 #[test]
506 fn classified_request_uses_canonical_peer_id_for_runtime_projection() {
507 let source_peer_id =
508 PeerId::parse("11111111-1111-4111-8111-111111111111").expect("canonical peer id");
509 let request_id = make_interaction_id();
510 let classified = PeerInputCandidate {
511 interaction: InboxInteraction {
512 objective_id: None,
513 sender_taint: None,
514 from_route: None,
515 from: "test-mob/lead/l-requester".into(),
516 content: InteractionContent::Request {
517 intent: "interpret_image".into(),
518 params: serde_json::json!({"description": "tower with a light"}),
519 blocks: None,
520 },
521 id: request_id,
522 rendered_text: "stale helper prose".into(),
523 handling_mode: meerkat_core::types::HandlingMode::Steer,
524 render_metadata: None,
525 },
526 ingress: PeerIngressFact::peer(
527 request_id,
528 PeerInputClass::ActionableRequest,
529 meerkat_core::PeerIngressKind::Request,
530 Some(meerkat_core::PeerIngressAuthDecision::Required),
531 PeerIngressIdentity::new(
532 source_peer_id,
533 "test-mob/lead/l-requester",
534 PeerIngressConvention::Request {
535 request_id: request_id.to_string(),
536 intent: "interpret_image".to_string(),
537 },
538 ),
539 ),
540 lifecycle_peer: None,
541 response_terminality: None,
542 };
543
544 let input =
545 classified_interaction_to_runtime_input(&classified, &LogicalRuntimeId::new("worker"))
546 .expect("classified request should project to peer input");
547 let Input::Peer(peer) = &input else {
548 panic!("Expected PeerInput");
549 };
550 let InputOrigin::Peer { peer_id, .. } = &peer.header.source else {
551 panic!("Expected peer source");
552 };
553 assert_eq!(peer_id, "11111111-1111-4111-8111-111111111111");
554 assert_eq!(peer.content.text_content(), "stale helper prose");
555
556 let prompt = crate::input::input_prompt_text(&input);
557 assert!(prompt.starts_with(
558 "Peer request from peer_id 11111111-1111-4111-8111-111111111111 (display_name: test-mob/lead/l-requester)."
559 ));
560 assert!(prompt.contains("\"peer_id\":\"11111111-1111-4111-8111-111111111111\""));
561 assert!(prompt.contains("\"display_name\":\"test-mob/lead/l-requester\""));
562 assert!(prompt.contains(&format!("\"in_reply_to\":\"{}\"", request_id.0)));
563 assert!(prompt.contains("\"status\":\"completed\""));
564 assert!(!prompt.contains("to=\""));
565 }
566
567 #[test]
568 fn plain_event_to_external_event_input() {
569 let id = make_interaction_id();
570 let classified = PeerInputCandidate {
571 lifecycle_peer: None,
572 response_terminality: None,
573 ingress: plain_event_ingress(id, "webhook"),
574 interaction: InboxInteraction {
575 objective_id: None,
576 sender_taint: None,
577 from_route: None,
578 from: "event:webhook".into(),
579 content: InteractionContent::Message {
580 body: "{\"ok\":true}".into(),
581 blocks: None,
582 },
583 id,
584 rendered_text: String::new(),
585 handling_mode: meerkat_core::types::HandlingMode::Queue,
586 render_metadata: None,
587 },
588 };
589 let input =
590 classified_interaction_to_runtime_input(&classified, &LogicalRuntimeId::new("test"))
591 .expect("plain event should project to external event input");
592 match input {
593 Input::ExternalEvent(event) => {
594 assert_eq!(event.event_type, "webhook");
595 assert_eq!(event.payload["body"], "{\"ok\":true}");
596 assert_eq!(event.blocks, None);
597 assert_eq!(
598 event.handling_mode,
599 meerkat_core::types::HandlingMode::Queue
600 );
601 assert_eq!(event.render_metadata, None);
602 }
603 other => panic!("Expected ExternalEvent input, got {other:?}"),
604 }
605 }
606
607 #[test]
608 fn peer_named_event_prefix_stays_peer_without_plain_event_class() {
609 let id = make_interaction_id();
610 let classified = PeerInputCandidate {
611 lifecycle_peer: None,
612 response_terminality: None,
613 ingress: peer_ingress(
614 id,
615 test_peer_id(),
616 "event:webhook",
617 PeerInputClass::ActionableMessage,
618 PeerIngressConvention::Message,
619 ),
620 interaction: InboxInteraction {
621 objective_id: None,
622 sender_taint: None,
623 from_route: None,
624 from: "event:webhook".into(),
625 content: InteractionContent::Message {
626 body: "hello".into(),
627 blocks: None,
628 },
629 id,
630 rendered_text: "stale rendered text".into(),
631 handling_mode: meerkat_core::types::HandlingMode::Queue,
632 render_metadata: None,
633 },
634 };
635 let input =
636 classified_interaction_to_runtime_input(&classified, &LogicalRuntimeId::new("test"))
637 .expect("classified peer event should project to peer input");
638 match input {
639 Input::Peer(peer) => {
640 assert_eq!(peer.content.text_content(), "stale rendered text");
641 match peer.header.source {
642 InputOrigin::Peer { peer_id, .. } => {
643 assert_eq!(peer_id, test_peer_id().as_str());
644 }
645 other => panic!("Expected peer source, got {other:?}"),
646 }
647 }
648 other => panic!("Expected Peer input, got {other:?}"),
649 }
650 }
651
652 #[test]
653 fn classified_peer_projection_uses_ingress_canonical_peer_id_not_display_from() {
654 let id = make_interaction_id();
655 let canonical_peer_id = meerkat_core::comms::PeerId::new();
656 let classified = PeerInputCandidate {
657 lifecycle_peer: None,
658 response_terminality: None,
659 ingress: PeerIngressFact::peer(
660 id,
661 PeerInputClass::ActionableRequest,
662 meerkat_core::PeerIngressKind::Request,
663 Some(meerkat_core::PeerIngressAuthDecision::Required),
664 PeerIngressIdentity::new(
665 canonical_peer_id,
666 "display-agent",
667 PeerIngressConvention::Request {
668 request_id: id.to_string(),
669 intent: "review".to_string(),
670 },
671 ),
672 ),
673 interaction: InboxInteraction {
674 objective_id: None,
675 sender_taint: None,
676 from_route: None,
677 from: "display-agent".into(),
678 content: InteractionContent::Request {
679 intent: "review".into(),
680 params: serde_json::json!({"pr": 42}),
681 blocks: None,
682 },
683 id,
684 rendered_text: "stale rendered text".into(),
685 handling_mode: meerkat_core::types::HandlingMode::Queue,
686 render_metadata: None,
687 },
688 };
689
690 let input =
691 classified_interaction_to_runtime_input(&classified, &LogicalRuntimeId::new("test"))
692 .expect("classified peer projection should use typed canonical id");
693 let Input::Peer(peer) = input else {
694 panic!("Expected Peer input");
695 };
696 match peer.header.source {
697 InputOrigin::Peer { peer_id, .. } => {
698 assert_eq!(peer_id, canonical_peer_id.as_str());
699 assert_ne!(peer_id, "display-agent");
700 }
701 other => panic!("Expected peer source, got {other:?}"),
702 }
703 assert_eq!(peer.content.text_content(), "stale rendered text");
704 }
705
706 #[test]
707 fn classified_peer_projection_rejects_display_only_ingress_identity() {
708 let id = make_interaction_id();
709 let classified = PeerInputCandidate {
710 lifecycle_peer: None,
711 response_terminality: None,
712 ingress: PeerIngressFact {
713 interaction_id: id,
714 class: PeerInputClass::ActionableMessage,
715 kind: meerkat_core::PeerIngressKind::Message,
716 canonical_peer_id: None,
717 display_name: meerkat_core::comms::PeerName::new("display-agent".to_string()).ok(),
718 signing_pubkey: None,
719 route: None,
720 declared_reply_endpoint: None,
721 auth: Some(meerkat_core::PeerIngressAuthDecision::Required),
722 convention: PeerIngressConvention::Message,
723 },
724 interaction: InboxInteraction {
725 objective_id: None,
726 sender_taint: None,
727 from_route: None,
728 from: "display-agent".into(),
729 content: InteractionContent::Message {
730 body: "hello".into(),
731 blocks: None,
732 },
733 id,
734 rendered_text: "stale rendered text".into(),
735 handling_mode: meerkat_core::types::HandlingMode::Queue,
736 render_metadata: None,
737 },
738 };
739
740 let result =
741 classified_interaction_to_runtime_input(&classified, &LogicalRuntimeId::new("test"));
742 assert!(
743 matches!(
744 result,
745 Err(PeerIngressProjectionError::MissingCanonicalPeerId { interaction_id })
746 if interaction_id == id
747 ),
748 "display-only ingress must fail closed, got {result:?}"
749 );
750 }
751
752 #[test]
753 fn request_body_preserves_rendered_text_and_structured_payload() {
754 let interaction = InboxInteraction {
755 objective_id: None,
756 sender_taint: None,
757 from_route: None,
758 from: "event:webhook".into(),
759 content: InteractionContent::Request {
760 intent: "mob.peer_added".into(),
761 params: serde_json::json!({"peer":"agent-1"}),
762 blocks: None,
763 },
764 id: make_interaction_id(),
765 rendered_text: "stale rendered text".into(),
766 handling_mode: meerkat_core::types::HandlingMode::Queue,
767 render_metadata: None,
768 };
769 let input = peer_input_for_test(&interaction, &LogicalRuntimeId::new("test"));
770 if let Input::Peer(peer) = input {
771 assert_eq!(peer.content.text_content(), "stale rendered text");
772 assert_eq!(peer.payload, Some(serde_json::json!({"peer":"agent-1"})));
773 } else {
774 panic!("Expected PeerInput");
775 }
776 }
777
778 #[test]
779 fn message_blocks_are_preserved_on_peer_input() {
780 let blocks = vec![
781 meerkat_core::types::ContentBlock::Text {
782 text: "see image".into(),
783 },
784 meerkat_core::types::ContentBlock::Image {
785 media_type: "image/png".into(),
786 data: "abc".into(),
787 },
788 ];
789 let interaction = InboxInteraction {
790 objective_id: None,
791 sender_taint: None,
792 from_route: None,
793 from: "peer-1".into(),
794 content: InteractionContent::Message {
795 body: "see image".into(),
796 blocks: Some(blocks.clone()),
797 },
798 id: make_interaction_id(),
799 rendered_text: "stale rendered text".into(),
800 handling_mode: meerkat_core::types::HandlingMode::Queue,
801 render_metadata: None,
802 };
803 let input = peer_input_for_test(&interaction, &LogicalRuntimeId::new("test"));
804 if let Input::Peer(peer) = input {
805 assert_eq!(
808 peer.content,
809 meerkat_core::types::ContentInput::Blocks(blocks)
810 );
811 } else {
812 panic!("Expected PeerInput");
813 }
814 }
815
816 #[test]
817 fn request_blocks_are_preserved_on_peer_input() {
818 let blocks = vec![
819 meerkat_core::types::ContentBlock::Text {
820 text: "describe this image".into(),
821 },
822 meerkat_core::types::ContentBlock::Image {
823 media_type: "image/png".into(),
824 data: "abc".into(),
825 },
826 ];
827 let interaction_id = make_interaction_id();
828 let peer_id = PeerId::new();
829 let classified = PeerInputCandidate {
830 interaction: InboxInteraction {
831 objective_id: None,
832 sender_taint: None,
833 from_route: Some(peer_id),
834 from: "vision-peer".into(),
835 content: InteractionContent::Request {
836 intent: "checksum_token".into(),
837 params: serde_json::json!({"subject": "describe-image"}),
838 blocks: Some(blocks.clone()),
839 },
840 id: interaction_id,
841 rendered_text: String::new(),
842 handling_mode: meerkat_core::types::HandlingMode::Steer,
843 render_metadata: None,
844 },
845 ingress: PeerIngressFact::peer(
846 interaction_id,
847 PeerInputClass::ActionableRequest,
848 PeerIngressKind::Request,
849 Some(meerkat_core::interaction::PeerIngressAuthDecision::Required),
850 PeerIngressIdentity::new(
851 peer_id,
852 "vision-peer",
853 meerkat_core::interaction::PeerIngressConvention::Request {
854 request_id: interaction_id.to_string(),
855 intent: "checksum_token".to_string(),
856 },
857 ),
858 ),
859 lifecycle_peer: None,
860 response_terminality: None,
861 };
862
863 let input = classified_interaction_to_runtime_input(
864 &classified,
865 &LogicalRuntimeId::new("runtime-a"),
866 )
867 .expect("classified request should project");
868 if let Input::Peer(peer) = input {
869 assert_eq!(
870 peer.content,
871 meerkat_core::types::ContentInput::Blocks(blocks)
872 );
873 assert_eq!(
874 peer.payload,
875 Some(serde_json::json!({"subject": "describe-image"}))
876 );
877 } else {
878 panic!("Expected PeerInput");
879 }
880 }
881
882 #[test]
883 fn multimodal_message_blocks_own_content_with_derived_text_projection() {
884 let blocks = vec![
885 meerkat_core::types::ContentBlock::Text {
886 text: "caption text".into(),
887 },
888 meerkat_core::types::ContentBlock::Image {
889 media_type: "image/png".into(),
890 data: "abc".into(),
891 },
892 ];
893 let interaction = InboxInteraction {
894 objective_id: None,
895 sender_taint: None,
896 from_route: None,
897 from: "peer-1".into(),
898 content: InteractionContent::Message {
899 body: "please inspect this image".into(),
900 blocks: Some(blocks.clone()),
901 },
902 id: make_interaction_id(),
903 rendered_text: "stale rendered text".into(),
904 handling_mode: meerkat_core::types::HandlingMode::Queue,
905 render_metadata: None,
906 };
907 let input = peer_input_for_test(&interaction, &LogicalRuntimeId::new("test"));
908 if let Input::Peer(peer) = input {
909 assert_eq!(
912 peer.content,
913 meerkat_core::types::ContentInput::Blocks(blocks)
914 );
915 assert_eq!(
916 peer.content.text_content(),
917 "caption text\n[image: image/png]"
918 );
919 } else {
920 panic!("Expected PeerInput");
921 }
922 }
923
924 #[test]
925 fn plain_event_blocks_are_preserved_on_external_event_input() {
926 let blocks = vec![
927 meerkat_core::types::ContentBlock::Text {
928 text: "see image".into(),
929 },
930 meerkat_core::types::ContentBlock::Image {
931 media_type: "image/png".into(),
932 data: "abc".into(),
933 },
934 ];
935 let id = make_interaction_id();
936 let classified = PeerInputCandidate {
937 lifecycle_peer: None,
938 response_terminality: None,
939 ingress: plain_event_ingress(id, "webhook"),
940 interaction: InboxInteraction {
941 objective_id: None,
942 sender_taint: None,
943 from_route: None,
944 from: "event:webhook".into(),
945 content: InteractionContent::Message {
946 body: "see image".into(),
947 blocks: Some(blocks.clone()),
948 },
949 id,
950 rendered_text: "stale rendered text".into(),
951 handling_mode: meerkat_core::types::HandlingMode::Queue,
952 render_metadata: None,
953 },
954 };
955 let input =
956 classified_interaction_to_runtime_input(&classified, &LogicalRuntimeId::new("test"))
957 .expect("plain event with blocks should project");
958 match input {
959 Input::ExternalEvent(event) => {
960 assert_eq!(event.payload["body"], "see image");
961 assert!(event.payload.get("blocks").is_none());
962 assert_eq!(event.blocks, Some(blocks));
963 assert_eq!(
964 event.handling_mode,
965 meerkat_core::types::HandlingMode::Queue
966 );
967 assert_eq!(event.render_metadata, None);
968 }
969 other => panic!("Expected ExternalEvent input, got {other:?}"),
970 }
971 }
972
973 #[test]
974 fn plain_event_preserves_handling_mode_and_render_metadata() {
975 let render_metadata = meerkat_core::types::RenderMetadata {
976 class: meerkat_core::types::RenderClass::ExternalEvent,
977 salience: meerkat_core::types::RenderSalience::Urgent,
978 };
979 let id = make_interaction_id();
980 let classified = PeerInputCandidate {
981 lifecycle_peer: None,
982 response_terminality: None,
983 ingress: plain_event_ingress(id, "webhook"),
984 interaction: InboxInteraction {
985 objective_id: None,
986 sender_taint: None,
987 from_route: None,
988 from: "event:webhook".into(),
989 content: InteractionContent::Message {
990 body: "urgent".into(),
991 blocks: None,
992 },
993 id,
994 rendered_text: "stale rendered text".into(),
995 handling_mode: meerkat_core::types::HandlingMode::Steer,
996 render_metadata: Some(render_metadata.clone()),
997 },
998 };
999
1000 match classified_interaction_to_runtime_input(&classified, &LogicalRuntimeId::new("test"))
1001 .expect("plain event should preserve render metadata")
1002 {
1003 Input::ExternalEvent(event) => {
1004 assert_eq!(
1005 event.handling_mode,
1006 meerkat_core::types::HandlingMode::Steer
1007 );
1008 assert_eq!(event.render_metadata, Some(render_metadata));
1009 }
1010 other => panic!("Expected ExternalEvent input, got {other:?}"),
1011 }
1012 }
1013
1014 #[test]
1015 fn response_completed_to_terminal() {
1016 let in_reply_to = make_interaction_id();
1017 let route_id = meerkat_core::comms::PeerId::from_uuid(
1018 uuid::Uuid::parse_str("018f6f79-7a82-7c4e-a552-a3b86f9630f2").unwrap(),
1019 );
1020 let interaction = InboxInteraction {
1021 objective_id: None,
1022 sender_taint: None,
1023 from_route: Some(route_id),
1024 from: "Peer One".into(),
1025 content: InteractionContent::Response {
1026 status: ResponseStatus::Completed,
1027 result: serde_json::json!({"ok": true}),
1028 in_reply_to,
1029 blocks: None,
1030 },
1031 id: make_interaction_id(),
1032 rendered_text: String::new(),
1033 handling_mode: meerkat_core::types::HandlingMode::Queue,
1034 render_metadata: None,
1035 };
1036 let input = peer_input_for_test(&interaction, &LogicalRuntimeId::new("test"));
1037 if let Input::Peer(p) = &input {
1038 match &p.header.source {
1039 InputOrigin::Peer {
1040 peer_id,
1041 display_identity,
1042 ..
1043 } => {
1044 assert_eq!(peer_id, &route_id.to_string());
1045 assert_eq!(display_identity.as_deref(), Some("Peer One"));
1046 }
1047 other => panic!("Expected Peer source, got {other:?}"),
1048 }
1049 assert!(matches!(
1050 p.convention,
1051 Some(PeerConvention::ResponseTerminal {
1052 status: ResponseTerminalStatus::Completed,
1053 ..
1054 })
1055 ));
1056 assert_eq!(
1057 p.header.correlation_id,
1058 Some(CorrelationId::from_uuid(in_reply_to.0)),
1059 "terminal peer responses must use the request interaction id that InteractionComplete reports",
1060 );
1061 assert_eq!(p.header.durability, InputDurability::Durable);
1062 assert_eq!(
1063 p.payload,
1064 Some(serde_json::json!({"ok": true})),
1065 "terminal response result must remain structured on PeerInput so runtime prompt projection stays runtime-owned"
1066 );
1067 } else {
1068 panic!("Expected PeerInput");
1069 }
1070 let projection = crate::input::runtime_input_projection_for_machine_batch(&input);
1071 let context = projection
1072 .context_append
1073 .expect("terminal machine-batch context projection");
1074 let expected_key = format!("peer_response_terminal:{route_id}:{in_reply_to}");
1075 assert_eq!(context.key, expected_key);
1076 let meerkat_core::lifecycle::run_primitive::CoreRenderable::SystemNotice { blocks, .. } =
1077 context.content
1078 else {
1079 panic!("Expected terminal context notice");
1080 };
1081 assert!(matches!(
1082 blocks.first(),
1083 Some(meerkat_core::types::SystemNoticeBlock::Comms { peer, .. })
1084 if peer.as_ref().and_then(|peer| peer.display_name.as_deref()) == Some("Peer One")
1085 ));
1086 }
1087
1088 #[test]
1089 fn classified_response_uses_ingress_terminal_class() {
1090 let in_reply_to = make_interaction_id();
1091 let id = make_interaction_id();
1092 let classified = PeerInputCandidate {
1093 interaction: InboxInteraction {
1094 objective_id: None,
1095 sender_taint: None,
1096 from_route: None,
1097 from: "peer-1".into(),
1098 content: InteractionContent::Response {
1099 status: ResponseStatus::Completed,
1100 result: serde_json::json!({"ok": true}),
1101 in_reply_to,
1102 blocks: None,
1103 },
1104 id,
1105 rendered_text: String::new(),
1106 handling_mode: meerkat_core::types::HandlingMode::Queue,
1107 render_metadata: None,
1108 },
1109 ingress: PeerIngressFact::peer(
1110 id,
1111 PeerInputClass::ResponseProgress,
1112 meerkat_core::PeerIngressKind::Response,
1113 Some(meerkat_core::PeerIngressAuthDecision::Required),
1114 PeerIngressIdentity::new(
1115 test_peer_id(),
1116 "peer-1",
1117 PeerIngressConvention::Response {
1118 in_reply_to,
1119 status: ResponseStatus::Completed,
1120 },
1121 ),
1122 ),
1123 lifecycle_peer: None,
1124 response_terminality: Some(meerkat_core::TerminalityClass::Progress),
1125 };
1126
1127 let input =
1128 classified_interaction_to_runtime_input(&classified, &LogicalRuntimeId::new("test"))
1129 .expect("classified response should project");
1130 if let Input::Peer(peer) = input {
1131 assert!(
1132 matches!(
1133 peer.convention,
1134 Some(PeerConvention::ResponseProgress { .. })
1135 ),
1136 "classified bridge must consume ingress-owned response class"
1137 );
1138 } else {
1139 panic!("Expected PeerInput");
1140 }
1141 }
1142
1143 #[test]
1144 fn classified_response_missing_machine_terminality_fails_closed() {
1145 let in_reply_to = make_interaction_id();
1146 let id = make_interaction_id();
1147 let classified = PeerInputCandidate {
1148 interaction: InboxInteraction {
1149 objective_id: None,
1150 sender_taint: None,
1151 from_route: None,
1152 from: "peer-1".into(),
1153 content: InteractionContent::Response {
1154 status: ResponseStatus::Completed,
1155 result: serde_json::json!({"ok": true}),
1156 in_reply_to,
1157 blocks: None,
1158 },
1159 id,
1160 rendered_text: String::new(),
1161 handling_mode: meerkat_core::types::HandlingMode::Queue,
1162 render_metadata: None,
1163 },
1164 ingress: PeerIngressFact::peer(
1165 id,
1166 PeerInputClass::ResponseTerminal,
1167 meerkat_core::PeerIngressKind::Response,
1168 Some(meerkat_core::PeerIngressAuthDecision::Required),
1169 PeerIngressIdentity::new(
1170 test_peer_id(),
1171 "peer-1",
1172 PeerIngressConvention::Response {
1173 in_reply_to,
1174 status: ResponseStatus::Completed,
1175 },
1176 ),
1177 ),
1178 lifecycle_peer: None,
1179 response_terminality: None,
1180 };
1181
1182 let result =
1183 classified_interaction_to_runtime_input(&classified, &LogicalRuntimeId::new("test"));
1184 assert!(
1185 matches!(
1186 result,
1187 Err(PeerIngressProjectionError::MissingResponseTerminality { interaction_id })
1188 if interaction_id == id
1189 ),
1190 "runtime projection must not infer public terminality from raw status: {result:?}"
1191 );
1192 }
1193
1194 #[test]
1195 fn response_terminal_without_canonical_peer_id_fails_typed_projection() {
1196 let in_reply_to = make_interaction_id();
1197 let interaction_id = make_interaction_id();
1198 let candidate = PeerInputCandidate {
1199 interaction: InboxInteraction {
1200 objective_id: None,
1201 sender_taint: None,
1202 from_route: None,
1203 from: "Peer One".into(),
1204 content: InteractionContent::Response {
1205 status: ResponseStatus::Completed,
1206 result: serde_json::json!({"ok": true}),
1207 in_reply_to,
1208 blocks: None,
1209 },
1210 id: interaction_id,
1211 rendered_text: String::new(),
1212 handling_mode: meerkat_core::types::HandlingMode::Queue,
1213 render_metadata: None,
1214 },
1215 ingress: PeerIngressFact {
1216 interaction_id,
1217 class: PeerInputClass::ResponseTerminal,
1218 kind: meerkat_core::PeerIngressKind::Response,
1219 canonical_peer_id: None,
1220 display_name: meerkat_core::comms::PeerName::new("Peer One".to_string()).ok(),
1221 signing_pubkey: None,
1222 route: None,
1223 declared_reply_endpoint: None,
1224 auth: Some(meerkat_core::PeerIngressAuthDecision::Required),
1225 convention: PeerIngressConvention::Response {
1226 in_reply_to,
1227 status: ResponseStatus::Completed,
1228 },
1229 },
1230 lifecycle_peer: None,
1231 response_terminality: Some(meerkat_core::TerminalityClass::Terminal {
1232 disposition: meerkat_core::TerminalDisposition::Completed,
1233 }),
1234 };
1235 let err =
1236 classified_interaction_to_runtime_input(&candidate, &LogicalRuntimeId::new("test"))
1237 .unwrap_err();
1238 assert!(matches!(
1239 err,
1240 PeerIngressProjectionError::MissingCanonicalPeerId { .. }
1241 ));
1242 }
1243
1244 #[test]
1245 fn response_failed_to_terminal() {
1246 let in_reply_to = make_interaction_id();
1247 let route_id = meerkat_core::comms::PeerId::from_uuid(
1248 uuid::Uuid::parse_str("018f6f79-7a82-7c4e-a552-a3b86f9630f3").unwrap(),
1249 );
1250 let interaction = InboxInteraction {
1251 objective_id: None,
1252 sender_taint: None,
1253 from_route: Some(route_id),
1254 from: "peer-1".into(),
1255 content: InteractionContent::Response {
1256 status: ResponseStatus::Failed,
1257 result: serde_json::json!({"error": "timeout"}),
1258 in_reply_to,
1259 blocks: None,
1260 },
1261 id: make_interaction_id(),
1262 rendered_text: String::new(),
1263 handling_mode: meerkat_core::types::HandlingMode::Queue,
1264 render_metadata: None,
1265 };
1266 let input = peer_input_for_test(&interaction, &LogicalRuntimeId::new("test"));
1267 if let Input::Peer(p) = &input {
1268 assert!(matches!(
1269 p.convention,
1270 Some(PeerConvention::ResponseTerminal {
1271 status: ResponseTerminalStatus::Failed,
1272 ..
1273 })
1274 ));
1275 } else {
1276 panic!("Expected PeerInput");
1277 }
1278 }
1279
1280 #[test]
1281 fn response_accepted_to_progress() {
1282 let in_reply_to = make_interaction_id();
1283 let interaction = InboxInteraction {
1284 objective_id: None,
1285 sender_taint: None,
1286 from_route: None,
1287 from: "peer-1".into(),
1288 content: InteractionContent::Response {
1289 status: ResponseStatus::Accepted,
1290 result: serde_json::json!(null),
1291 in_reply_to,
1292 blocks: None,
1293 },
1294 id: make_interaction_id(),
1295 rendered_text: String::new(),
1296 handling_mode: meerkat_core::types::HandlingMode::Queue,
1297 render_metadata: None,
1298 };
1299 let input = peer_input_for_test(&interaction, &LogicalRuntimeId::new("test"));
1300 if let Input::Peer(p) = &input {
1301 assert!(matches!(
1302 p.convention,
1303 Some(PeerConvention::ResponseProgress {
1304 phase: ResponseProgressPhase::Accepted,
1305 ..
1306 })
1307 ));
1308 assert_eq!(
1309 p.header.correlation_id,
1310 Some(CorrelationId::from_uuid(in_reply_to.0)),
1311 "progress peer responses must share the same request correlation as terminal response completion",
1312 );
1313 assert_eq!(p.header.durability, InputDurability::Ephemeral);
1314 assert!(
1315 p.handling_mode.is_none(),
1316 "ResponseProgress inputs must not carry handling_mode"
1317 );
1318 } else {
1319 panic!("Expected PeerInput");
1320 }
1321 }
1322
1323 #[test]
1324 fn classified_response_uses_ingress_terminality_over_raw_status() {
1325 let in_reply_to = make_interaction_id();
1326 let id = make_interaction_id();
1327 let classified = PeerInputCandidate {
1328 interaction: InboxInteraction {
1329 objective_id: None,
1330 sender_taint: None,
1331 from_route: None,
1332 from: "peer-1".into(),
1333 content: InteractionContent::Response {
1334 status: ResponseStatus::Completed,
1335 result: serde_json::json!({"ok": true}),
1336 in_reply_to,
1337 blocks: None,
1338 },
1339 id,
1340 rendered_text: String::new(),
1341 handling_mode: meerkat_core::types::HandlingMode::Queue,
1342 render_metadata: None,
1343 },
1344 ingress: PeerIngressFact::peer(
1345 id,
1346 PeerInputClass::ResponseProgress,
1347 meerkat_core::PeerIngressKind::Response,
1348 Some(meerkat_core::PeerIngressAuthDecision::Required),
1349 PeerIngressIdentity::new(
1350 test_peer_id(),
1351 "peer-1",
1352 PeerIngressConvention::Response {
1353 in_reply_to,
1354 status: ResponseStatus::Completed,
1355 },
1356 ),
1357 ),
1358 lifecycle_peer: None,
1359 response_terminality: Some(meerkat_core::TerminalityClass::Progress),
1360 };
1361
1362 let input =
1363 classified_interaction_to_runtime_input(&classified, &LogicalRuntimeId::new("test"))
1364 .expect("classified response should project");
1365
1366 if let Input::Peer(p) = &input {
1367 assert!(matches!(
1368 p.convention,
1369 Some(PeerConvention::ResponseProgress {
1370 phase: ResponseProgressPhase::Accepted,
1371 ..
1372 })
1373 ));
1374 assert_eq!(p.header.durability, InputDurability::Ephemeral);
1375 assert_eq!(p.handling_mode, None);
1376 } else {
1377 panic!("Expected PeerInput");
1378 }
1379 }
1380
1381 #[test]
1382 fn peer_source_includes_runtime_id() {
1383 let interaction = InboxInteraction {
1384 objective_id: None,
1385 sender_taint: None,
1386 from_route: None,
1387 from: "peer-1".into(),
1388 content: InteractionContent::Message {
1389 body: "hi".into(),
1390 blocks: None,
1391 },
1392 id: make_interaction_id(),
1393 rendered_text: String::new(),
1394 handling_mode: meerkat_core::types::HandlingMode::Queue,
1395 render_metadata: None,
1396 };
1397 let input = peer_input_for_test(&interaction, &LogicalRuntimeId::new("agent-runtime-1"));
1398 if let Input::Peer(p) = &input {
1399 if let InputOrigin::Peer {
1400 peer_id,
1401 display_identity,
1402 runtime_id,
1403 ..
1404 } = &p.header.source
1405 {
1406 assert_eq!(peer_id, &test_peer_id().as_str());
1407 assert_eq!(display_identity.as_deref(), Some("peer-1"));
1408 assert_eq!(runtime_id.as_ref().unwrap().0, "agent-runtime-1");
1409 } else {
1410 panic!("Expected Peer source");
1411 }
1412 } else {
1413 panic!("Expected PeerInput");
1414 }
1415 }
1416
1417 #[test]
1418 fn all_interaction_types_produce_valid_inputs() {
1419 let in_reply_to = make_interaction_id();
1420 let interactions = vec![
1421 InboxInteraction {
1422 objective_id: None,
1423 sender_taint: None,
1424 from_route: None,
1425 from: "p".into(),
1426 content: InteractionContent::Message {
1427 body: "m".into(),
1428 blocks: None,
1429 },
1430 id: make_interaction_id(),
1431 rendered_text: String::new(),
1432 handling_mode: meerkat_core::types::HandlingMode::Queue,
1433 render_metadata: None,
1434 },
1435 InboxInteraction {
1436 objective_id: None,
1437 sender_taint: None,
1438 from_route: None,
1439 from: "p".into(),
1440 content: InteractionContent::Request {
1441 intent: "i".into(),
1442 params: serde_json::json!({}),
1443 blocks: None,
1444 },
1445 id: make_interaction_id(),
1446 rendered_text: String::new(),
1447 handling_mode: meerkat_core::types::HandlingMode::Queue,
1448 render_metadata: None,
1449 },
1450 InboxInteraction {
1451 objective_id: None,
1452 sender_taint: None,
1453 from_route: Some(meerkat_core::comms::PeerId::from_uuid(
1454 uuid::Uuid::parse_str("018f6f79-7a82-7c4e-a552-a3b86f9630f6").unwrap(),
1455 )),
1456 from: "p".into(),
1457 content: InteractionContent::Response {
1458 status: ResponseStatus::Completed,
1459 result: serde_json::json!(null),
1460 in_reply_to,
1461 blocks: None,
1462 },
1463 id: make_interaction_id(),
1464 rendered_text: String::new(),
1465 handling_mode: meerkat_core::types::HandlingMode::Queue,
1466 render_metadata: None,
1467 },
1468 ];
1469
1470 let rid = LogicalRuntimeId::new("test");
1471 for interaction in &interactions {
1472 let input = peer_input_for_test(interaction, &rid);
1473 assert!(matches!(input, Input::Peer(_)));
1474 }
1475 }
1476}