stygian_graph/adapters/
mock_ai.rs

1//! Mock AI provider adapter for testing
2//!
3//! A minimal implementation of AIProvider that returns predefined responses.
4//! Used to validate that the port trait compiles and can be implemented.
5
6use crate::domain::error::Result;
7use crate::ports::{AIProvider, ProviderCapabilities};
8use async_trait::async_trait;
9use futures::stream::{self, BoxStream};
10use serde_json::{Value, json};
11
12/// Mock AI provider for testing
13///
14/// Returns predefined JSON data without calling any actual LLM API.
15/// Useful for testing pipeline execution without incurring API costs.
16///
17/// # Example
18///
19/// ```
20/// use stygian_graph::adapters::mock_ai::MockAIProvider;
21/// use stygian_graph::ports::AIProvider;
22/// use serde_json::json;
23///
24/// # #[tokio::main]
25/// # async fn main() {
26/// let provider = MockAIProvider;
27/// let schema = json!({"type": "object"});
28/// let content = "Test content".to_string();
29///
30/// let result = provider.extract(content, schema).await.unwrap();
31/// assert_eq!(result["mock"], true);
32/// # }
33/// ```
34pub struct MockAIProvider;
35
36#[async_trait]
37impl AIProvider for MockAIProvider {
38    async fn extract(&self, content: String, _schema: Value) -> Result<Value> {
39        Ok(json!({
40            "mock": true,
41            "provider": self.name(),
42            "content_length": content.len(),
43            "extracted_data": {
44                "title": "Mock Title",
45                "description": "Mock Description",
46            }
47        }))
48    }
49
50    async fn stream_extract(
51        &self,
52        content: String,
53        _schema: Value,
54    ) -> Result<BoxStream<'static, Result<Value>>> {
55        // Mock streaming by emitting three chunks
56        let chunks = vec![
57            Ok(json!({"chunk": 1, "data": "first"})),
58            Ok(json!({"chunk": 2, "data": "second"})),
59            Ok(json!({"chunk": 3, "data": "third", "content_length": content.len()})),
60        ];
61        Ok(Box::pin(stream::iter(chunks)))
62    }
63
64    fn capabilities(&self) -> ProviderCapabilities {
65        ProviderCapabilities {
66            streaming: true,
67            vision: false,
68            tool_use: false,
69            json_mode: true,
70        }
71    }
72
73    fn name(&self) -> &'static str {
74        "mock-ai"
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use futures::StreamExt;
82    use serde_json::json;
83
84    #[tokio::test]
85    async fn test_mock_provider_extract() -> crate::domain::error::Result<()> {
86        let provider = MockAIProvider;
87        let schema = json!({
88            "type": "object",
89            "properties": {
90                "title": {"type": "string"}
91            }
92        });
93        let content = "Test HTML content".to_string();
94
95        let output = provider.extract(content.clone(), schema).await?;
96        assert_eq!(output.get("mock").and_then(Value::as_bool), Some(true));
97        assert_eq!(
98            output.get("provider").and_then(Value::as_str),
99            Some("mock-ai")
100        );
101        assert_eq!(
102            output.get("content_length").and_then(Value::as_u64),
103            u64::try_from(content.len()).ok()
104        );
105        assert_eq!(
106            output
107                .get("extracted_data")
108                .and_then(|d| d.get("title"))
109                .and_then(Value::as_str),
110            Some("Mock Title")
111        );
112        Ok(())
113    }
114
115    #[tokio::test]
116    async fn test_mock_provider_stream_extract() -> crate::domain::error::Result<()> {
117        let provider = MockAIProvider;
118        let schema = json!({"type": "object"});
119        let content = "Stream test content".to_string();
120
121        let mut stream = provider.stream_extract(content.clone(), schema).await?;
122        let mut chunks = Vec::new();
123
124        while let Some(chunk_result) = stream.next().await {
125            chunks.push(chunk_result?);
126        }
127
128        assert_eq!(chunks.len(), 3, "Should emit 3 chunks");
129        assert_eq!(
130            chunks
131                .first()
132                .and_then(|c| c.get("chunk"))
133                .and_then(Value::as_u64),
134            Some(1)
135        );
136        assert_eq!(
137            chunks
138                .get(1)
139                .and_then(|c| c.get("chunk"))
140                .and_then(Value::as_u64),
141            Some(2)
142        );
143        assert_eq!(
144            chunks
145                .get(2)
146                .and_then(|c| c.get("chunk"))
147                .and_then(Value::as_u64),
148            Some(3)
149        );
150        assert_eq!(
151            chunks
152                .get(2)
153                .and_then(|c| c.get("content_length"))
154                .and_then(Value::as_u64),
155            u64::try_from(content.len()).ok()
156        );
157        Ok(())
158    }
159
160    #[tokio::test]
161    async fn test_mock_provider_capabilities() {
162        let provider = MockAIProvider;
163        let caps = provider.capabilities();
164
165        assert!(caps.streaming, "Mock provider supports streaming");
166        assert!(!caps.vision, "Mock provider does not support vision");
167        assert!(!caps.tool_use, "Mock provider does not support tool use");
168        assert!(caps.json_mode, "Mock provider supports JSON mode");
169    }
170
171    #[tokio::test]
172    async fn test_mock_provider_name() {
173        let provider = MockAIProvider;
174        assert_eq!(provider.name(), "mock-ai");
175    }
176
177    #[tokio::test]
178    async fn test_mock_provider_is_send_sync() {
179        // Compile-time check that MockAIProvider implements Send + Sync
180        fn assert_send_sync<T: Send + Sync>() {}
181        assert_send_sync::<MockAIProvider>();
182    }
183
184    #[tokio::test]
185    async fn test_default_capabilities() {
186        let default_caps = ProviderCapabilities::default();
187        assert!(!default_caps.streaming);
188        assert!(!default_caps.vision);
189        assert!(!default_caps.tool_use);
190        assert!(!default_caps.json_mode);
191    }
192
193    #[tokio::test]
194    async fn test_capabilities_equality() {
195        let caps1 = ProviderCapabilities {
196            streaming: true,
197            vision: false,
198            tool_use: true,
199            json_mode: true,
200        };
201        let caps2 = ProviderCapabilities {
202            streaming: true,
203            vision: false,
204            tool_use: true,
205            json_mode: true,
206        };
207        assert_eq!(caps1, caps2);
208    }
209}