Skip to main content

mentedb_server/
grpc.rs

1//! gRPC service implementations for MenteDB.
2//!
3//! Provides a CognitionService with bidirectional streaming that wraps the
4//! CognitionStream engine, and a MemoryService with standard CRUD operations.
5
6use std::collections::HashMap;
7use std::pin::Pin;
8use std::sync::Arc;
9
10use mentedb_cognitive::stream::{CognitionStream, StreamAlert};
11use mentedb_core::edge::EdgeType;
12use mentedb_core::memory::{AttributeValue, MemoryType};
13use mentedb_core::{MemoryEdge, MemoryNode};
14use tokio::sync::mpsc;
15use tokio_stream::{Stream, StreamExt, wrappers::ReceiverStream};
16use tonic::{Request, Response, Status, Streaming};
17use tracing::{error, info};
18use uuid::Uuid;
19
20use crate::state::AppState;
21
22// Generated by tonic-build, not hand written: lints on it are tonic's to fix, and
23// each new clippy release adds some (large `tonic::Status` errors, doubled
24// `#[must_use]`), breaking CI on code nobody here writes.
25#[allow(clippy::all)]
26pub mod pb {
27    tonic::include_proto!("mentedb");
28}
29
30use mentedb_core::types::{AgentId, MemoryId, SpaceId, UserId};
31use pb::cognition_service_server::CognitionService;
32use pb::memory_service_server::MemoryService;
33
34// ---------------------------------------------------------------------------
35// CognitionService
36// ---------------------------------------------------------------------------
37
38/// gRPC service implementation for cognitive memory operations.
39pub struct CognitionServiceImpl {
40    #[allow(dead_code)]
41    pub state: Arc<AppState>,
42}
43
44type CognitionStream_ =
45    Pin<Box<dyn Stream<Item = Result<pb::CognitionAlert, Status>> + Send + 'static>>;
46
47#[tonic::async_trait]
48impl CognitionService for CognitionServiceImpl {
49    type StreamCognitionStream = CognitionStream_;
50
51    async fn stream_cognition(
52        &self,
53        request: Request<Streaming<pb::CognitionRequest>>,
54    ) -> Result<Response<Self::StreamCognitionStream>, Status> {
55        authenticate_grpc_streaming(&self.state, request.metadata())?;
56        info!("gRPC cognition stream opened");
57
58        let stream_engine = Arc::new(CognitionStream::new(1000));
59        let known_facts: Arc<std::sync::Mutex<Vec<(MemoryId, String)>>> =
60            Arc::new(std::sync::Mutex::new(Vec::new()));
61
62        let (tx, rx) = mpsc::channel(128);
63        let mut inbound = request.into_inner();
64
65        let engine = stream_engine.clone();
66        let facts = known_facts.clone();
67
68        tokio::spawn(async move {
69            while let Some(result) = inbound.next().await {
70                let req = match result {
71                    Ok(r) => r,
72                    Err(e) => {
73                        error!("cognition stream receive error: {e}");
74                        break;
75                    }
76                };
77
78                let payload = match req.payload {
79                    Some(p) => p,
80                    None => continue,
81                };
82
83                match payload {
84                    pb::cognition_request::Payload::Token(t) => {
85                        engine.feed_token(&t.text);
86
87                        let current_facts = facts.lock().unwrap().clone();
88                        let alerts = engine.check_alerts(&current_facts);
89                        for alert in alerts {
90                            let proto_alert = stream_alert_to_proto(alert);
91                            if tx.send(Ok(proto_alert)).await.is_err() {
92                                return;
93                            }
94                        }
95                    }
96                    pb::cognition_request::Payload::KnownFact(kf) => {
97                        let mid = kf
98                            .memory_id
99                            .parse::<MemoryId>()
100                            .unwrap_or_else(|_| MemoryId::nil());
101                        facts.lock().unwrap().push((mid, kf.content));
102                    }
103                    pb::cognition_request::Payload::EndOfTurn(_) => {
104                        let current_facts = facts.lock().unwrap().clone();
105                        let alerts = engine.check_alerts(&current_facts);
106                        for alert in alerts {
107                            let proto_alert = stream_alert_to_proto(alert);
108                            if tx.send(Ok(proto_alert)).await.is_err() {
109                                return;
110                            }
111                        }
112                    }
113                    pb::cognition_request::Payload::Flush(_) => {
114                        let text = engine.drain_buffer();
115                        let alert = pb::CognitionAlert {
116                            alert: Some(pb::cognition_alert::Alert::BufferFlushed(
117                                pb::BufferFlushedAlert {
118                                    accumulated_text: text,
119                                },
120                            )),
121                        };
122                        if tx.send(Ok(alert)).await.is_err() {
123                            return;
124                        }
125                    }
126                }
127            }
128            info!("gRPC cognition stream closed");
129        });
130
131        let output = ReceiverStream::new(rx);
132        Ok(Response::new(Box::pin(output)))
133    }
134}
135
136fn stream_alert_to_proto(alert: StreamAlert) -> pb::CognitionAlert {
137    let inner = match alert {
138        StreamAlert::Contradiction {
139            memory_id,
140            ai_said,
141            stored,
142        } => pb::cognition_alert::Alert::Contradiction(pb::ContradictionAlert {
143            memory_id: memory_id.to_string(),
144            ai_said,
145            stored,
146        }),
147        StreamAlert::Forgotten { memory_id, summary } => {
148            pb::cognition_alert::Alert::Forgotten(pb::ForgottenAlert {
149                memory_id: memory_id.to_string(),
150                summary,
151            })
152        }
153        StreamAlert::Correction {
154            memory_id,
155            old,
156            new,
157        } => pb::cognition_alert::Alert::Correction(pb::CorrectionAlert {
158            memory_id: memory_id.to_string(),
159            old_content: old,
160            new_content: new,
161        }),
162        StreamAlert::Reinforcement { memory_id } => {
163            pb::cognition_alert::Alert::Reinforcement(pb::ReinforcementAlert {
164                memory_id: memory_id.to_string(),
165            })
166        }
167    };
168    pb::CognitionAlert { alert: Some(inner) }
169}
170
171// ---------------------------------------------------------------------------
172// MemoryService
173// ---------------------------------------------------------------------------
174
175/// gRPC service implementation for basic memory CRUD operations.
176pub struct MemoryServiceImpl {
177    /// Shared application state containing the database handle.
178    pub state: Arc<AppState>,
179}
180
181#[tonic::async_trait]
182impl MemoryService for MemoryServiceImpl {
183    async fn store(
184        &self,
185        request: Request<pb::StoreRequest>,
186    ) -> Result<Response<pb::StoreResponse>, Status> {
187        let caller = authenticate_grpc_request(&self.state, &request)?;
188        let req = request.into_inner();
189
190        let agent_id = AgentId(parse_uuid_field(&req.agent_id, "agent_id")?);
191        if let Some(ref ta) = caller {
192            let tu: AgentId = ta
193                .parse()
194                .map_err(|_| Status::internal("bad token agent_id"))?;
195            if tu != agent_id {
196                return Err(Status::permission_denied("agent_id mismatch"));
197            }
198        }
199        let memory_type = parse_memory_type_str(&req.memory_type)?;
200        let space_id = if req.space_id.is_empty() {
201            SpaceId::nil()
202        } else {
203            SpaceId(parse_uuid_field(&req.space_id, "space_id")?)
204        };
205
206        let salience = if req.salience == 0.0 {
207            0.5
208        } else {
209            req.salience
210        };
211        let confidence = if req.confidence == 0.0 {
212            1.0
213        } else {
214            req.confidence
215        };
216
217        let now = std::time::SystemTime::now()
218            .duration_since(std::time::UNIX_EPOCH)
219            .unwrap_or_default()
220            .as_micros() as u64;
221
222        let id = MemoryId::new();
223
224        let attributes = req
225            .attributes
226            .into_iter()
227            .map(|(k, v)| (k, AttributeValue::String(v)))
228            .collect::<HashMap<String, AttributeValue>>();
229
230        let node = MemoryNode {
231            id,
232            agent_id,
233            user_id: UserId::nil(),
234            memory_type,
235            embedding: req.embedding,
236            content: req.content,
237            created_at: now,
238            accessed_at: now,
239            access_count: 0,
240            salience,
241            confidence,
242            space_id,
243            attributes,
244            tags: req.tags,
245            valid_from: req.valid_from,
246            valid_until: req.valid_until,
247            context: None,
248        };
249
250        let db = &*self.state.db;
251        let outcome = db.store_with_outcome(node).map_err(|e| {
252            error!("gRPC store failed: {e}");
253            Status::internal(format!("store failed: {e}"))
254        })?;
255
256        let (id, status) = match outcome {
257            mentedb::StoreOutcome::Stored => (id, "stored"),
258            mentedb::StoreOutcome::Duplicate { of } => (of, "duplicate"),
259        };
260        Ok(Response::new(pb::StoreResponse {
261            id: id.to_string(),
262            status: status.into(),
263        }))
264    }
265
266    async fn recall(
267        &self,
268        request: Request<pb::RecallRequest>,
269    ) -> Result<Response<pb::RecallResponse>, Status> {
270        authenticate_grpc_request(&self.state, &request)?;
271        let req = request.into_inner();
272
273        let db = &*self.state.db;
274        let window = db.recall(&req.query).map_err(|e| {
275            error!("gRPC recall failed: {e}");
276            Status::internal(format!("recall failed: {e}"))
277        })?;
278
279        let memory_count: usize = window.blocks.iter().map(|b| b.memories.len()).sum();
280
281        Ok(Response::new(pb::RecallResponse {
282            context: window.format,
283            total_tokens: window.total_tokens as u64,
284            memory_count: memory_count as u64,
285        }))
286    }
287
288    async fn search(
289        &self,
290        request: Request<pb::SearchRequest>,
291    ) -> Result<Response<pb::SearchResponse>, Status> {
292        authenticate_grpc_request(&self.state, &request)?;
293        let req = request.into_inner();
294        let k = if req.k == 0 { 10 } else { req.k as usize };
295
296        if req.embedding.is_empty() {
297            return Err(Status::invalid_argument("missing embedding vector"));
298        }
299
300        let db = &*self.state.db;
301        let results = db.recall_similar(&req.embedding, k).map_err(|e| {
302            error!("gRPC search failed: {e}");
303            Status::internal(format!("search failed: {e}"))
304        })?;
305
306        let items: Vec<pb::SearchResult> = results
307            .iter()
308            .map(|(id, score)| pb::SearchResult {
309                id: id.to_string(),
310                score: *score,
311            })
312            .collect();
313
314        Ok(Response::new(pb::SearchResponse { results: items }))
315    }
316
317    async fn forget(
318        &self,
319        request: Request<pb::ForgetRequest>,
320    ) -> Result<Response<pb::ForgetResponse>, Status> {
321        authenticate_grpc_request(&self.state, &request)?;
322        let req = request.into_inner();
323        let id = MemoryId(parse_uuid_field(&req.id, "id")?);
324
325        let db = &*self.state.db;
326        db.forget(id).map_err(|e| {
327            error!("gRPC forget failed: {e}");
328            Status::internal(format!("forget failed: {e}"))
329        })?;
330
331        Ok(Response::new(pb::ForgetResponse {
332            status: "deleted".into(),
333        }))
334    }
335
336    async fn relate(
337        &self,
338        request: Request<pb::RelateRequest>,
339    ) -> Result<Response<pb::RelateResponse>, Status> {
340        authenticate_grpc_request(&self.state, &request)?;
341        let req = request.into_inner();
342        let source = MemoryId(parse_uuid_field(&req.source, "source")?);
343        let target = MemoryId(parse_uuid_field(&req.target, "target")?);
344        let edge_type = parse_edge_type_str(&req.edge_type)?;
345        let weight = if req.weight == 0.0 { 1.0 } else { req.weight };
346
347        let now = std::time::SystemTime::now()
348            .duration_since(std::time::UNIX_EPOCH)
349            .unwrap_or_default()
350            .as_micros() as u64;
351
352        let edge = MemoryEdge {
353            source,
354            target,
355            edge_type,
356            weight,
357            created_at: now,
358            valid_from: req.valid_from,
359            valid_until: req.valid_until,
360            label: None,
361        };
362
363        let db = &*self.state.db;
364        db.relate(edge).map_err(|e| {
365            error!("gRPC relate failed: {e}");
366            Status::internal(format!("relate failed: {e}"))
367        })?;
368
369        Ok(Response::new(pb::RelateResponse {
370            status: "created".into(),
371        }))
372    }
373}
374
375#[allow(clippy::result_large_err)]
376fn authenticate_grpc_request<T>(
377    state: &AppState,
378    request: &Request<T>,
379) -> Result<Option<String>, Status> {
380    let verifier = match &state.verifier {
381        Some(v) => v,
382        None => return Ok(None),
383    };
384    let token = request
385        .metadata()
386        .get("authorization")
387        .and_then(|v| v.to_str().ok())
388        .and_then(|v| v.strip_prefix("Bearer "))
389        .ok_or_else(|| Status::unauthenticated("missing or invalid authorization metadata"))?;
390    verifier
391        .verify(token)
392        .map(|c| Some(c.agent_id))
393        .map_err(|e| Status::unauthenticated(format!("invalid token: {e}")))
394}
395#[allow(clippy::result_large_err)]
396fn authenticate_grpc_streaming(
397    state: &AppState,
398    metadata: &tonic::metadata::MetadataMap,
399) -> Result<Option<String>, Status> {
400    let verifier = match &state.verifier {
401        Some(v) => v,
402        None => return Ok(None),
403    };
404    let token = metadata
405        .get("authorization")
406        .and_then(|v| v.to_str().ok())
407        .and_then(|v| v.strip_prefix("Bearer "))
408        .ok_or_else(|| Status::unauthenticated("missing or invalid authorization metadata"))?;
409    verifier
410        .verify(token)
411        .map(|c| Some(c.agent_id))
412        .map_err(|e| Status::unauthenticated(format!("invalid token: {e}")))
413}
414
415// ---------------------------------------------------------------------------
416// Helpers
417// ---------------------------------------------------------------------------
418
419#[allow(clippy::result_large_err)]
420fn parse_uuid_field(s: &str, field: &str) -> Result<Uuid, Status> {
421    Uuid::parse_str(s)
422        .map_err(|_| Status::invalid_argument(format!("invalid UUID for '{field}': {s}")))
423}
424
425#[allow(clippy::result_large_err)]
426fn parse_memory_type_str(s: &str) -> Result<MemoryType, Status> {
427    match s.to_lowercase().as_str() {
428        "episodic" => Ok(MemoryType::Episodic),
429        "semantic" => Ok(MemoryType::Semantic),
430        "procedural" => Ok(MemoryType::Procedural),
431        "antipattern" | "anti_pattern" => Ok(MemoryType::AntiPattern),
432        "reasoning" => Ok(MemoryType::Reasoning),
433        "correction" => Ok(MemoryType::Correction),
434        _ => Err(Status::invalid_argument(format!(
435            "unknown memory_type: {s}"
436        ))),
437    }
438}
439
440#[allow(clippy::result_large_err)]
441fn parse_edge_type_str(s: &str) -> Result<EdgeType, Status> {
442    match s.to_lowercase().as_str() {
443        "caused" => Ok(EdgeType::Caused),
444        "before" => Ok(EdgeType::Before),
445        "related" => Ok(EdgeType::Related),
446        "contradicts" => Ok(EdgeType::Contradicts),
447        "supports" => Ok(EdgeType::Supports),
448        "supersedes" => Ok(EdgeType::Supersedes),
449        "derived" => Ok(EdgeType::Derived),
450        "partof" | "part_of" => Ok(EdgeType::PartOf),
451        _ => Err(Status::invalid_argument(format!("unknown edge_type: {s}"))),
452    }
453}
454
455// ---------------------------------------------------------------------------
456// Tests
457// ---------------------------------------------------------------------------
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462    use std::time::Instant;
463    use tempfile::TempDir;
464
465    fn make_test_state() -> (Arc<AppState>, TempDir) {
466        let tmp = TempDir::new().unwrap();
467        let databases = Arc::new(
468            crate::databases::Databases::open(
469                tmp.path(),
470                None,
471                Default::default(),
472                Default::default(),
473            )
474            .unwrap(),
475        );
476        let state = Arc::new(AppState {
477            db: databases.default_db(),
478            databases: databases.clone(),
479            verifier: None,
480            spaces: Arc::new(tokio::sync::RwLock::new(mentedb_core::SpaceManager::new())),
481            jwt_secret: None,
482            admin_key: None,
483            start_time: Instant::now(),
484            extraction_config: None,
485            auto_extract: false,
486            extraction_tx: None,
487            cluster: None,
488            turn_trackers: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())),
489        });
490        (state, tmp)
491    }
492
493    #[tokio::test]
494    async fn test_grpc_memory_store_and_recall() {
495        let (state, _tmp) = make_test_state();
496        let svc = MemoryServiceImpl {
497            state: state.clone(),
498        };
499
500        let agent_id = AgentId::new().to_string();
501        let store_req = Request::new(pb::StoreRequest {
502            agent_id: agent_id.clone(),
503            memory_type: "episodic".into(),
504            content: "The user prefers dark mode".into(),
505            embedding: vec![],
506            tags: vec!["preference".into()],
507            attributes: HashMap::new(),
508            space_id: String::new(),
509            salience: 0.8,
510            confidence: 1.0,
511            valid_from: None,
512            valid_until: None,
513        });
514
515        let resp = svc.store(store_req).await.unwrap();
516        let inner = resp.into_inner();
517        assert_eq!(inner.status, "stored");
518        assert!(!inner.id.is_empty());
519
520        let recall_req = Request::new(pb::RecallRequest {
521            query: "RECALL memories LIMIT 100".into(),
522        });
523        let resp = svc.recall(recall_req).await.unwrap();
524        let inner = resp.into_inner();
525        // Recall succeeded without error; count may be 0 if the engine has not
526        // indexed the memory yet, so we just verify the response is valid.
527        let _ = inner.memory_count;
528    }
529
530    #[tokio::test]
531    async fn test_grpc_memory_forget() {
532        let (state, _tmp) = make_test_state();
533        let svc = MemoryServiceImpl {
534            state: state.clone(),
535        };
536
537        let agent_id = AgentId::new().to_string();
538        let store_resp = svc
539            .store(Request::new(pb::StoreRequest {
540                agent_id,
541                memory_type: "semantic".into(),
542                content: "Temporary memory".into(),
543                embedding: vec![],
544                tags: vec![],
545                attributes: HashMap::new(),
546                space_id: String::new(),
547                salience: 0.5,
548                confidence: 1.0,
549                valid_from: None,
550                valid_until: None,
551            }))
552            .await
553            .unwrap();
554        let stored_id = store_resp.into_inner().id;
555
556        let forget_resp = svc
557            .forget(Request::new(pb::ForgetRequest {
558                id: stored_id.clone(),
559            }))
560            .await
561            .unwrap();
562        assert_eq!(forget_resp.into_inner().status, "deleted");
563    }
564
565    #[tokio::test]
566    async fn test_grpc_memory_relate() {
567        let (state, _tmp) = make_test_state();
568        let svc = MemoryServiceImpl {
569            state: state.clone(),
570        };
571
572        let agent_id = AgentId::new().to_string();
573        let id1 = svc
574            .store(Request::new(pb::StoreRequest {
575                agent_id: agent_id.clone(),
576                memory_type: "episodic".into(),
577                content: "Event A".into(),
578                embedding: vec![],
579                tags: vec![],
580                attributes: HashMap::new(),
581                space_id: String::new(),
582                salience: 0.5,
583                confidence: 1.0,
584                valid_from: None,
585                valid_until: None,
586            }))
587            .await
588            .unwrap()
589            .into_inner()
590            .id;
591
592        let id2 = svc
593            .store(Request::new(pb::StoreRequest {
594                agent_id,
595                memory_type: "episodic".into(),
596                content: "Event B".into(),
597                embedding: vec![],
598                tags: vec![],
599                attributes: HashMap::new(),
600                space_id: String::new(),
601                salience: 0.5,
602                confidence: 1.0,
603                valid_from: None,
604                valid_until: None,
605            }))
606            .await
607            .unwrap()
608            .into_inner()
609            .id;
610
611        let relate_resp = svc
612            .relate(Request::new(pb::RelateRequest {
613                source: id1,
614                target: id2,
615                edge_type: "caused".into(),
616                weight: 0.9,
617                valid_from: None,
618                valid_until: None,
619            }))
620            .await
621            .unwrap();
622        assert_eq!(relate_resp.into_inner().status, "created");
623    }
624
625    #[tokio::test]
626    async fn test_grpc_cognition_stream() {
627        use pb::cognition_service_server::CognitionServiceServer;
628        use tokio::net::TcpListener;
629        use tonic::transport::{Channel, Server};
630
631        let (state, _tmp) = make_test_state();
632
633        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
634        let addr = listener.local_addr().unwrap();
635
636        let svc = CognitionServiceServer::new(CognitionServiceImpl {
637            state: state.clone(),
638        });
639
640        tokio::spawn(async move {
641            let incoming = tokio_stream::wrappers::TcpListenerStream::new(listener);
642            Server::builder()
643                .add_service(svc)
644                .serve_with_incoming(incoming)
645                .await
646                .unwrap();
647        });
648
649        // Small delay to let the server start
650        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
651
652        let channel = Channel::from_shared(format!("http://{addr}"))
653            .unwrap()
654            .connect()
655            .await
656            .unwrap();
657
658        let mut client = pb::cognition_service_client::CognitionServiceClient::new(channel);
659
660        let (tx, rx) = mpsc::channel(32);
661        let outbound = ReceiverStream::new(rx);
662
663        let response = client.stream_cognition(outbound).await.unwrap();
664        let mut inbound = response.into_inner();
665
666        // Register a known fact
667        let mid = MemoryId::new();
668        tx.send(pb::CognitionRequest {
669            payload: Some(pb::cognition_request::Payload::KnownFact(pb::KnownFact {
670                memory_id: mid.to_string(),
671                content: "The system uses PostgreSQL for storage".into(),
672            })),
673        })
674        .await
675        .unwrap();
676
677        // Feed tokens that contradict the known fact
678        tx.send(pb::CognitionRequest {
679            payload: Some(pb::cognition_request::Payload::Token(pb::TokenPayload {
680                text: "The system does not use PostgreSQL, actually it uses MySQL".into(),
681            })),
682        })
683        .await
684        .unwrap();
685
686        // We should get a contradiction alert
687        if let Some(Ok(alert)) = inbound.next().await {
688            match alert.alert {
689                Some(pb::cognition_alert::Alert::Contradiction(c)) => {
690                    assert_eq!(c.memory_id, mid.to_string());
691                    assert!(!c.ai_said.is_empty());
692                    assert!(!c.stored.is_empty());
693                }
694                other => panic!("expected contradiction alert, got: {:?}", other),
695            }
696        } else {
697            panic!("expected an alert from the cognition stream");
698        }
699
700        // Test flush
701        tx.send(pb::CognitionRequest {
702            payload: Some(pb::cognition_request::Payload::Flush(pb::FlushBuffer {})),
703        })
704        .await
705        .unwrap();
706
707        if let Some(Ok(alert)) = inbound.next().await {
708            match alert.alert {
709                Some(pb::cognition_alert::Alert::BufferFlushed(f)) => {
710                    assert!(!f.accumulated_text.is_empty());
711                }
712                other => panic!("expected buffer_flushed alert, got: {:?}", other),
713            }
714        }
715
716        drop(tx);
717    }
718
719    #[tokio::test]
720    async fn test_grpc_invalid_uuid() {
721        let (state, _tmp) = make_test_state();
722        let svc = MemoryServiceImpl { state };
723
724        let result = svc
725            .store(Request::new(pb::StoreRequest {
726                agent_id: "not-a-uuid".into(),
727                memory_type: "episodic".into(),
728                content: "test".into(),
729                embedding: vec![],
730                tags: vec![],
731                attributes: HashMap::new(),
732                space_id: String::new(),
733                salience: 0.5,
734                confidence: 1.0,
735                valid_from: None,
736                valid_until: None,
737            }))
738            .await;
739
740        assert!(result.is_err());
741        let status = result.unwrap_err();
742        assert_eq!(status.code(), tonic::Code::InvalidArgument);
743    }
744
745    #[tokio::test]
746    async fn test_grpc_invalid_memory_type() {
747        let (state, _tmp) = make_test_state();
748        let svc = MemoryServiceImpl { state };
749
750        let result = svc
751            .store(Request::new(pb::StoreRequest {
752                agent_id: AgentId::new().to_string(),
753                memory_type: "nonexistent".into(),
754                content: "test".into(),
755                embedding: vec![],
756                tags: vec![],
757                attributes: HashMap::new(),
758                space_id: String::new(),
759                salience: 0.5,
760                confidence: 1.0,
761                valid_from: None,
762                valid_until: None,
763            }))
764            .await;
765
766        assert!(result.is_err());
767        let status = result.unwrap_err();
768        assert_eq!(status.code(), tonic::Code::InvalidArgument);
769    }
770
771    #[tokio::test]
772    async fn test_grpc_search_missing_embedding() {
773        let (state, _tmp) = make_test_state();
774        let svc = MemoryServiceImpl { state };
775
776        let result = svc
777            .search(Request::new(pb::SearchRequest {
778                embedding: vec![],
779                k: 10,
780            }))
781            .await;
782
783        assert!(result.is_err());
784        assert_eq!(result.unwrap_err().code(), tonic::Code::InvalidArgument);
785    }
786
787    fn make_auth_test_state(secret: &str) -> (Arc<AppState>, TempDir) {
788        let tmp = TempDir::new().unwrap();
789        let databases = Arc::new(
790            crate::databases::Databases::open(
791                tmp.path(),
792                None,
793                Default::default(),
794                Default::default(),
795            )
796            .unwrap(),
797        );
798        (
799            Arc::new(AppState {
800                db: databases.default_db(),
801                databases: databases.clone(),
802                verifier: Some(Arc::new(crate::auth::TokenVerifier::shared_secret(secret))),
803                spaces: Arc::new(tokio::sync::RwLock::new(mentedb_core::SpaceManager::new())),
804                jwt_secret: Some(secret.into()),
805                admin_key: Some("ak".into()),
806                start_time: Instant::now(),
807                extraction_config: None,
808                auto_extract: false,
809                extraction_tx: None,
810                cluster: None,
811                turn_trackers: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())),
812            }),
813            tmp,
814        )
815    }
816    #[tokio::test]
817    async fn test_grpc_auth_required_when_secret_set() {
818        let (s, _t) = make_auth_test_state("s");
819        let svc = MemoryServiceImpl { state: s };
820        let r = svc
821            .recall(Request::new(pb::RecallRequest {
822                query: "RECALL memories LIMIT 10".into(),
823            }))
824            .await;
825        assert!(r.is_err());
826        assert_eq!(r.unwrap_err().code(), tonic::Code::Unauthenticated);
827    }
828    #[tokio::test]
829    async fn test_grpc_auth_succeeds_with_valid_token() {
830        let (s, _t) = make_auth_test_state("s");
831        let svc = MemoryServiceImpl { state: s };
832        let a = AgentId::new();
833        let tok = crate::auth::create_token("s", &a.to_string(), false, 1);
834        let mut r = Request::new(pb::StoreRequest {
835            agent_id: a.to_string(),
836            memory_type: "episodic".into(),
837            content: "t".into(),
838            embedding: vec![],
839            tags: vec![],
840            attributes: HashMap::new(),
841            space_id: String::new(),
842            salience: 0.5,
843            confidence: 1.0,
844            valid_from: None,
845            valid_until: None,
846        });
847        r.metadata_mut()
848            .insert("authorization", format!("Bearer {tok}").parse().unwrap());
849        assert_eq!(svc.store(r).await.unwrap().into_inner().status, "stored");
850    }
851    #[tokio::test]
852    async fn test_grpc_auth_rejects_wrong_agent_id() {
853        let (s, _t) = make_auth_test_state("s");
854        let svc = MemoryServiceImpl { state: s };
855        let ta = AgentId::new();
856        let ra = AgentId::new();
857        let tok = crate::auth::create_token("s", &ta.to_string(), false, 1);
858        let mut r = Request::new(pb::StoreRequest {
859            agent_id: ra.to_string(),
860            memory_type: "episodic".into(),
861            content: "t".into(),
862            embedding: vec![],
863            tags: vec![],
864            attributes: HashMap::new(),
865            space_id: String::new(),
866            salience: 0.5,
867            confidence: 1.0,
868            valid_from: None,
869            valid_until: None,
870        });
871        r.metadata_mut()
872            .insert("authorization", format!("Bearer {tok}").parse().unwrap());
873        assert_eq!(
874            svc.store(r).await.unwrap_err().code(),
875            tonic::Code::PermissionDenied
876        );
877    }
878    #[tokio::test]
879    async fn test_grpc_auth_invalid_token() {
880        let (s, _t) = make_auth_test_state("s");
881        let svc = MemoryServiceImpl { state: s };
882        let mut r = Request::new(pb::RecallRequest {
883            query: "RECALL memories LIMIT 10".into(),
884        });
885        r.metadata_mut()
886            .insert("authorization", "Bearer bad.tok".parse().unwrap());
887        assert_eq!(
888            svc.recall(r).await.unwrap_err().code(),
889            tonic::Code::Unauthenticated
890        );
891    }
892}