soma-studio-server 0.1.0

Local HTTP server runtime for Soma Studio.
Documentation
use soma_studio_core::ChatStreamEvent;

#[derive(Debug, Clone)]
pub struct ChatEventEnvelope {
    pub session_id: String,
    pub conversation_id: String,
    pub payload: String,
}

pub fn chat_stream_event_envelope(
    session_id: &str,
    conversation_id: &str,
    event: &ChatStreamEvent,
) -> ChatEventEnvelope {
    ChatEventEnvelope {
        session_id: session_id.to_string(),
        conversation_id: conversation_id.to_string(),
        payload: chat_stream_event_payload(event),
    }
}

pub fn chat_event_matches_subscription(
    event: &ChatEventEnvelope,
    session_id: &str,
    conversation_filter: Option<&str>,
) -> bool {
    if event.session_id != session_id {
        return false;
    }

    if let Some(filter) = conversation_filter {
        return event.conversation_id == filter;
    }

    true
}

pub fn chat_stream_event_payload(event: &ChatStreamEvent) -> String {
    serde_json::to_string(event).expect("chat stream event should serialize")
}

#[cfg(test)]
mod tests {
    use super::{
        ChatEventEnvelope, chat_event_matches_subscription, chat_stream_event_envelope,
        chat_stream_event_payload,
    };
    use soma_studio_core::ChatStreamEvent;

    fn sample_event(session_id: &str, conversation_id: &str) -> ChatEventEnvelope {
        ChatEventEnvelope {
            session_id: session_id.to_string(),
            conversation_id: conversation_id.to_string(),
            payload: "{\"type\":\"chat-start\"}".to_string(),
        }
    }

    #[test]
    fn chat_event_filter_allows_same_session_without_conversation_filter() {
        let event = sample_event("session-a", "conversation-1");

        assert!(chat_event_matches_subscription(&event, "session-a", None));
    }

    #[test]
    fn chat_event_filter_rejects_other_session() {
        let event = sample_event("session-a", "conversation-1");

        assert!(!chat_event_matches_subscription(&event, "session-b", None));
    }

    #[test]
    fn chat_event_filter_rejects_other_conversation_when_filtered() {
        let event = sample_event("session-a", "conversation-1");

        assert!(chat_event_matches_subscription(
            &event,
            "session-a",
            Some("conversation-1"),
        ));
        assert!(!chat_event_matches_subscription(
            &event,
            "session-a",
            Some("conversation-2"),
        ));
    }

    #[test]
    fn chat_stream_event_payload_uses_wire_type_tags() {
        let payload = chat_stream_event_payload(&ChatStreamEvent::ChatDelta {
            conversation_id: "conversation-1".to_string(),
            message_id: "message-1".to_string(),
            selected_provider: Some("lmstudio".to_string()),
            selected_model_id: Some("model-1".to_string()),
            delta: "hello".to_string(),
        });
        let value: serde_json::Value = serde_json::from_str(&payload).expect("json payload");

        assert_eq!(value["type"], "chat-delta");
        assert_eq!(value["conversation_id"], "conversation-1");
        assert_eq!(value["message_id"], "message-1");
        assert_eq!(value["delta"], "hello");
    }

    #[test]
    fn chat_stream_event_envelope_carries_subscription_keys() {
        let event = ChatStreamEvent::Heartbeat {
            message: "ready".to_string(),
        };
        let envelope = chat_stream_event_envelope("session-a", "conversation-1", &event);

        assert_eq!(envelope.session_id, "session-a");
        assert_eq!(envelope.conversation_id, "conversation-1");
        assert!(envelope.payload.contains("\"type\":\"heartbeat\""));
    }
}