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