feat(events): Wire resource event fetching into sync pipeline (bd-1ep)
Enqueue resource_events jobs for all issues/MRs after discussion sync, then drain the queue by fetching state/label/milestone events from GitLab API and storing them via transaction-based wrappers. Adds progress events, count tracking through orchestrator->ingest->sync result chain, and respects fetch_resource_events config flag. Includes clippy fixes across codebase from parallel agent work. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -102,7 +102,10 @@ fn knn_search_returns_nearest_neighbors() {
|
||||
let results = lore::search::search_vector(&conn, &query, 10).unwrap();
|
||||
|
||||
assert!(!results.is_empty(), "Should return at least one result");
|
||||
assert_eq!(results[0].document_id, 1, "Nearest neighbor should be doc 1");
|
||||
assert_eq!(
|
||||
results[0].document_id, 1,
|
||||
"Nearest neighbor should be doc 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -122,7 +125,12 @@ fn knn_search_respects_limit() {
|
||||
fn knn_search_deduplicates_chunks() {
|
||||
let (_tmp, conn) = create_test_db();
|
||||
|
||||
insert_document(&conn, 1, "Multi-chunk doc", "Very long content that was chunked.");
|
||||
insert_document(
|
||||
&conn,
|
||||
1,
|
||||
"Multi-chunk doc",
|
||||
"Very long content that was chunked.",
|
||||
);
|
||||
|
||||
// Same document, two chunks, both similar to query
|
||||
let mut v1 = vec![0.0f32; 768];
|
||||
@@ -137,7 +145,8 @@ fn knn_search_deduplicates_chunks() {
|
||||
let results = lore::search::search_vector(&conn, &axis_vector(0), 10).unwrap();
|
||||
|
||||
// Should deduplicate: same document_id appears at most once
|
||||
let unique_docs: std::collections::HashSet<i64> = results.iter().map(|r| r.document_id).collect();
|
||||
let unique_docs: std::collections::HashSet<i64> =
|
||||
results.iter().map(|r| r.document_id).collect();
|
||||
assert_eq!(
|
||||
unique_docs.len(),
|
||||
results.len(),
|
||||
@@ -154,22 +163,38 @@ fn orphan_trigger_deletes_embeddings_on_document_delete() {
|
||||
|
||||
// Verify embedding exists
|
||||
let count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM embeddings WHERE rowid = 1000", [], |r| r.get(0))
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM embeddings WHERE rowid = 1000",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(count, 1, "Embedding should exist before delete");
|
||||
|
||||
// Delete the document
|
||||
conn.execute("DELETE FROM documents WHERE id = 1", []).unwrap();
|
||||
conn.execute("DELETE FROM documents WHERE id = 1", [])
|
||||
.unwrap();
|
||||
|
||||
// Verify embedding was cascade-deleted via trigger
|
||||
let count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM embeddings WHERE rowid = 1000", [], |r| r.get(0))
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM embeddings WHERE rowid = 1000",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(count, 0, "Trigger should delete embeddings when document is deleted");
|
||||
assert_eq!(
|
||||
count, 0,
|
||||
"Trigger should delete embeddings when document is deleted"
|
||||
);
|
||||
|
||||
// Verify metadata was cascade-deleted via FK
|
||||
let meta_count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM embedding_metadata WHERE document_id = 1", [], |r| r.get(0))
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM embedding_metadata WHERE document_id = 1",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(meta_count, 0, "Metadata should be cascade-deleted");
|
||||
}
|
||||
@@ -206,7 +231,8 @@ fn overflow_doc_with_error_sentinel_not_re_detected_as_pending() {
|
||||
.unwrap();
|
||||
|
||||
// Now find_pending_documents should NOT return this document
|
||||
let pending = lore::embedding::find_pending_documents(&conn, 100, 0, "nomic-embed-text").unwrap();
|
||||
let pending =
|
||||
lore::embedding::find_pending_documents(&conn, 100, 0, "nomic-embed-text").unwrap();
|
||||
assert!(
|
||||
pending.is_empty(),
|
||||
"Document with overflow error sentinel should not be re-detected as pending, got {} pending",
|
||||
@@ -215,7 +241,10 @@ fn overflow_doc_with_error_sentinel_not_re_detected_as_pending() {
|
||||
|
||||
// count_pending_documents should also return 0
|
||||
let count = lore::embedding::count_pending_documents(&conn, "nomic-embed-text").unwrap();
|
||||
assert_eq!(count, 0, "Count should be 0 for document with overflow sentinel");
|
||||
assert_eq!(
|
||||
count, 0,
|
||||
"Count should be 0 for document with overflow sentinel"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -226,14 +255,24 @@ fn count_and_find_pending_agree() {
|
||||
|
||||
// Case 1: No documents at all
|
||||
let count = lore::embedding::count_pending_documents(&conn, "nomic-embed-text").unwrap();
|
||||
let found = lore::embedding::find_pending_documents(&conn, 1000, 0, "nomic-embed-text").unwrap();
|
||||
assert_eq!(count as usize, found.len(), "Empty DB: count and find should agree");
|
||||
let found =
|
||||
lore::embedding::find_pending_documents(&conn, 1000, 0, "nomic-embed-text").unwrap();
|
||||
assert_eq!(
|
||||
count as usize,
|
||||
found.len(),
|
||||
"Empty DB: count and find should agree"
|
||||
);
|
||||
|
||||
// Case 2: New document (no metadata)
|
||||
insert_document(&conn, 1, "New doc", "Content");
|
||||
let count = lore::embedding::count_pending_documents(&conn, "nomic-embed-text").unwrap();
|
||||
let found = lore::embedding::find_pending_documents(&conn, 1000, 0, "nomic-embed-text").unwrap();
|
||||
assert_eq!(count as usize, found.len(), "New doc: count and find should agree");
|
||||
let found =
|
||||
lore::embedding::find_pending_documents(&conn, 1000, 0, "nomic-embed-text").unwrap();
|
||||
assert_eq!(
|
||||
count as usize,
|
||||
found.len(),
|
||||
"New doc: count and find should agree"
|
||||
);
|
||||
assert_eq!(count, 1);
|
||||
|
||||
// Case 3: Document with matching metadata (not pending)
|
||||
@@ -247,8 +286,13 @@ fn count_and_find_pending_agree() {
|
||||
)
|
||||
.unwrap();
|
||||
let count = lore::embedding::count_pending_documents(&conn, "nomic-embed-text").unwrap();
|
||||
let found = lore::embedding::find_pending_documents(&conn, 1000, 0, "nomic-embed-text").unwrap();
|
||||
assert_eq!(count as usize, found.len(), "Complete doc: count and find should agree");
|
||||
let found =
|
||||
lore::embedding::find_pending_documents(&conn, 1000, 0, "nomic-embed-text").unwrap();
|
||||
assert_eq!(
|
||||
count as usize,
|
||||
found.len(),
|
||||
"Complete doc: count and find should agree"
|
||||
);
|
||||
assert_eq!(count, 0);
|
||||
|
||||
// Case 4: Config drift (chunk_max_bytes mismatch)
|
||||
@@ -258,8 +302,13 @@ fn count_and_find_pending_agree() {
|
||||
)
|
||||
.unwrap();
|
||||
let count = lore::embedding::count_pending_documents(&conn, "nomic-embed-text").unwrap();
|
||||
let found = lore::embedding::find_pending_documents(&conn, 1000, 0, "nomic-embed-text").unwrap();
|
||||
assert_eq!(count as usize, found.len(), "Config drift: count and find should agree");
|
||||
let found =
|
||||
lore::embedding::find_pending_documents(&conn, 1000, 0, "nomic-embed-text").unwrap();
|
||||
assert_eq!(
|
||||
count as usize,
|
||||
found.len(),
|
||||
"Config drift: count and find should agree"
|
||||
);
|
||||
assert_eq!(count, 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -51,26 +51,72 @@ fn insert_document(conn: &Connection, id: i64, source_type: &str, title: &str, c
|
||||
fn fts_basic_search() {
|
||||
let conn = create_test_db();
|
||||
|
||||
insert_document(&conn, 1, "issue", "Authentication bug", "Users cannot login when using OAuth tokens. The JWT refresh fails silently.");
|
||||
insert_document(&conn, 2, "merge_request", "Add user profile page", "This MR adds a new user profile page with avatar upload support.");
|
||||
insert_document(&conn, 3, "issue", "Database migration failing", "The migration script crashes on PostgreSQL 14 due to deprecated syntax.");
|
||||
insert_document(
|
||||
&conn,
|
||||
1,
|
||||
"issue",
|
||||
"Authentication bug",
|
||||
"Users cannot login when using OAuth tokens. The JWT refresh fails silently.",
|
||||
);
|
||||
insert_document(
|
||||
&conn,
|
||||
2,
|
||||
"merge_request",
|
||||
"Add user profile page",
|
||||
"This MR adds a new user profile page with avatar upload support.",
|
||||
);
|
||||
insert_document(
|
||||
&conn,
|
||||
3,
|
||||
"issue",
|
||||
"Database migration failing",
|
||||
"The migration script crashes on PostgreSQL 14 due to deprecated syntax.",
|
||||
);
|
||||
|
||||
let results = lore::search::search_fts(&conn, "authentication login", 10, lore::search::FtsQueryMode::Safe).unwrap();
|
||||
let results = lore::search::search_fts(
|
||||
&conn,
|
||||
"authentication login",
|
||||
10,
|
||||
lore::search::FtsQueryMode::Safe,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(!results.is_empty(), "Expected at least one result for 'authentication login'");
|
||||
assert_eq!(results[0].document_id, 1, "Authentication issue should be top result");
|
||||
assert!(
|
||||
!results.is_empty(),
|
||||
"Expected at least one result for 'authentication login'"
|
||||
);
|
||||
assert_eq!(
|
||||
results[0].document_id, 1,
|
||||
"Authentication issue should be top result"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fts_stemming_matches() {
|
||||
let conn = create_test_db();
|
||||
|
||||
insert_document(&conn, 1, "issue", "Running tests", "The test runner is executing integration tests.");
|
||||
insert_document(&conn, 2, "issue", "Deployment config", "Deployment configuration for production servers.");
|
||||
insert_document(
|
||||
&conn,
|
||||
1,
|
||||
"issue",
|
||||
"Running tests",
|
||||
"The test runner is executing integration tests.",
|
||||
);
|
||||
insert_document(
|
||||
&conn,
|
||||
2,
|
||||
"issue",
|
||||
"Deployment config",
|
||||
"Deployment configuration for production servers.",
|
||||
);
|
||||
|
||||
// "running" should match "runner" and "executing" via porter stemmer
|
||||
let results = lore::search::search_fts(&conn, "running", 10, lore::search::FtsQueryMode::Safe).unwrap();
|
||||
assert!(!results.is_empty(), "Stemming should match 'running' to 'runner'");
|
||||
let results =
|
||||
lore::search::search_fts(&conn, "running", 10, lore::search::FtsQueryMode::Safe).unwrap();
|
||||
assert!(
|
||||
!results.is_empty(),
|
||||
"Stemming should match 'running' to 'runner'"
|
||||
);
|
||||
assert_eq!(results[0].document_id, 1);
|
||||
}
|
||||
|
||||
@@ -78,20 +124,43 @@ fn fts_stemming_matches() {
|
||||
fn fts_empty_results() {
|
||||
let conn = create_test_db();
|
||||
|
||||
insert_document(&conn, 1, "issue", "Bug fix", "Fixed a null pointer dereference in the parser.");
|
||||
insert_document(
|
||||
&conn,
|
||||
1,
|
||||
"issue",
|
||||
"Bug fix",
|
||||
"Fixed a null pointer dereference in the parser.",
|
||||
);
|
||||
|
||||
let results = lore::search::search_fts(&conn, "kubernetes deployment helm", 10, lore::search::FtsQueryMode::Safe).unwrap();
|
||||
assert!(results.is_empty(), "No documents should match unrelated query");
|
||||
let results = lore::search::search_fts(
|
||||
&conn,
|
||||
"kubernetes deployment helm",
|
||||
10,
|
||||
lore::search::FtsQueryMode::Safe,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
results.is_empty(),
|
||||
"No documents should match unrelated query"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fts_special_characters_handled() {
|
||||
let conn = create_test_db();
|
||||
|
||||
insert_document(&conn, 1, "issue", "C++ compiler", "The C++ compiler segfaults on template metaprogramming.");
|
||||
insert_document(
|
||||
&conn,
|
||||
1,
|
||||
"issue",
|
||||
"C++ compiler",
|
||||
"The C++ compiler segfaults on template metaprogramming.",
|
||||
);
|
||||
|
||||
// Special characters should not crash the search
|
||||
let results = lore::search::search_fts(&conn, "C++ compiler", 10, lore::search::FtsQueryMode::Safe).unwrap();
|
||||
let results =
|
||||
lore::search::search_fts(&conn, "C++ compiler", 10, lore::search::FtsQueryMode::Safe)
|
||||
.unwrap();
|
||||
// Safe mode sanitizes the query — it should still return results or at least not crash
|
||||
assert!(results.len() <= 1);
|
||||
}
|
||||
@@ -101,17 +170,44 @@ fn fts_result_ordering_by_relevance() {
|
||||
let conn = create_test_db();
|
||||
|
||||
// Doc 1: "authentication" in title and content
|
||||
insert_document(&conn, 1, "issue", "Authentication system redesign", "The authentication system needs a complete redesign. Authentication flows are broken.");
|
||||
insert_document(
|
||||
&conn,
|
||||
1,
|
||||
"issue",
|
||||
"Authentication system redesign",
|
||||
"The authentication system needs a complete redesign. Authentication flows are broken.",
|
||||
);
|
||||
// Doc 2: "authentication" only in content, once
|
||||
insert_document(&conn, 2, "issue", "Login page update", "Updated the login page with better authentication error messages.");
|
||||
insert_document(
|
||||
&conn,
|
||||
2,
|
||||
"issue",
|
||||
"Login page update",
|
||||
"Updated the login page with better authentication error messages.",
|
||||
);
|
||||
// Doc 3: unrelated
|
||||
insert_document(&conn, 3, "issue", "Database optimization", "Optimize database queries for faster response times.");
|
||||
insert_document(
|
||||
&conn,
|
||||
3,
|
||||
"issue",
|
||||
"Database optimization",
|
||||
"Optimize database queries for faster response times.",
|
||||
);
|
||||
|
||||
let results = lore::search::search_fts(&conn, "authentication", 10, lore::search::FtsQueryMode::Safe).unwrap();
|
||||
let results = lore::search::search_fts(
|
||||
&conn,
|
||||
"authentication",
|
||||
10,
|
||||
lore::search::FtsQueryMode::Safe,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(results.len() >= 2, "Should match at least 2 documents");
|
||||
// Doc 1 should rank higher (more occurrences of the term)
|
||||
assert_eq!(results[0].document_id, 1, "Document with more term occurrences should rank first");
|
||||
assert_eq!(
|
||||
results[0].document_id, 1,
|
||||
"Document with more term occurrences should rank first"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -128,7 +224,8 @@ fn fts_respects_limit() {
|
||||
);
|
||||
}
|
||||
|
||||
let results = lore::search::search_fts(&conn, "bug login", 5, lore::search::FtsQueryMode::Safe).unwrap();
|
||||
let results =
|
||||
lore::search::search_fts(&conn, "bug login", 5, lore::search::FtsQueryMode::Safe).unwrap();
|
||||
assert!(results.len() <= 5, "Results should be capped at limit");
|
||||
}
|
||||
|
||||
@@ -136,24 +233,45 @@ fn fts_respects_limit() {
|
||||
fn fts_snippet_generated() {
|
||||
let conn = create_test_db();
|
||||
|
||||
insert_document(&conn, 1, "issue", "Performance issue", "The application performance degrades significantly when more than 100 users are connected simultaneously. Memory usage spikes to 4GB.");
|
||||
insert_document(
|
||||
&conn,
|
||||
1,
|
||||
"issue",
|
||||
"Performance issue",
|
||||
"The application performance degrades significantly when more than 100 users are connected simultaneously. Memory usage spikes to 4GB.",
|
||||
);
|
||||
|
||||
let results = lore::search::search_fts(&conn, "performance", 10, lore::search::FtsQueryMode::Safe).unwrap();
|
||||
let results =
|
||||
lore::search::search_fts(&conn, "performance", 10, lore::search::FtsQueryMode::Safe)
|
||||
.unwrap();
|
||||
|
||||
assert!(!results.is_empty());
|
||||
// Snippet should contain some text (may have FTS5 highlight markers)
|
||||
assert!(!results[0].snippet.is_empty(), "Snippet should be generated");
|
||||
assert!(
|
||||
!results[0].snippet.is_empty(),
|
||||
"Snippet should be generated"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fts_triggers_sync_on_insert() {
|
||||
let conn = create_test_db();
|
||||
|
||||
insert_document(&conn, 1, "issue", "Test document", "This is test content for FTS trigger verification.");
|
||||
insert_document(
|
||||
&conn,
|
||||
1,
|
||||
"issue",
|
||||
"Test document",
|
||||
"This is test content for FTS trigger verification.",
|
||||
);
|
||||
|
||||
// Verify FTS table has an entry via direct query
|
||||
let fts_count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM documents_fts WHERE documents_fts MATCH 'test'", [], |r| r.get(0))
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM documents_fts WHERE documents_fts MATCH 'test'",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(fts_count, 1, "FTS trigger should auto-index on INSERT");
|
||||
@@ -163,20 +281,35 @@ fn fts_triggers_sync_on_insert() {
|
||||
fn fts_triggers_sync_on_delete() {
|
||||
let conn = create_test_db();
|
||||
|
||||
insert_document(&conn, 1, "issue", "Deletable document", "This content will be deleted from the index.");
|
||||
insert_document(
|
||||
&conn,
|
||||
1,
|
||||
"issue",
|
||||
"Deletable document",
|
||||
"This content will be deleted from the index.",
|
||||
);
|
||||
|
||||
// Verify it's indexed
|
||||
let before: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM documents_fts WHERE documents_fts MATCH 'deletable'", [], |r| r.get(0))
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM documents_fts WHERE documents_fts MATCH 'deletable'",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(before, 1);
|
||||
|
||||
// Delete the document
|
||||
conn.execute("DELETE FROM documents WHERE id = 1", []).unwrap();
|
||||
conn.execute("DELETE FROM documents WHERE id = 1", [])
|
||||
.unwrap();
|
||||
|
||||
// Verify it's removed from FTS
|
||||
let after: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM documents_fts WHERE documents_fts MATCH 'deletable'", [], |r| r.get(0))
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM documents_fts WHERE documents_fts MATCH 'deletable'",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(after, 0, "FTS trigger should remove entry on DELETE");
|
||||
}
|
||||
@@ -193,6 +326,8 @@ fn fts_null_title_handled() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let results = lore::search::search_fts(&conn, "rate limiting", 10, lore::search::FtsQueryMode::Safe).unwrap();
|
||||
let results =
|
||||
lore::search::search_fts(&conn, "rate limiting", 10, lore::search::FtsQueryMode::Safe)
|
||||
.unwrap();
|
||||
assert!(!results.is_empty(), "Should find documents with NULL title");
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use rusqlite::Connection;
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use lore::search::{self, FtsQueryMode, SearchFilters, SearchMode, search_fts, apply_filters};
|
||||
use lore::search::{FtsQueryMode, SearchFilters, SearchMode, apply_filters, search_fts};
|
||||
|
||||
/// A golden query test case.
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -35,8 +35,7 @@ struct GoldenFilters {
|
||||
}
|
||||
|
||||
fn load_golden_queries() -> Vec<GoldenQuery> {
|
||||
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures/golden_queries.json");
|
||||
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)
|
||||
@@ -77,63 +76,88 @@ fn create_seeded_db() -> Connection {
|
||||
// Seed deterministic documents
|
||||
let documents = vec![
|
||||
// id=1: Auth issue (matches: authentication, login, OAuth, JWT, token, refresh)
|
||||
(1, "issue", "Authentication and login broken with OAuth",
|
||||
"Users cannot login when using OAuth tokens. The JWT token refresh fails silently, \
|
||||
(
|
||||
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"),
|
||||
|
||||
"testuser",
|
||||
),
|
||||
// id=2: User profile MR (matches: user, profile, avatar, upload)
|
||||
(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, \
|
||||
(
|
||||
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"),
|
||||
|
||||
"developer1",
|
||||
),
|
||||
// id=3: Database migration issue (matches: database, migration, PostgreSQL, schema)
|
||||
(3, "issue", "Database migration failing on PostgreSQL 14",
|
||||
"The database migration script crashes on PostgreSQL 14 due to deprecated syntax. \
|
||||
(
|
||||
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"),
|
||||
|
||||
"dba_admin",
|
||||
),
|
||||
// id=4: Performance MR (matches: performance, optimization, caching, query)
|
||||
(4, "merge_request", "Performance optimization for dashboard queries",
|
||||
"Optimized the dashboard query performance by adding database indexes and implementing \
|
||||
(
|
||||
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"),
|
||||
|
||||
"senior_dev",
|
||||
),
|
||||
// id=5: API rate limiting discussion (matches: API, rate, limiting, throttle)
|
||||
(5, "discussion", "API rate limiting strategies for public endpoints",
|
||||
"Discussion about implementing API rate limiting on public-facing endpoints. \
|
||||
(
|
||||
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"),
|
||||
|
||||
"architect",
|
||||
),
|
||||
// id=6: UI/CSS issue (matches: CSS, styling, frontend, responsive, UI)
|
||||
(6, "issue", "CSS styling issues on mobile frontend",
|
||||
"Multiple CSS styling problems on the mobile frontend. The navigation menu overlaps \
|
||||
(
|
||||
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"),
|
||||
|
||||
"frontend_dev",
|
||||
),
|
||||
// id=7: CI/CD MR (matches: CI, CD, pipeline, deployment, Docker)
|
||||
(7, "merge_request", "Revamp CI/CD pipeline with Docker caching",
|
||||
"Complete overhaul of the CI/CD pipeline. Added Docker layer caching to speed up \
|
||||
(
|
||||
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"),
|
||||
|
||||
"devops_lead",
|
||||
),
|
||||
// id=8: Security issue (matches: security, vulnerability, XSS, injection)
|
||||
(8, "issue", "Security vulnerability in form submission",
|
||||
"A cross-site scripting (XSS) vulnerability was found in the comment submission form. \
|
||||
(
|
||||
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"),
|
||||
"security_team",
|
||||
),
|
||||
];
|
||||
|
||||
for (id, source_type, title, content, author) in &documents {
|
||||
@@ -213,7 +237,11 @@ fn golden_queries_all_pass() {
|
||||
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
|
||||
i,
|
||||
gq.query,
|
||||
gq.min_results,
|
||||
filtered_ids.len(),
|
||||
gq.description
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -51,13 +51,24 @@ fn insert_document(conn: &Connection, id: i64, source_type: &str, title: &str, c
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn lexical_mode_uses_fts_only() {
|
||||
let (_tmp, conn) = create_test_db();
|
||||
|
||||
insert_document(&conn, 1, "issue", "Authentication bug", "OAuth token refresh fails silently.");
|
||||
insert_document(&conn, 2, "issue", "Database migration", "Migration script crashes on PostgreSQL.");
|
||||
insert_document(
|
||||
&conn,
|
||||
1,
|
||||
"issue",
|
||||
"Authentication bug",
|
||||
"OAuth token refresh fails silently.",
|
||||
);
|
||||
insert_document(
|
||||
&conn,
|
||||
2,
|
||||
"issue",
|
||||
"Database migration",
|
||||
"Migration script crashes on PostgreSQL.",
|
||||
);
|
||||
|
||||
let filters = SearchFilters {
|
||||
limit: 10,
|
||||
@@ -121,14 +132,23 @@ fn lexical_mode_no_embeddings_required() {
|
||||
.unwrap();
|
||||
|
||||
let results = search_fts(&conn, "testing", 10, FtsQueryMode::Safe).unwrap();
|
||||
assert!(!results.is_empty(), "FTS should work without embeddings tables");
|
||||
assert!(
|
||||
!results.is_empty(),
|
||||
"FTS should work without embeddings tables"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hybrid_mode_degrades_to_fts_without_client() {
|
||||
let (_tmp, conn) = create_test_db();
|
||||
|
||||
insert_document(&conn, 1, "issue", "Performance issue", "Application is slow under load.");
|
||||
insert_document(
|
||||
&conn,
|
||||
1,
|
||||
"issue",
|
||||
"Performance issue",
|
||||
"Application is slow under load.",
|
||||
);
|
||||
|
||||
let filters = SearchFilters {
|
||||
limit: 10,
|
||||
@@ -150,7 +170,11 @@ fn hybrid_mode_degrades_to_fts_without_client() {
|
||||
assert!(!results.is_empty(), "Should fall back to FTS results");
|
||||
// Should warn about missing Ollama client
|
||||
assert!(
|
||||
warnings.iter().any(|w| w.to_lowercase().contains("vector") || w.to_lowercase().contains("ollama") || w.to_lowercase().contains("client") || w.to_lowercase().contains("fallback") || w.to_lowercase().contains("fts")),
|
||||
warnings.iter().any(|w| w.to_lowercase().contains("vector")
|
||||
|| w.to_lowercase().contains("ollama")
|
||||
|| w.to_lowercase().contains("client")
|
||||
|| w.to_lowercase().contains("fallback")
|
||||
|| w.to_lowercase().contains("fts")),
|
||||
"Should produce a degradation warning, got: {:?}",
|
||||
warnings
|
||||
);
|
||||
@@ -177,8 +201,20 @@ fn rrf_ranking_combines_signals() {
|
||||
fn filters_by_source_type() {
|
||||
let (_tmp, conn) = create_test_db();
|
||||
|
||||
insert_document(&conn, 1, "issue", "Bug report", "Authentication bug in login flow.");
|
||||
insert_document(&conn, 2, "merge_request", "Fix auth", "Fixed authentication issue.");
|
||||
insert_document(
|
||||
&conn,
|
||||
1,
|
||||
"issue",
|
||||
"Bug report",
|
||||
"Authentication bug in login flow.",
|
||||
);
|
||||
insert_document(
|
||||
&conn,
|
||||
2,
|
||||
"merge_request",
|
||||
"Fix auth",
|
||||
"Fixed authentication issue.",
|
||||
);
|
||||
|
||||
let filters = SearchFilters {
|
||||
source_type: Some(lore::documents::SourceType::Issue),
|
||||
@@ -189,7 +225,11 @@ fn filters_by_source_type() {
|
||||
let all_ids = vec![1, 2];
|
||||
let filtered = lore::search::apply_filters(&conn, &all_ids, &filters).unwrap();
|
||||
|
||||
assert_eq!(filtered.len(), 1, "Filter should remove non-issue documents");
|
||||
assert_eq!(
|
||||
filtered.len(),
|
||||
1,
|
||||
"Filter should remove non-issue documents"
|
||||
);
|
||||
assert_eq!(filtered[0], 1, "Only issue document should remain");
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ fn apply_migrations(conn: &Connection, through_version: i32) {
|
||||
|
||||
let sql = std::fs::read_to_string(entries[0].path()).unwrap();
|
||||
conn.execute_batch(&sql)
|
||||
.expect(&format!("Migration {} failed", version));
|
||||
.unwrap_or_else(|e| panic!("Migration {} failed: {}", version, e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user