Skip to main content

meerkat_mobkit/
console_contracts.rs

1//! Console-facing contract types for the planned identity-native operator workbench.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6pub const IDENTITY_STREAM_NAME: &str = "identity";
7pub const ALL_EVENTS_STREAM_NAME: &str = "all_events";
8pub const SYSTEM_EVENT_IDENTITY: &str = "_system";
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct ConsoleIdentityEventEnvelope {
12    pub event_id: String,
13    #[serde(default, skip_serializing_if = "Option::is_none")]
14    pub interaction_id: Option<String>,
15    pub identity: String,
16    pub event_type: String,
17    pub timestamp_ms: u64,
18    pub data: Value,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct ReplayUnavailableError {
23    pub error: String,
24    pub stream: String,
25    pub requested_last_event_id: String,
26    pub latest_event_id: String,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct ConsoleInteractionRejectedError {
31    pub code: i64,
32    pub message: String,
33}
34
35#[cfg(test)]
36#[allow(clippy::expect_used)]
37mod tests {
38    use super::*;
39
40    #[test]
41    fn console_identity_event_envelope_roundtrips_with_interaction_id() {
42        let original = ConsoleIdentityEventEnvelope {
43            event_id: "evt-1".to_string(),
44            interaction_id: Some("turn-123".to_string()),
45            identity: "identity:luka".to_string(),
46            event_type: "interaction_complete".to_string(),
47            timestamp_ms: 1_717_171_717,
48            data: serde_json::json!({
49                "status": "ok",
50                "tool_call_id": "tool-1"
51            }),
52        };
53
54        let encoded = serde_json::to_value(&original).expect("envelope should serialize");
55        let decoded: ConsoleIdentityEventEnvelope =
56            serde_json::from_value(encoded).expect("envelope should deserialize");
57
58        assert_eq!(decoded, original);
59    }
60
61    #[test]
62    fn console_identity_event_envelope_roundtrips_without_interaction_id() {
63        let original = ConsoleIdentityEventEnvelope {
64            event_id: "evt-2".to_string(),
65            interaction_id: None,
66            identity: "identity:luka".to_string(),
67            event_type: "lease_updated".to_string(),
68            timestamp_ms: 1_717_171_718,
69            data: serde_json::json!({
70                "state": "healthy"
71            }),
72        };
73
74        let encoded = serde_json::to_value(&original).expect("envelope should serialize");
75        let decoded: ConsoleIdentityEventEnvelope =
76            serde_json::from_value(encoded).expect("envelope should deserialize");
77
78        assert_eq!(decoded, original);
79    }
80
81    #[test]
82    fn replay_unavailable_error_roundtrips_through_json() {
83        let original = ReplayUnavailableError {
84            error: "replay_unavailable".to_string(),
85            stream: "identity".to_string(),
86            requested_last_event_id: "evt-1".to_string(),
87            latest_event_id: "evt-9".to_string(),
88        };
89
90        let encoded = serde_json::to_value(&original).expect("replay error should serialize");
91        let decoded: ReplayUnavailableError =
92            serde_json::from_value(encoded).expect("replay error should deserialize");
93
94        assert_eq!(decoded, original);
95    }
96
97    #[test]
98    fn console_interaction_rejected_error_roundtrips_through_json() {
99        let original = ConsoleInteractionRejectedError {
100            code: -32001,
101            message: "unknown identity".to_string(),
102        };
103
104        let encoded = serde_json::to_value(&original).expect("rejection error should serialize");
105        let decoded: ConsoleInteractionRejectedError =
106            serde_json::from_value(encoded).expect("rejection error should deserialize");
107
108        assert_eq!(decoded, original);
109    }
110}