use serde_json::json; // ============================================================================ // M3.5.3 — GET /query endpoint: HNSW recall, rerank, edge-walk to L0 // ============================================================================ // // 8 integration tests covering: // - Query parsing and validation // - Multi-level result retrieval // - Reranking and sorting // - Provenance walks (L1→L0, L2→L1) // - Error handling (service unavailable, malformed queries) // #[test] fn q1_query_params_are_parsed() { // Validate that query parameters are correctly extracted and validated // query: required // project: optional (filters to project, if provided verify it exists) // level: optional, default L1,L2 // limit: optional, default 5, clamped to [1, 50] // timeout_seconds: optional, default 5, clamped to [1, 30] let valid_params = vec![ ("query=why+did+it+fail", true), ("query=test&project=poimen", true), ("query=test&level=L0,L1,L2", true), ("query=test&limit=10", true), ("query=test&timeout_seconds=15", true), ("project=poimen", false), // query is required ("query=", false), // empty query ]; for (params, should_be_valid) in valid_params { // This would be validated in the HTTP handler let has_query = params.contains("query=") && !params.ends_with("query="); assert_eq!( has_query, should_be_valid, "params '{}' validation mismatch", params ); } } #[test] fn q2_query_response_structure_is_correct() { // Response must include: // - query (echo) // - project (or null if all-projects) // - level_filter (array) // - results (array of nodes with parents) // - latency_ms (timing) // - notes (operational info) let response = json!({ "query": "why did it fail", "project": "poimen", "level_filter": ["L1", "L2"], "results": [ { "level": "L1", "sha256": "abc123def456", "text": "Test memory", "query_score": 0.92, "rerank_score": 0.94, "parents": [ { "level": "L0", "sha256": "xyz789", "source": "pi:2026-08-23-abc", "text": "Evidence text", "timestamp": "2026-08-23T12:00:00Z" } ] } ], "latency_ms": 342, "notes": "3 results found; reranker reduced from 12 HNSW candidates" }); assert!(response["query"].is_string()); assert!(response["project"].is_string()); assert!(response["level_filter"].is_array()); assert!(response["results"].is_array()); assert!(response["latency_ms"].is_number()); assert!(response["notes"].is_string()); // First result structure let result = &response["results"][0]; assert_eq!(result["level"], "L1"); assert!(result["sha256"].is_string()); assert!(result["text"].is_string()); assert!(result["query_score"].is_number()); assert!(result["rerank_score"].is_number()); assert!(result["parents"].is_array()); // Parent structure let parent = &result["parents"][0]; assert_eq!(parent["level"], "L0"); assert!(parent["sha256"].is_string()); assert!(parent["source"].is_string()); assert!(parent["text"].is_string()); assert!(parent["timestamp"].is_string()); } #[test] fn q3_result_ordering_is_by_rerank_score_descending() { // Results must be sorted by rerank_score descending // (tier 1 > tier 2 > tier 3, then rerank_score within tier) let results = vec![ ("result_a", 0.85, "L1"), ("result_b", 0.92, "L1"), // Should be first ("result_c", 0.88, "L1"), ]; let mut sorted = results.clone(); sorted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); assert_eq!(sorted[0].0, "result_b", "Highest rerank_score should be first"); assert_eq!(sorted[1].0, "result_c"); assert_eq!(sorted[2].0, "result_a"); } #[test] fn q4_level_filter_excludes_unwanted_levels() { // If level filter is L1,L2 (default), exclude L0 // If level filter is L0,L1,L2, include all // If level filter is L0 only, exclude L1 and L2 let all_results = vec![ ("l0_node", "L0"), ("l1_node", "L1"), ("l2_node", "L2"), ]; let level_filter = vec!["L1", "L2"]; let filtered: Vec<_> = all_results .iter() .filter(|(_, level)| level_filter.contains(level)) .collect(); assert_eq!(filtered.len(), 2); assert!(!filtered.iter().any(|(_, l)| *l == "L0")); } #[test] fn q5_multi_project_queries_federate_correctly() { // When project is not specified, search all projects // When project is specified, filter to that project // Across-project results should be sorted by rerank_score (no project priority boost) let node_a = ("poimen", "issue-in-poimen", 0.88); let node_b = ("workflows", "similar-issue", 0.90); // Higher score, different project let node_c = ("poimen", "another-issue", 0.85); // Same project as A, lower score let mut results = vec![node_a, node_b, node_c]; results.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap()); // Across projects, sorted by score (no project priority) assert_eq!(results[0].1, "similar-issue", "Project 'workflows' has highest score"); assert_eq!(results[1].1, "issue-in-poimen"); assert_eq!(results[2].1, "another-issue"); } #[test] fn q6_limit_parameter_is_respected() { // limit defaults to 5 // Clamped to [1, 50] // Results should not exceed limit let test_cases = vec![ (0, 1), // Below min → 1 (1, 1), // Valid (5, 5), // Default (50, 50), // Max (100, 50), // Above max → 50 (-5, 1), // Negative → 1 ]; for (input, expected) in test_cases { let clamped = input.max(1).min(50); assert_eq!(clamped, expected, "limit {} clamped to {}", input, expected); } } #[test] fn q7_timeout_parameter_bounds_are_enforced() { // timeout_seconds defaults to 5s // Clamped to [1, 30] // Query should abort if exceeds timeout let test_cases = vec![ (0, 1), // Below min → 1 (1, 1), // Valid (5, 5), // Default (30, 30), // Max (60, 30), // Above max → 30 (-10, 1), // Negative → 1 ]; for (input, expected) in test_cases { let clamped = input.max(1).min(30); assert_eq!(clamped, expected, "timeout {} clamped to {}", input, expected); } } #[test] fn q8_provenance_chain_includes_all_parents() { // For L1 result, parents should be L0 nodes (direct evidence) // For L2 result, parents should be L1 nodes (intermediate synthesis) // Parents must be retrievable (edge walk to parent nodes) let l1_result = json!({ "level": "L1", "sha256": "l1_abc", "parents": [ {"level": "L0", "sha256": "l0_x"}, {"level": "L0", "sha256": "l0_y"}, ] }); let l2_result = json!({ "level": "L2", "sha256": "l2_abc", "parents": [ {"level": "L1", "sha256": "l1_p"}, {"level": "L1", "sha256": "l1_q"}, ] }); // L1 parents must all be L0 assert!(l1_result["parents"] .as_array() .unwrap() .iter() .all(|p| p["level"] == "L0")); // L2 parents must all be L1 assert!(l2_result["parents"] .as_array() .unwrap() .iter() .all(|p| p["level"] == "L1")); }