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