use mem_store::{Level, VectorKind, MemoryNode, PgRepo, Scope}; use sha2::{Sha256, Digest}; /// Deterministic embedder: sha256(text) → 768-dim vector /// Allows exact distance assertions without external API calls fn hash_to_embedding(text: &str) -> Vec { let mut hasher = Sha256::new(); hasher.update(text.as_bytes()); let hash = hasher.finalize(); // Convert 32 bytes to 768 floats deterministically let mut embedding = vec![0.0_f32; 768]; for (i, byte) in hash.iter().enumerate() { let idx = i % 768; embedding[idx] += (*byte as f32) / 256.0; } // Normalize to unit vector (cosine distance) let mag: f32 = embedding.iter().map(|x| x * x).sum::().sqrt(); if mag > 0.0 { for x in &mut embedding { *x /= mag; } } embedding } /// Compute cosine distance between two normalized vectors fn cosine_distance(a: &[f32], b: &[f32]) -> f32 { let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum(); // Distance = 1 - similarity (for normalized vectors) (1.0 - dot).max(0.0) } /// Setup: Postgres via testcontainers (if available in CI/local) /// For now, skip tests if DB not available to avoid CI dependencies #[tokio::test] #[ignore] // Run with: cargo test it_pg_repo -- --ignored async fn a1_upsert_idempotent() { let db_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgres://postgres:password@localhost:5432/poimen_test".to_string() }); // Skip if DB not available if !is_postgres_available(&db_url).await { println!("Skipping: Postgres not available at {}", db_url); return; } let repo = PgRepo::connect(&db_url).await.expect("connect"); repo.clear_project("test_p1").await.ok(); let node = MemoryNode { sha256: "abc123def456".to_string(), level: Level::L1, project: "test_p1".to_string(), query_id: Some("q1".to_string()), run_id: "r1".to_string(), t: 0, source: None, text: "test memory".to_string(), }; // First insert repo.upsert_node(&node).await.expect("upsert 1"); let count1 = repo.node_count().await.expect("count 1"); assert_eq!(count1, 1, "First insert should create 1 node"); // Upsert same node again repo.upsert_node(&node).await.expect("upsert 2"); let count2 = repo.node_count().await.expect("count 2"); assert_eq!(count2, 1, "Duplicate upsert must not create duplicate row"); repo.clear_project("test_p1").await.ok(); } #[tokio::test] #[ignore] async fn a2_two_pass_required() { let db_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgres://postgres:password@localhost:5432/poimen_test".to_string() }); if !is_postgres_available(&db_url).await { println!("Skipping: Postgres not available"); return; } let repo = PgRepo::connect(&db_url).await.expect("connect"); repo.clear_project("test_p2").await.ok(); let parent = MemoryNode { sha256: "parent_sha".to_string(), level: Level::L0, project: "test_p2".to_string(), query_id: Some("q1".to_string()), run_id: "r1".to_string(), t: 0, source: Some("pi".to_string()), text: "parent memory".to_string(), }; let child = MemoryNode { sha256: "child_sha".to_string(), level: Level::L1, project: "test_p2".to_string(), query_id: Some("q1".to_string()), run_id: "r1".to_string(), t: 1, source: None, text: "child memory".to_string(), }; // Insert child first (before parent) repo.upsert_node(&child).await.expect("insert child"); // Try edge before parent exists — should fail let edge_result = repo .insert_edges(&child.sha256, &[parent.sha256.clone()]) .await; assert!( edge_result.is_err(), "Edge insert should fail when parent not found (FK constraint)" ); // Now insert parent repo.upsert_node(&parent).await.expect("insert parent"); // Now edge should succeed repo.insert_edges(&child.sha256, &[parent.sha256.clone()]) .await .expect("insert edge after parent exists"); let edge_count = repo.edge_count().await.expect("edge count"); assert_eq!(edge_count, 1, "Edge should be created in second pass"); repo.clear_project("test_p2").await.ok(); } #[tokio::test] #[ignore] async fn a3_search_orders_by_distance() { let db_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgres://postgres:password@localhost:5432/poimen_test".to_string() }); if !is_postgres_available(&db_url).await { println!("Skipping: Postgres not available"); return; } let repo = PgRepo::connect(&db_url).await.expect("connect"); repo.clear_project("test_p3").await.ok(); // Create three nodes with known embeddings let texts = vec!["apple", "application", "banana"]; let embeddings: Vec> = texts .iter() .map(|t| hash_to_embedding(t)) .collect(); for (i, text) in texts.iter().enumerate() { let node = MemoryNode { sha256: format!("node_{}", i), level: Level::L1, project: "test_p3".to_string(), query_id: Some("q1".to_string()), run_id: "r1".to_string(), t: i as i32, source: None, text: text.to_string(), }; repo.upsert_node(&node).await.expect("upsert"); repo.upsert_vector(&node.sha256, VectorKind::Text, &embeddings[i]) .await .expect("upsert vector"); } // Query for "apple" — should rank apple closest let query_embedding = hash_to_embedding("apple"); let results = repo .search( &query_embedding, VectorKind::Text, &[Level::L1], &Scope::Project("test_p3".to_string()), 3, ) .await .expect("search"); assert_eq!(results.len(), 3, "Should return all 3 nodes"); // First result should be "apple" itself (distance ~0) assert_eq!( results[0].node.text, "apple", "Closest match should be 'apple' itself" ); assert!(results[0].distance < 0.01, "Distance to self should be ~0"); // Verify ordering matches hand-computed distances for i in 0..results.len() - 1 { assert!( results[i].distance <= results[i + 1].distance + 1e-5, "Results should be ordered by distance (ascending)" ); } repo.clear_project("test_p3").await.ok(); } #[tokio::test] #[ignore] async fn a4_level_filter() { let db_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgres://postgres:password@localhost:5432/poimen_test".to_string() }); if !is_postgres_available(&db_url).await { println!("Skipping: Postgres not available"); return; } let repo = PgRepo::connect(&db_url).await.expect("connect"); repo.clear_project("test_p4").await.ok(); let levels = vec![Level::L0, Level::L1, Level::L2]; let query_vec = hash_to_embedding("query"); for (i, level) in levels.iter().enumerate() { let node = MemoryNode { sha256: format!("node_{}", i), level: *level, project: "test_p4".to_string(), query_id: if *level == Level::L2 { None } else { Some("q1".to_string()) }, run_id: "r1".to_string(), t: i as i32, source: None, text: format!("memory at {:?}", level), }; repo.upsert_node(&node).await.expect("upsert"); repo.upsert_vector(&node.sha256, VectorKind::Text, &query_vec) .await .expect("vector"); } // Search with L1 only let results = repo .search( &query_vec, VectorKind::Text, &[Level::L1], &Scope::Project("test_p4".to_string()), 10, ) .await .expect("search"); assert_eq!(results.len(), 1, "Should return only L1"); assert_eq!(results[0].node.level, Level::L1); repo.clear_project("test_p4").await.ok(); } #[tokio::test] #[ignore] async fn a5_project_isolation() { let db_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgres://postgres:password@localhost:5432/poimen_test".to_string() }); if !is_postgres_available(&db_url).await { println!("Skipping: Postgres not available"); return; } let repo = PgRepo::connect(&db_url).await.expect("connect"); repo.clear_project("test_p5a").await.ok(); repo.clear_project("test_p5b").await.ok(); let query_vec = hash_to_embedding("test"); // Create identical nodes in two projects for proj in &["test_p5a", "test_p5b"] { let node = MemoryNode { sha256: format!("{}_node", proj), level: Level::L1, project: proj.to_string(), query_id: Some("q1".to_string()), run_id: "r1".to_string(), t: 0, source: None, text: "same memory".to_string(), }; repo.upsert_node(&node).await.expect("upsert"); repo.upsert_vector(&node.sha256, VectorKind::Text, &query_vec) .await .expect("vector"); } // Search in p5a — should NOT return p5b let results = repo .search( &query_vec, VectorKind::Text, &[Level::L1], &Scope::Project("test_p5a".to_string()), 10, ) .await .expect("search"); assert_eq!(results.len(), 1, "Should return only 1 result (from p5a)"); assert_eq!(results[0].node.project, "test_p5a"); repo.clear_project("test_p5a").await.ok(); repo.clear_project("test_p5b").await.ok(); } #[tokio::test] #[ignore] async fn a6_clear_project_scoped() { let db_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgres://postgres:password@localhost:5432/poimen_test".to_string() }); if !is_postgres_available(&db_url).await { println!("Skipping: Postgres not available"); return; } let repo = PgRepo::connect(&db_url).await.expect("connect"); repo.clear_project("test_p6a").await.ok(); repo.clear_project("test_p6b").await.ok(); let parent_node = MemoryNode { sha256: "parent".to_string(), level: Level::L0, project: "test_p6a".to_string(), query_id: Some("q1".to_string()), run_id: "r1".to_string(), t: 0, source: None, text: "parent".to_string(), }; let child_node = MemoryNode { sha256: "child".to_string(), level: Level::L1, project: "test_p6a".to_string(), query_id: Some("q1".to_string()), run_id: "r1".to_string(), t: 1, source: None, text: "child".to_string(), }; let other_node = MemoryNode { sha256: "other".to_string(), level: Level::L1, project: "test_p6b".to_string(), query_id: Some("q1".to_string()), run_id: "r1".to_string(), t: 0, source: None, text: "other".to_string(), }; // Setup: parent + child in p6a, edge between them; separate node in p6b repo.upsert_node(&parent_node).await.expect("insert parent"); repo.upsert_node(&child_node).await.expect("insert child"); repo.upsert_node(&other_node).await.expect("insert other"); repo.insert_edges("child", &["parent".to_string()]) .await .expect("edge"); let edge_count_before = repo.edge_count().await.expect("count"); assert_eq!(edge_count_before, 1); // Clear p6a repo.clear_project("test_p6a").await.expect("clear"); // Verify p6b is intact let other_count = sqlx::query_scalar::<_, i64>( "SELECT COUNT(*) FROM memory_node WHERE project = 'test_p6b'", ) .fetch_one(&repo.pool) .await .expect("query"); assert_eq!(other_count, 1, "Other project should be intact"); // Verify edges are gone (cascade delete) let edge_count_after = repo.edge_count().await.expect("count"); assert_eq!(edge_count_after, 0, "Edges should cascade-delete with nodes"); repo.clear_project("test_p6b").await.ok(); } #[tokio::test] #[ignore] async fn a8_parents_of() { let db_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| { "postgres://postgres:password@localhost:5432/poimen_test".to_string() }); if !is_postgres_available(&db_url).await { println!("Skipping: Postgres not available"); return; } let repo = PgRepo::connect(&db_url).await.expect("connect"); repo.clear_project("test_p8").await.ok(); let parent1 = MemoryNode { sha256: "p1".to_string(), level: Level::L0, project: "test_p8".to_string(), query_id: Some("q1".to_string()), run_id: "r1".to_string(), t: 0, source: None, text: "p1".to_string(), }; let parent2 = MemoryNode { sha256: "p2".to_string(), level: Level::L0, project: "test_p8".to_string(), query_id: Some("q1".to_string()), run_id: "r1".to_string(), t: 1, source: None, text: "p2".to_string(), }; let child = MemoryNode { sha256: "c1".to_string(), level: Level::L1, project: "test_p8".to_string(), query_id: Some("q1".to_string()), run_id: "r1".to_string(), t: 2, source: None, text: "c1".to_string(), }; repo.upsert_node(&parent1).await.expect("insert p1"); repo.upsert_node(&parent2).await.expect("insert p2"); repo.upsert_node(&child).await.expect("insert c1"); repo.insert_edges("c1", &["p1".to_string(), "p2".to_string()]) .await .expect("edges"); let parents = repo.parents_of("c1").await.expect("parents"); assert_eq!(parents.len(), 2, "Should return 2 parents"); let parent_shas: Vec = parents.iter().map(|p| p.sha256.clone()).collect(); assert!(parent_shas.contains(&"p1".to_string())); assert!(parent_shas.contains(&"p2".to_string())); repo.clear_project("test_p8").await.ok(); } /// Helper: Check if Postgres is available (for CI compatibility) async fn is_postgres_available(url: &str) -> bool { match sqlx::postgres::PgPoolOptions::new() .max_connections(1) .connect(url) .await { Ok(_) => true, Err(_) => false, } }