#![allow(dead_code)] use rusqlite::Connection; use serde::Deserialize; use std::path::PathBuf; use lore::search::{FtsQueryMode, SearchFilters, SearchMode, apply_filters, search_fts}; #[derive(Debug, Deserialize)] struct GoldenQuery { query: String, mode: String, #[serde(default)] filters: GoldenFilters, expected_doc_ids: Vec, min_results: usize, max_rank: usize, description: String, } #[derive(Debug, Default, Deserialize)] struct GoldenFilters { source_type: Option, author: Option, project: Option, #[serde(default)] labels: Vec, } fn load_golden_queries() -> Vec { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/golden_queries.json"); let content = std::fs::read_to_string(&path) .unwrap_or_else(|_| panic!("Failed to read golden queries fixture")); serde_json::from_str(&content) .unwrap_or_else(|e| panic!("Failed to parse golden queries: {}", e)) } fn create_seeded_db() -> Connection { let conn = Connection::open_in_memory().unwrap(); conn.pragma_update(None, "foreign_keys", "ON").unwrap(); let migrations_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("migrations"); for version in 1..=8 { let entries: Vec<_> = std::fs::read_dir(&migrations_dir) .unwrap() .filter_map(|e| e.ok()) .filter(|e| { e.file_name() .to_string_lossy() .starts_with(&format!("{:03}", version)) }) .collect(); assert!(!entries.is_empty(), "Migration {} not found", version); let sql = std::fs::read_to_string(entries[0].path()).unwrap(); conn.execute_batch(&sql) .unwrap_or_else(|e| panic!("Migration {} failed: {}", version, e)); } conn.execute( "INSERT INTO projects (id, gitlab_project_id, path_with_namespace, web_url) VALUES (1, 100, 'group/project', 'https://gitlab.example.com/group/project')", [], ) .unwrap(); let documents = vec![ ( 1, "issue", "Authentication and login broken with OAuth", "Users cannot login when using OAuth tokens. The JWT token refresh fails silently, \ causing authentication errors. When the access token expires, the refresh flow returns \ a 401 instead of fetching new credentials. Login page shows a generic error. \ Multiple users reported authentication failures across all OAuth providers.", "testuser", ), ( 2, "merge_request", "Add user profile page with avatar upload", "This merge request adds a new user profile page. Users can now upload their avatar, \ edit their display name, and manage notification preferences. The profile page includes \ responsive design for mobile and desktop viewports.", "developer1", ), ( 3, "issue", "Database migration failing on PostgreSQL 14", "The database migration script crashes on PostgreSQL 14 due to deprecated syntax. \ The ALTER TABLE command uses a syntax removed in PG14. Migration 042 needs to be \ rewritten to use the new schema modification syntax. All staging environments affected.", "dba_admin", ), ( 4, "merge_request", "Performance optimization for dashboard queries", "Optimized the dashboard query performance by adding database indexes and implementing \ Redis caching for frequently accessed reports. Query execution time reduced from 3.2s \ to 180ms. Added connection pooling and prepared statement caching.", "senior_dev", ), ( 5, "discussion", "API rate limiting strategies for public endpoints", "Discussion about implementing API rate limiting on public-facing endpoints. \ Proposed approaches: token bucket with sliding window, fixed window counters, \ or leaky bucket algorithm. Rate limits should be configurable per API key tier. \ Need to handle burst traffic during peak hours without throttling legitimate users.", "architect", ), ( 6, "issue", "CSS styling issues on mobile frontend", "Multiple CSS styling problems on the mobile frontend. The navigation menu overlaps \ content on screens smaller than 768px. Button text truncates on compact viewports. \ Frontend responsive breakpoints need adjustment. The UI components library has \ conflicting CSS specificity with the theme system.", "frontend_dev", ), ( 7, "merge_request", "Revamp CI/CD pipeline with Docker caching", "Complete overhaul of the CI/CD pipeline. Added Docker layer caching to speed up \ builds. Deployment stages now run in parallel where possible. Added rollback \ support for failed deployments. Pipeline runtime reduced from 45min to 12min.", "devops_lead", ), ( 8, "issue", "Security vulnerability in form submission", "A cross-site scripting (XSS) vulnerability was found in the comment submission form. \ User input is not properly sanitized before rendering. The security scanner also flagged \ potential SQL injection in the search endpoint. Both vulnerabilities need immediate patching.", "security_team", ), ]; for (id, source_type, title, content, author) in &documents { conn.execute( "INSERT INTO documents (id, source_type, source_id, project_id, title, content_text, content_hash, url, author_username) VALUES (?1, ?2, ?1, 1, ?3, ?4, 'hash_' || ?1, 'https://gitlab.example.com/group/project/-/' || ?2 || 's/' || ?1, ?5)", rusqlite::params![id, source_type, title, content, author], ) .unwrap(); } conn.execute_batch( "INSERT INTO document_labels (document_id, label_name) VALUES (1, 'bug'); INSERT INTO document_labels (document_id, label_name) VALUES (1, 'authentication'); INSERT INTO document_labels (document_id, label_name) VALUES (3, 'bug'); INSERT INTO document_labels (document_id, label_name) VALUES (3, 'database'); INSERT INTO document_labels (document_id, label_name) VALUES (6, 'bug'); INSERT INTO document_labels (document_id, label_name) VALUES (6, 'frontend'); INSERT INTO document_labels (document_id, label_name) VALUES (8, 'security'); INSERT INTO document_labels (document_id, label_name) VALUES (8, 'critical');", ) .unwrap(); conn } fn build_search_filters(golden: &GoldenFilters) -> SearchFilters { let source_type = golden.source_type.as_deref().and_then(|s| match s { "issue" => Some(lore::documents::SourceType::Issue), "merge_request" => Some(lore::documents::SourceType::MergeRequest), "discussion" => Some(lore::documents::SourceType::Discussion), _ => None, }); SearchFilters { source_type, author: golden.author.clone(), labels: golden.labels.clone(), limit: 100, ..Default::default() } } #[test] fn golden_queries_all_pass() { let queries = load_golden_queries(); let conn = create_seeded_db(); let mut failures: Vec = Vec::new(); for (i, gq) in queries.iter().enumerate() { let mode = SearchMode::parse(&gq.mode).unwrap_or(SearchMode::Lexical); assert_eq!( mode, SearchMode::Lexical, "Golden query {} uses non-lexical mode '{}' which requires Ollama — not supported in CI", i, gq.mode ); let fts_results = search_fts(&conn, &gq.query, 50, FtsQueryMode::Safe).unwrap(); let doc_ids: Vec = fts_results.iter().map(|r| r.document_id).collect(); let filters = build_search_filters(&gq.filters); let filtered_ids = if filters.has_any_filter() { apply_filters(&conn, &doc_ids, &filters).unwrap() } else { doc_ids.clone() }; if filtered_ids.len() < gq.min_results { failures.push(format!( "FAIL [{}] \"{}\": expected >= {} results, got {} (description: {})", i, gq.query, gq.min_results, filtered_ids.len(), gq.description )); continue; } for expected_id in &gq.expected_doc_ids { let position = filtered_ids.iter().position(|id| id == expected_id); match position { Some(pos) if pos < gq.max_rank => {} Some(pos) => { failures.push(format!( "FAIL [{}] \"{}\": expected doc_id {} in top {}, found at rank {} (description: {})", i, gq.query, expected_id, gq.max_rank, pos + 1, gq.description )); } None => { failures.push(format!( "FAIL [{}] \"{}\": expected doc_id {} not found in results {:?} (description: {})", i, gq.query, expected_id, filtered_ids, gq.description )); } } } } if !failures.is_empty() { panic!( "Golden query failures ({}/{}):\n{}", failures.len(), queries.len(), failures.join("\n") ); } } #[test] fn golden_queries_fixture_is_valid() { let queries = load_golden_queries(); assert!( queries.len() >= 5, "Golden queries fixture should have at least 5 queries, got {}", queries.len() ); for (i, gq) in queries.iter().enumerate() { assert!(!gq.query.is_empty(), "Query {} has empty query string", i); assert!( !gq.expected_doc_ids.is_empty(), "Query {} has no expected doc IDs", i ); assert!(gq.min_results > 0, "Query {} has min_results=0", i); assert!(gq.max_rank > 0, "Query {} has max_rank=0", i); assert!( SearchMode::parse(&gq.mode).is_some(), "Query {} has invalid mode '{}'", i, gq.mode ); } }