tinychannels 0.1.0

Pluggable channel and messaging primitives for OpenHuman harness communication.
Documentation
use crate::adapters::{LocalChannelAdapter, LocalOutboundSink};
use crate::channel::{
    ChannelAdapter, ChannelDescriptor, ChannelInboundEnvelope, ChannelInboundSink,
    ChannelOutboundIntent, ChannelSendError, ChannelStaticCapabilities, DeliveryDurability,
    MessageReceipt, OutboundPayload,
};
use async_trait::async_trait;
use std::sync::{Arc, Mutex};

#[derive(Default)]
struct RecordingOutboundSink {
    ids: Mutex<Vec<String>>,
}

#[async_trait]
impl LocalOutboundSink for RecordingOutboundSink {
    async fn send(
        &self,
        intent: ChannelOutboundIntent,
    ) -> Result<MessageReceipt, ChannelSendError> {
        self.ids
            .lock()
            .expect("ids lock")
            .push(intent.idempotency_key);
        Ok(MessageReceipt {
            primary_platform_message_id: Some("local-1".into()),
            platform_message_ids: vec!["local-1".into()],
            sent_at: 1,
            ..Default::default()
        })
    }
}

struct NoopInboundSink;

#[async_trait]
impl ChannelInboundSink for NoopInboundSink {
    async fn push(&self, _envelope: ChannelInboundEnvelope) -> Result<(), ChannelSendError> {
        Ok(())
    }
}

fn local_adapter(sink: Arc<RecordingOutboundSink>) -> LocalChannelAdapter {
    LocalChannelAdapter::new(
        ChannelDescriptor {
            id: "local".into(),
            display_name: "Local".into(),
            account_id: Some("host".into()),
            ..Default::default()
        },
        ChannelStaticCapabilities {
            reply: true,
            ..Default::default()
        },
        sink,
    )
}

#[tokio::test]
async fn local_adapter_delegates_outbound_delivery() {
    let sink = Arc::new(RecordingOutboundSink::default());
    let adapter = local_adapter(sink.clone());

    let receipt = adapter
        .send(ChannelOutboundIntent {
            idempotency_key: "turn-1:text".into(),
            channel_id: "local".into(),
            conversation_id: "chat-1".into(),
            reply_to_id: None,
            thread_id: None,
            durability: DeliveryDurability::BestEffort,
            payload: OutboundPayload::Text { text: "hi".into() },
        })
        .await
        .expect("send");

    assert_eq!(
        receipt.primary_platform_message_id.as_deref(),
        Some("local-1")
    );
    assert_eq!(
        sink.ids.lock().expect("ids lock").as_slice(),
        ["turn-1:text"]
    );
}

#[tokio::test]
async fn local_adapter_reports_configured_status() {
    let adapter = local_adapter(Arc::new(RecordingOutboundSink::default()));
    adapter.start(&NoopInboundSink).await.expect("start");

    let status = adapter.status().await.expect("status");
    assert_eq!(status.channel_id, "local");
    assert_eq!(status.account_id.as_deref(), Some("host"));
    assert!(
        status
            .states
            .iter()
            .any(|state| { *state == crate::controllers::ChannelAccountState::Configured })
    );
    assert!(adapter.static_capabilities().reply);
}