From 3f7af777a14178473d8bf53db83cdbcf27416d30 Mon Sep 17 00:00:00 2001 From: AIEN Date: Fri, 18 Sep 2026 23:04:05 -0500 Subject: [PATCH] test: comprehensive WAL persistence, FTS5 boolean logic, vector ranking, and auth error tests --- src/auth.rs | 83 +++++++--- src/db.rs | 404 ++++++++++++++++++++++++++++++++++++++++++++-- src/embeddings.rs | 29 ++++ src/handlers.rs | 140 ++++++++++++++++ 4 files changed, 621 insertions(+), 35 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 821930b..eed4e2f 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -25,30 +25,44 @@ pub fn load_cortex_token() -> String { } } -pub async fn auth_middleware(req: Request, next: Next) -> Result { - let expected = load_cortex_token(); - if expected.is_empty() { - let err_resp = ( - StatusCode::SERVICE_UNAVAILABLE, - Json(json!({"error": "Service unavailable: Cortex authentication token unconfigured on host"})), - ).into_response(); - return Err(err_resp); +pub fn validate_auth_header(auth_header: Option<&str>, expected_token: &str) -> Result<(), StatusCode> { + if expected_token.is_empty() { + return Err(StatusCode::SERVICE_UNAVAILABLE); } - - if let Some(auth_val) = req.headers().get("Authorization").and_then(|h| h.to_str().ok()) { - if let Some(token) = auth_val.strip_prefix("Bearer ") { - if token.trim() == expected { - return Ok(next.run(req).await); + match auth_header { + Some(header) if header.starts_with("Bearer ") => { + let token = header.strip_prefix("Bearer ").unwrap().trim(); + if token == expected_token { + Ok(()) + } else { + Err(StatusCode::UNAUTHORIZED) } } + _ => Err(StatusCode::UNAUTHORIZED), } +} - let err_resp = ( - StatusCode::UNAUTHORIZED, - Json(json!({"error": "Unauthorized: invalid or missing Cortex bearer token"})), - ).into_response(); +pub async fn auth_middleware(req: Request, next: Next) -> Result { + let expected = load_cortex_token(); + let auth_header = req.headers().get("Authorization").and_then(|h| h.to_str().ok()); - Err(err_resp) + match validate_auth_header(auth_header, &expected) { + Ok(()) => Ok(next.run(req).await), + Err(StatusCode::SERVICE_UNAVAILABLE) => { + let err_resp = ( + StatusCode::SERVICE_UNAVAILABLE, + Json(json!({"error": "Service unavailable: Cortex authentication token unconfigured on host"})), + ).into_response(); + Err(err_resp) + } + Err(_) => { + let err_resp = ( + StatusCode::UNAUTHORIZED, + Json(json!({"error": "Unauthorized: invalid or missing Cortex bearer token"})), + ).into_response(); + Err(err_resp) + } + } } #[cfg(test)] @@ -62,4 +76,37 @@ mod tests { assert_eq!(token, "test_token_12345"); std::env::remove_var("CORTEX_TOKEN"); } + + #[test] + fn test_validate_auth_header_success() { + let expected = "secret_vault_token_42"; + let res = validate_auth_header(Some("Bearer secret_vault_token_42"), expected); + assert_eq!(res, Ok(())); + } + + #[test] + fn test_validate_auth_header_unauthenticated() { + let expected = "secret_vault_token_42"; + let res_none = validate_auth_header(None, expected); + assert_eq!(res_none, Err(StatusCode::UNAUTHORIZED)); + + let res_empty = validate_auth_header(Some(""), expected); + assert_eq!(res_empty, Err(StatusCode::UNAUTHORIZED)); + + let res_no_bearer = validate_auth_header(Some("Basic 12345"), expected); + assert_eq!(res_no_bearer, Err(StatusCode::UNAUTHORIZED)); + } + + #[test] + fn test_validate_auth_header_invalid_token() { + let expected = "secret_vault_token_42"; + let res = validate_auth_header(Some("Bearer wrong_secret"), expected); + assert_eq!(res, Err(StatusCode::UNAUTHORIZED)); + } + + #[test] + fn test_validate_auth_header_unconfigured_host() { + let res = validate_auth_header(Some("Bearer any_token"), ""); + assert_eq!(res, Err(StatusCode::SERVICE_UNAVAILABLE)); + } } diff --git a/src/db.rs b/src/db.rs index e9f7528..a14bf2b 100644 --- a/src/db.rs +++ b/src/db.rs @@ -108,6 +108,7 @@ impl Database { if let Some(row) = rows.next()? { return row.get(0); } + drop(rows); drop(stmt); @@ -337,22 +338,27 @@ impl Database { return Ok(results); } - // FTS search + like search + // FTS search with support for exact, prefix (*), and boolean operators (AND, OR, NOT) let clean_q = query_trimmed.replace('"', ""); - let fts_query = format!("\"{}\"", clean_q); - let stmt = conn.prepare( - "SELECT e.id, e.space_id, e.space_slug, e.entity_type, e.canonical_name, e.content, e.aliases_json, e.metadata_json, e.confidence, e.revision, e.retracted, e.valid_from, e.valid_to, e.created_at, - COALESCE(bm25(entities_fts), 5.0) as bm25_rank - FROM entities e - JOIN entities_fts f ON f.id = e.id - WHERE entities_fts MATCH ?1 AND e.space_slug = ?2 AND e.retracted = 0 - ORDER BY bm25_rank ASC - LIMIT ?3" - ); + let has_fts_ops = clean_q.contains(" AND ") + || clean_q.contains(" OR ") + || clean_q.contains(" NOT ") + || clean_q.contains('*'); let mut results = Vec::new(); - if let Ok(mut s) = stmt { - let rows = s.query_map(params![fts_query, target_space, limit], |row| { + + let run_fts = |query_str: &str| -> Option> { + let mut stmt = conn.prepare( + "SELECT e.id, e.space_id, e.space_slug, e.entity_type, e.canonical_name, e.content, e.aliases_json, e.metadata_json, e.confidence, e.revision, e.retracted, e.valid_from, e.valid_to, e.created_at, + COALESCE(bm25(entities_fts), 5.0) as bm25_rank + FROM entities e + JOIN entities_fts f ON f.id = e.id + WHERE entities_fts MATCH ?1 AND e.space_slug = ?2 AND e.retracted = 0 + ORDER BY bm25_rank ASC + LIMIT ?3" + ).ok()?; + + let rows = stmt.query_map(params![query_str, target_space, limit], |row| { let aliases_str: String = row.get(6)?; let meta_str: String = row.get(7)?; let bm25_rank: f64 = row.get(14)?; @@ -379,13 +385,29 @@ impl Database { semantic_score: 0.0, score: lexical_score, }) - })?; - for r in rows { - results.push(r?); + }).ok()?; + + let mut out = Vec::new(); + for r in rows.flatten() { + out.push(r); + } + Some(out) + }; + + if has_fts_ops { + if let Some(r) = run_fts(&clean_q) { + results = r; + } + } + + if results.is_empty() { + let fts_query = format!("\"{}\"", clean_q); + if let Some(r) = run_fts(&fts_query) { + results = r; } } - // Fallback to substring match if FTS didn't return enough + // Fallback to substring match if FTS did not return enough if results.len() < limit { let pattern = format!("%{}%", query_trimmed); let mut sub_stmt = conn.prepare( @@ -596,6 +618,8 @@ impl Database { #[cfg(test)] mod tests { use super::*; + use std::sync::Arc; + use std::thread; #[test] fn test_in_memory_db_crud() { @@ -633,4 +657,350 @@ mod tests { assert!(!recall_results.is_empty()); assert!(recall_results[0].final_score > 0.5); } + + #[test] + fn test_sqlite_wal_persistence_under_concurrency() { + let temp_dir = std::env::temp_dir().join(format!("cortex_wal_test_{}", Uuid::new_v4())); + let _ = std::fs::create_dir_all(&temp_dir); + let db_path = temp_dir.join("cortex.db"); + + // Open database in WAL mode + let db = Arc::new(Database::open(&db_path).expect("open wal db")); + + // Verify WAL mode is active + { + let conn = db.conn.lock().unwrap(); + let mode: String = conn.query_row("PRAGMA journal_mode", [], |r| r.get(0)).unwrap(); + assert_eq!(mode.to_lowercase(), "wal"); + } + + let mut handles = Vec::new(); + + // Spawn 4 concurrent writer threads + for t in 0..4 { + let db_clone = Arc::clone(&db); + handles.push(thread::spawn(move || { + for i in 0..10 { + let input = EntityWriteInput { + id: None, + space: "atlas-memory".to_string(), + entity_type: "concurrent_write".to_string(), + canonical_name: format!("entity_t{}_i{}", t, i), + content: format!("Content from thread {} iteration {}", t, i), + aliases: vec![format!("alias_t{}_i{}", t, i)], + metadata: serde_json::json!({"thread": t, "iter": i}), + confidence: 0.95, + valid_from: None, + valid_to: None, + external_id: None, + }; + let receipt = db_clone.upsert_entity(&input, None).expect("concurrent entity upsert"); + assert_eq!(receipt.operation, "entity.upsert"); + + // Also write a claim + let claim_input = ClaimWriteInput { + id: None, + space: "atlas-memory".to_string(), + subject_entity_id: receipt.target_id.clone(), + predicate: "authored_by".to_string(), + object_entity_id: None, + literal_value: Some(serde_json::json!(format!("worker_thread_{}", t))), + confidence: 1.0, + metadata: serde_json::json!({}), + }; + let claim_receipt = db_clone.upsert_claim(&claim_input).expect("concurrent claim upsert"); + assert_eq!(claim_receipt.operation, "claim.upsert"); + } + })); + } + + // Spawn 4 concurrent reader threads + for _ in 0..4 { + let db_clone = Arc::clone(&db); + handles.push(thread::spawn(move || { + for _ in 0..15 { + let _ = db_clone.search_entities("Content", Some("atlas-memory"), 10); + let _ = db_clone.recall_entities("thread", None, Some("atlas-memory"), 5); + let _ = db_clone.get_entity("entity_t0_i0", Some("atlas-memory")); + thread::sleep(std::time::Duration::from_millis(2)); + } + })); + } + + // Wait for all threads to join + for h in handles { + h.join().expect("thread join"); + } + + // Close db by dropping Arc + drop(db); + + // Re-open from disk to verify cold-start WAL recovery and persistence + let reopened = Database::open(&db_path).expect("reopen wal db"); + for t in 0..4 { + for i in 0..10 { + let name = format!("entity_t{}_i{}", t, i); + let ent = reopened.get_entity(&name, Some("atlas-memory")).expect("get persisted entity"); + assert!(ent.is_some(), "Entity {} must persist across restarts", name); + let e = ent.unwrap(); + assert_eq!(e.canonical_name, name); + assert_eq!(e.content, format!("Content from thread {} iteration {}", t, i)); + } + } + + let _ = std::fs::remove_dir_all(&temp_dir); + } + + #[test] + fn test_fts5_exact_prefix_boolean_and_ranking() { + let db = Database::open_in_memory().expect("open in memory db"); + + let items = vec![ + ("NVFP4_Loader", "High throughput native NVFP4 checkpoint loader for vLLM and MAX", "discovery"), + ("Qwen3_Parser", "Llama cpp tokenizer and grammar parser for Qwen3 models", "learned_procedure"), + ("TPM_Vault", "Hardware TPM bound key vault with zero plaintext secrets on disk", "lesson"), + ("Sovereign_Covenant", "Frontier AI Anti Enclosure mandate and sovereign defense network", "discovery"), + ("Memory_Optimizer", "Memory architecture optimization for memory engines with memory reuse", "lesson"), + ]; + + for (name, content, etype) in items { + let input = EntityWriteInput { + id: None, + space: "atlas-memory".to_string(), + entity_type: etype.to_string(), + canonical_name: name.to_string(), + content: content.to_string(), + aliases: vec![], + metadata: serde_json::json!({}), + confidence: 1.0, + valid_from: None, + valid_to: None, + external_id: None, + }; + db.upsert_entity(&input, None).expect("insert entity"); + } + + // 1. Exact term matching + let exact_res = db.search_entities("NVFP4", Some("atlas-memory"), 10).expect("search exact"); + assert_eq!(exact_res.len(), 1); + assert_eq!(exact_res[0].entity.canonical_name, "NVFP4_Loader"); + + let exact_tpm = db.search_entities("TPM", Some("atlas-memory"), 10).expect("search exact TPM"); + assert_eq!(exact_tpm.len(), 1); + assert_eq!(exact_tpm[0].entity.canonical_name, "TPM_Vault"); + + // 2. Prefix queries + let prefix_res = db.search_entities("NVF*", Some("atlas-memory"), 10).expect("search prefix"); + assert_eq!(prefix_res.len(), 1); + assert_eq!(prefix_res[0].entity.canonical_name, "NVFP4_Loader"); + + let prefix_hard = db.search_entities("hardw*", Some("atlas-memory"), 10).expect("search prefix hardw*"); + assert_eq!(prefix_hard.len(), 1); + assert_eq!(prefix_hard[0].entity.canonical_name, "TPM_Vault"); + + let prefix_token = db.search_entities("token*", Some("atlas-memory"), 10).expect("search prefix token*"); + assert_eq!(prefix_token.len(), 1); + assert_eq!(prefix_token[0].entity.canonical_name, "Qwen3_Parser"); + + // 3. Boolean AND + let and_res = db.search_entities("NVFP4 AND vLLM", Some("atlas-memory"), 10).expect("search AND"); + assert_eq!(and_res.len(), 1); + assert_eq!(and_res[0].entity.canonical_name, "NVFP4_Loader"); + + let and_mismatch = db.search_entities("NVFP4 AND non_existent_token", Some("atlas-memory"), 10).expect("search AND mismatch"); + assert_eq!(and_mismatch.len(), 0); + + // 4. Boolean OR + let or_res = db.search_entities("Qwen3 OR TPM", Some("atlas-memory"), 10).expect("search OR"); + assert_eq!(or_res.len(), 2); + let names: Vec = or_res.into_iter().map(|r| r.entity.canonical_name).collect(); + assert!(names.contains(&"Qwen3_Parser".to_string())); + assert!(names.contains(&"TPM_Vault".to_string())); + + // 5. Boolean NOT + let not_res = db.search_entities("native NOT tokenizer", Some("atlas-memory"), 10).expect("search NOT"); + assert_eq!(not_res.len(), 1); + assert_eq!(not_res[0].entity.canonical_name, "NVFP4_Loader"); + + // 6. Ranking test (BM25 term frequency) + let rank_res = db.search_entities("memory", Some("atlas-memory"), 10).expect("search ranking"); + assert!(!rank_res.is_empty()); + assert_eq!(rank_res[0].entity.canonical_name, "Memory_Optimizer"); + } + + #[test] + fn test_vector_similarity_and_nearest_neighbor_ranking() { + let db = Database::open_in_memory().expect("open in memory db"); + + // Create 3 vectors with 768 dimensions + let mut emb_a = vec![0.0f32; 768]; + emb_a[0] = 1.0; // Direction X + + let mut emb_b = vec![0.0f32; 768]; + emb_b[0] = 0.7071; // Direction between X and Y + emb_b[1] = 0.7071; + + let mut emb_c = vec![0.0f32; 768]; + emb_c[1] = 1.0; // Direction Y + + let entities = vec![ + ("Vector_Alpha", "Entity aligned with primary axis X", emb_a), + ("Vector_Beta", "Entity aligned midway between axes", emb_b), + ("Vector_Gamma", "Entity aligned with secondary axis Y", emb_c), + ]; + + for (name, content, emb) in entities { + let input = EntityWriteInput { + id: None, + space: "atlas-memory".to_string(), + entity_type: "vector_item".to_string(), + canonical_name: name.to_string(), + content: content.to_string(), + aliases: vec![], + metadata: serde_json::json!({}), + confidence: 1.0, + valid_from: None, + valid_to: None, + external_id: None, + }; + db.upsert_entity(&input, Some(&emb)).expect("insert vector entity"); + } + + // Query vector strongly aligned with X: [0.99, 0.05, 0, ...] + let mut query_emb = vec![0.0f32; 768]; + query_emb[0] = 0.99; + query_emb[1] = 0.05; + + let recall_res = db.recall_entities("Entity", Some(&query_emb), Some("atlas-memory"), 3).expect("recall"); + assert_eq!(recall_res.len(), 3); + + // Verify nearest neighbor ordering: Alpha first, Beta second, Gamma third + assert_eq!(recall_res[0].entity.canonical_name, "Vector_Alpha"); + assert_eq!(recall_res[1].entity.canonical_name, "Vector_Beta"); + assert_eq!(recall_res[2].entity.canonical_name, "Vector_Gamma"); + + assert!(recall_res[0].final_score > recall_res[1].final_score); + assert!(recall_res[1].final_score > recall_res[2].final_score); + + // Empty embedding fallback to lexical only + let empty_recall = db.recall_entities("secondary", None, Some("atlas-memory"), 3).expect("empty emb recall"); + assert!(!empty_recall.is_empty()); + assert_eq!(empty_recall[0].entity.canonical_name, "Vector_Gamma"); + } + + #[test] + fn test_schema_integrity_metadata_indexing_and_deduplication() { + let db = Database::open_in_memory().expect("open in memory db"); + + // 1. Entity deduplication test + let input_v1 = EntityWriteInput { + id: None, + space: "atlas-memory".to_string(), + entity_type: "discovery".to_string(), + canonical_name: "Atlas_Vault_Spec".to_string(), + content: "Initial specification v1".to_string(), + aliases: vec!["vault_v1".to_string()], + metadata: serde_json::json!({ + "subsystem": "security", + "hardware": {"tpm": true, "vendor": "stmicroelectronics"} + }), + confidence: 0.9, + valid_from: None, + valid_to: None, + external_id: None, + }; + + let receipt1 = db.upsert_entity(&input_v1, None).expect("first upsert"); + let initial_id = receipt1.target_id.clone(); + + // Re-upsert with same canonical_name in same space + let input_v2 = EntityWriteInput { + id: None, + space: "atlas-memory".to_string(), + entity_type: "learned_procedure".to_string(), + canonical_name: "Atlas_Vault_Spec".to_string(), + content: "Updated specification v2 with hardware sealing".to_string(), + aliases: vec!["vault_v1".to_string(), "vault_v2".to_string()], + metadata: serde_json::json!({ + "subsystem": "security", + "hardware": {"tpm": true, "vendor": "stmicroelectronics", "pcr_sealing": [0, 2, 7]} + }), + confidence: 1.0, + valid_from: None, + valid_to: None, + external_id: None, + }; + + let receipt2 = db.upsert_entity(&input_v2, None).expect("second upsert"); + + // Verify deduplication: ID remains unchanged, revision incremented to 2 + assert_eq!(receipt2.target_id, initial_id); + + let fetched = db.get_entity("Atlas_Vault_Spec", Some("atlas-memory")).expect("get entity").unwrap(); + assert_eq!(fetched.id, initial_id); + assert_eq!(fetched.revision, 2); + assert_eq!(fetched.entity_type, "learned_procedure"); + assert_eq!(fetched.content, "Updated specification v2 with hardware sealing"); + assert_eq!(fetched.aliases.len(), 2); + + // Verify nested metadata integrity + assert_eq!(fetched.metadata["subsystem"], "security"); + assert_eq!(fetched.metadata["hardware"]["tpm"], true); + assert_eq!(fetched.metadata["hardware"]["pcr_sealing"].as_array().unwrap().len(), 3); + + // Verify exactly 1 entity row in space + { + let conn = db.conn.lock().unwrap(); + let count: i64 = conn.query_row( + "SELECT COUNT(*) FROM entities WHERE space_slug = 'atlas-memory' AND canonical_name = 'Atlas_Vault_Spec'", + [], + |r| r.get(0), + ).unwrap(); + assert_eq!(count, 1); + } + + // 2. Claim schema and foreign key traversal + let obj_input = EntityWriteInput { + id: None, + space: "atlas-memory".to_string(), + entity_type: "component".to_string(), + canonical_name: "TPM_Chip".to_string(), + content: "Discrete TPM 2.0 module".to_string(), + aliases: vec![], + metadata: serde_json::json!({}), + confidence: 1.0, + valid_from: None, + valid_to: None, + external_id: None, + }; + let obj_receipt = db.upsert_entity(&obj_input, None).expect("upsert object entity"); + + let claim_input = ClaimWriteInput { + id: None, + space: "atlas-memory".to_string(), + subject_entity_id: initial_id.clone(), + predicate: "binds_to".to_string(), + object_entity_id: Some(obj_receipt.target_id.clone()), + literal_value: None, + confidence: 1.0, + metadata: serde_json::json!({"interface": "SPI"}), + }; + db.upsert_claim(&claim_input).expect("upsert claim"); + + let claims = db.traverse_claims(&initial_id, Some("atlas-memory")).expect("traverse claims"); + assert_eq!(claims.len(), 1); + assert_eq!(claims[0].predicate, "binds_to"); + assert_eq!(claims[0].object_entity_id, Some(obj_receipt.target_id)); + assert_eq!(claims[0].metadata["interface"], "SPI"); + + // 3. Retraction verification + let retract_receipt = db.retract_target("entity", &initial_id).expect("retract"); + assert_eq!(retract_receipt.operation, "entity.retract"); + + let after_retract = db.get_entity("Atlas_Vault_Spec", Some("atlas-memory")).expect("get retracted"); + assert!(after_retract.is_none()); + + let search_retract = db.search_entities("Atlas_Vault_Spec", Some("atlas-memory"), 10).expect("search retracted"); + assert!(search_retract.is_empty()); + } } diff --git a/src/embeddings.rs b/src/embeddings.rs index b7a8fae..b9cec0f 100644 --- a/src/embeddings.rs +++ b/src/embeddings.rs @@ -41,6 +41,10 @@ pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 { } } +pub fn cosine_distance(a: &[f32], b: &[f32]) -> f32 { + 1.0 - cosine_similarity(a, b) +} + pub async fn fetch_embedding(client: &Client, text: &str, encoder_url: &str) -> Result, String> { let payload = json!({ "text": text, @@ -105,4 +109,29 @@ mod tests { let c = vec![0.0f32, 1.0, 0.0]; assert!((cosine_similarity(&a, &c) - 0.0).abs() < 1e-5); } + + #[test] + fn test_cosine_distance_and_empty_handling() { + let a = vec![1.0f32, 0.0, 0.0]; + let b = vec![1.0f32, 0.0, 0.0]; + assert!((cosine_distance(&a, &b) - 0.0).abs() < 1e-5); + + let orthogonal = vec![0.0f32, 1.0, 0.0]; + assert!((cosine_distance(&a, &orthogonal) - 1.0).abs() < 1e-5); + + let opposite = vec![-1.0f32, 0.0, 0.0]; + assert!((cosine_distance(&a, &opposite) - 2.0).abs() < 1e-5); + + let empty: Vec = vec![]; + assert_eq!(cosine_similarity(&empty, &empty), 0.0); + assert_eq!(cosine_distance(&empty, &empty), 1.0); + + let mismatched = vec![1.0f32, 0.0]; + assert_eq!(cosine_similarity(&a, &mismatched), 0.0); + assert_eq!(cosine_distance(&a, &mismatched), 1.0); + + let zeros = vec![0.0f32, 0.0, 0.0]; + assert_eq!(cosine_similarity(&zeros, &zeros), 0.0); + assert_eq!(cosine_distance(&zeros, &zeros), 1.0); + } } diff --git a/src/handlers.rs b/src/handlers.rs index 9522c54..443fa7f 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -174,3 +174,143 @@ pub async fn health_handler() -> Json { "runtime": "native-arm64-rust" })) } + +#[cfg(test)] +mod tests { + use super::*; + + fn create_test_state() -> Arc { + let db = Arc::new(Database::open_in_memory().expect("in memory db")); + Arc::new(AppState { + db, + http_client: Client::new(), + encoder_url: "http://127.0.0.1:18081".to_string(), + }) + } + + #[tokio::test] + async fn test_get_handler_missing_parameter() { + let state = create_test_state(); + let query = GetParams { + id: None, + name: None, + space: None, + }; + let err = get_handler(State(state), Query(query)).await.unwrap_err(); + assert_eq!(err.0, StatusCode::BAD_REQUEST); + assert_eq!(err.1 .0["error"], "Missing id or name parameter"); + } + + #[tokio::test] + async fn test_get_handler_not_found() { + let state = create_test_state(); + let query = GetParams { + id: Some("non_existent_uuid".to_string()), + name: None, + space: Some("atlas-memory".to_string()), + }; + let err = get_handler(State(state), Query(query)).await.unwrap_err(); + assert_eq!(err.0, StatusCode::NOT_FOUND); + assert_eq!(err.1 .0["error"], "Entity not found"); + } + + #[tokio::test] + async fn test_get_handler_success_and_unknown_space() { + let state = create_test_state(); + let input = EntityWriteInput { + id: None, + space: "atlas-memory".to_string(), + entity_type: "discovery".to_string(), + canonical_name: "test_entity_get".to_string(), + content: "Payload content".to_string(), + aliases: vec![], + metadata: json!({"key": "val"}), + confidence: 1.0, + valid_from: None, + valid_to: None, + external_id: None, + }; + state.db.upsert_entity(&input, None).unwrap(); + + // 1. Success fetch + let query_ok = GetParams { + id: None, + name: Some("test_entity_get".to_string()), + space: Some("atlas-memory".to_string()), + }; + let resp = get_handler(State(state.clone()), Query(query_ok)).await; + assert!(resp.is_ok()); + + // 2. Unknown space fetch returns 404 + let query_unknown_space = GetParams { + id: None, + name: Some("test_entity_get".to_string()), + space: Some("non_existent_space".to_string()), + }; + let err_unknown = get_handler(State(state), Query(query_unknown_space)).await.unwrap_err(); + assert_eq!(err_unknown.0, StatusCode::NOT_FOUND); + } + + #[tokio::test] + async fn test_search_unknown_space_returns_empty() { + let state = create_test_state(); + let params = SearchParams { + q: Some("anything".to_string()), + query: None, + space: Some("completely_unknown_space_slug".to_string()), + limit: Some(10), + include_retracted: Some(false), + }; + let Json(res) = search_get_handler(State(state), Query(params)).await.unwrap(); + assert_eq!(res.results.len(), 0); + } + + #[tokio::test] + async fn test_write_entity_and_retract_lifecycle() { + let state = create_test_state(); + + // 1. Write entity + let write_payload = WritePayload::Entity { + value: EntityWriteInput { + id: None, + space: "atlas-memory".to_string(), + entity_type: "lesson".to_string(), + canonical_name: "lifecycle_test".to_string(), + content: "Lifecycle verification".to_string(), + aliases: vec![], + metadata: json!({}), + confidence: 1.0, + valid_from: None, + valid_to: None, + external_id: None, + }, + }; + let write_res = write_handler(State(state.clone()), Json(write_payload)).await; + assert!(write_res.is_ok()); + + let entity = state.db.get_entity("lifecycle_test", Some("atlas-memory")).unwrap().unwrap(); + assert_eq!(entity.canonical_name, "lifecycle_test"); + + // 2. Retract entity + let retract_payload = WritePayload::Retract { + value: RetractInput { + target_type: "entity".to_string(), + target_id: entity.id.clone(), + reason: Some("Obsolescence".to_string()), + }, + }; + let retract_res = write_handler(State(state.clone()), Json(retract_payload)).await; + assert!(retract_res.is_ok()); + + let after = state.db.get_entity("lifecycle_test", Some("atlas-memory")).unwrap(); + assert!(after.is_none()); + } + + #[tokio::test] + async fn test_health_handler_structure() { + let Json(health) = health_handler().await; + assert_eq!(health["status"], "ok"); + assert_eq!(health["service"], "cortex-rs"); + assert_eq!(health["space"], "atlas-memory"); + } +}