diff --git a/src/lib.rs b/src/lib.rs index 6f140e1..cf9c9c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,7 @@ pub use contracts::{CoreError, CoreResult}; #[cfg(test)] mod tests { use crate::{ + CoreError, config::{ AuthConfig, CacheConfig, EmbeddingProviderConfig, EmbeddingProviderKind, ExtractionProviderConfig, ExtractionProviderKind, IngestionPolicy, NextralConfig, @@ -192,6 +193,45 @@ mod tests { ); } + #[test] + fn ingest_rejects_cross_tenant_id_collision() { + let mut store = TestMemoryStore::new(); + let policy = IngestionPolicy { + min_importance_score: 0.0, + min_confidence_score: 0.0, + }; + + let mut tenant_a = IngestMemoryRequest::new( + "tenant_a", + "usr_1", + "Tenant A fact", + ContentType::Fact, + MemoryType::Semantic, + SourceType::Manual, + policy.clone(), + ); + tenant_a.id = Some("shared-id".to_string()); + tenant_a.importance_score = 1.0; + assert_eq!(ingest_memory(&mut store, tenant_a).unwrap().status, IngestStatus::Accepted); + + let mut tenant_b = IngestMemoryRequest::new( + "tenant_b", + "usr_1", + "Tenant B overwrite attempt", + ContentType::Fact, + MemoryType::Semantic, + SourceType::Manual, + policy, + ); + tenant_b.id = Some("shared-id".to_string()); + tenant_b.importance_score = 1.0; + + let error = ingest_memory(&mut store, tenant_b).unwrap_err(); + assert!(matches!(error, CoreError::Conflict(_))); + let original = store.get_memory("tenant_a", "usr_1", "shared-id").unwrap(); + assert_eq!(original.unwrap().content, "Tenant A fact"); + } + #[test] fn all_seven_memory_types_have_runtime_topology() { let profiles = all_profiles(); diff --git a/src/testkit/mod.rs b/src/testkit/mod.rs index 5e2ba40..dcb0cb0 100644 --- a/src/testkit/mod.rs +++ b/src/testkit/mod.rs @@ -81,10 +81,22 @@ impl TestMemoryStore { impl MemoryIndexStore for TestMemoryStore { fn upsert_memory(&mut self, record: MemoryRecord) -> CoreResult<()> { record.validate()?; + if self.memories.iter().any(|memory| { + memory.id == record.id + && (memory.tenant_id != record.tenant_id || memory.user_id != record.user_id) + }) { + return Err(CoreError::Conflict( + "memory id is already in use by a different tenant/user scope".to_string(), + )); + } if let Some(existing) = self .memories .iter_mut() - .find(|memory| memory.id == record.id) + .find(|memory| { + memory.id == record.id + && memory.tenant_id == record.tenant_id + && memory.user_id == record.user_id + }) { *existing = record; return Ok(());