Skip to main content

meerkat_runtime/
comms_bridge.rs

1//! Runtime comms bridge helpers.
2//!
3//! These helpers translate drained comms interactions into the runtime-owned
4//! input families used by the comms classification bridge.
5
6use 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
48/// Convert a classified comms interaction into the appropriate runtime-owned
49/// input family.
50pub 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        // Content-adjacent metadata from the classified interaction: the
152        // sender's signed taint declaration (when one was made) rides to the
153        // typed transcript notice.
154        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    /// Ask 5 gate: the classified interaction's sender-declared content taint
413    /// threads through to `PeerInput.sender_taint` as content-adjacent
414    /// metadata, and `None` (no declaration) survives as `None` — never
415    /// coalesced into `Clean`.
416    #[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                // K15: lifecycle-classed requests carry the typed peer
458                // subject; a missing subject is rejected at ingress.
459                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            // Single-owner semantics: when typed blocks are present they ARE
785            // the content; the stale rendered text is not stored beside them.
786            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            // Single-owner semantics: typed blocks ARE the content; the text
887            // projection is derived from them at read time, never stored.
888            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}