Move inline #[cfg(test)] mod tests { ... } blocks from 22 source files
into dedicated _tests.rs companion files, wired via:
#[cfg(test)]
#[path = "module_tests.rs"]
mod tests;
This keeps implementation-focused source files leaner and more scannable
while preserving full access to private items through `use super::*;`.
Modules extracted:
core: db, note_parser, payloads, project, references, sync_run,
timeline_collect, timeline_expand, timeline_seed
cli: list (55 tests), who (75 tests)
documents: extractor (43 tests), regenerator
embedding: change_detector, chunking
gitlab: graphql (wiremock async tests), transformers/issue
ingestion: dirty_tracker, discussions, issues, mr_diffs
Also adds conflicts_with("explain_score") to the --detail flag in the
who command to prevent mutually exclusive flags from being combined.
All 629 unit tests pass. No behavior changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
306 lines
9.0 KiB
Rust
306 lines
9.0 KiB
Rust
use super::*;
|
|
use crate::core::db::{create_connection, run_migrations};
|
|
use std::path::Path;
|
|
|
|
fn setup_test_db() -> Connection {
|
|
let conn = create_connection(Path::new(":memory:")).unwrap();
|
|
run_migrations(&conn).unwrap();
|
|
conn
|
|
}
|
|
|
|
fn insert_project(conn: &Connection) -> i64 {
|
|
conn.execute(
|
|
"INSERT INTO projects (gitlab_project_id, path_with_namespace, web_url) VALUES (1, 'group/project', 'https://gitlab.com/group/project')",
|
|
[],
|
|
)
|
|
.unwrap();
|
|
conn.last_insert_rowid()
|
|
}
|
|
|
|
fn insert_issue(conn: &Connection, project_id: i64, iid: i64) -> i64 {
|
|
conn.execute(
|
|
"INSERT INTO issues (gitlab_id, project_id, iid, title, state, author_username, created_at, updated_at, last_seen_at) VALUES (?1, ?2, ?3, 'Test', 'opened', 'alice', 1000, 2000, 3000)",
|
|
rusqlite::params![iid * 100, project_id, iid],
|
|
)
|
|
.unwrap();
|
|
conn.last_insert_rowid()
|
|
}
|
|
|
|
fn insert_mr(conn: &Connection, project_id: i64, iid: i64) -> i64 {
|
|
conn.execute(
|
|
"INSERT INTO merge_requests (gitlab_id, project_id, iid, title, state, author_username, created_at, updated_at, last_seen_at) VALUES (?1, ?2, ?3, 'Test MR', 'opened', 'bob', 1000, 2000, 3000)",
|
|
rusqlite::params![iid * 100, project_id, iid],
|
|
)
|
|
.unwrap();
|
|
conn.last_insert_rowid()
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn insert_ref(
|
|
conn: &Connection,
|
|
project_id: i64,
|
|
source_type: &str,
|
|
source_id: i64,
|
|
target_type: &str,
|
|
target_id: Option<i64>,
|
|
ref_type: &str,
|
|
source_method: &str,
|
|
) {
|
|
conn.execute(
|
|
"INSERT INTO entity_references (project_id, source_entity_type, source_entity_id, target_entity_type, target_entity_id, reference_type, source_method, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, 1000)",
|
|
rusqlite::params![project_id, source_type, source_id, target_type, target_id, ref_type, source_method],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
fn make_entity_ref(entity_type: &str, entity_id: i64, iid: i64) -> EntityRef {
|
|
EntityRef {
|
|
entity_type: entity_type.to_owned(),
|
|
entity_id,
|
|
entity_iid: iid,
|
|
project_path: "group/project".to_owned(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_expand_depth_zero() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
let seeds = vec![make_entity_ref("issue", issue_id, 1)];
|
|
|
|
let result = expand_timeline(&conn, &seeds, 0, false, 100).unwrap();
|
|
assert!(result.expanded_entities.is_empty());
|
|
assert!(result.unresolved_references.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_expand_finds_linked_entity() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
let mr_id = insert_mr(&conn, project_id, 10);
|
|
|
|
// MR closes issue
|
|
insert_ref(
|
|
&conn,
|
|
project_id,
|
|
"merge_request",
|
|
mr_id,
|
|
"issue",
|
|
Some(issue_id),
|
|
"closes",
|
|
"api",
|
|
);
|
|
|
|
let seeds = vec![make_entity_ref("issue", issue_id, 1)];
|
|
let result = expand_timeline(&conn, &seeds, 1, false, 100).unwrap();
|
|
|
|
assert_eq!(result.expanded_entities.len(), 1);
|
|
assert_eq!(
|
|
result.expanded_entities[0].entity_ref.entity_type,
|
|
"merge_request"
|
|
);
|
|
assert_eq!(result.expanded_entities[0].entity_ref.entity_iid, 10);
|
|
assert_eq!(result.expanded_entities[0].depth, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_expand_bidirectional() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
let mr_id = insert_mr(&conn, project_id, 10);
|
|
|
|
// MR closes issue (MR is source, issue is target)
|
|
insert_ref(
|
|
&conn,
|
|
project_id,
|
|
"merge_request",
|
|
mr_id,
|
|
"issue",
|
|
Some(issue_id),
|
|
"closes",
|
|
"api",
|
|
);
|
|
|
|
// Starting from MR should find the issue (outgoing)
|
|
let seeds = vec![make_entity_ref("merge_request", mr_id, 10)];
|
|
let result = expand_timeline(&conn, &seeds, 1, false, 100).unwrap();
|
|
|
|
assert_eq!(result.expanded_entities.len(), 1);
|
|
assert_eq!(result.expanded_entities[0].entity_ref.entity_type, "issue");
|
|
}
|
|
|
|
#[test]
|
|
fn test_expand_respects_max_entities() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
|
|
// Create 10 MRs that all close this issue
|
|
for i in 2..=11 {
|
|
let mr_id = insert_mr(&conn, project_id, i);
|
|
insert_ref(
|
|
&conn,
|
|
project_id,
|
|
"merge_request",
|
|
mr_id,
|
|
"issue",
|
|
Some(issue_id),
|
|
"closes",
|
|
"api",
|
|
);
|
|
}
|
|
|
|
let seeds = vec![make_entity_ref("issue", issue_id, 1)];
|
|
let result = expand_timeline(&conn, &seeds, 1, false, 3).unwrap();
|
|
|
|
assert!(result.expanded_entities.len() <= 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_expand_skips_mentions_by_default() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
let mr_id = insert_mr(&conn, project_id, 10);
|
|
|
|
// MR mentions issue (should be skipped by default)
|
|
insert_ref(
|
|
&conn,
|
|
project_id,
|
|
"merge_request",
|
|
mr_id,
|
|
"issue",
|
|
Some(issue_id),
|
|
"mentioned",
|
|
"note_parse",
|
|
);
|
|
|
|
let seeds = vec![make_entity_ref("issue", issue_id, 1)];
|
|
let result = expand_timeline(&conn, &seeds, 1, false, 100).unwrap();
|
|
assert!(result.expanded_entities.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_expand_includes_mentions_when_flagged() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
let mr_id = insert_mr(&conn, project_id, 10);
|
|
|
|
// MR mentions issue
|
|
insert_ref(
|
|
&conn,
|
|
project_id,
|
|
"merge_request",
|
|
mr_id,
|
|
"issue",
|
|
Some(issue_id),
|
|
"mentioned",
|
|
"note_parse",
|
|
);
|
|
|
|
let seeds = vec![make_entity_ref("issue", issue_id, 1)];
|
|
let result = expand_timeline(&conn, &seeds, 1, true, 100).unwrap();
|
|
assert_eq!(result.expanded_entities.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_expand_collects_unresolved() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
|
|
// Unresolved cross-project reference
|
|
conn.execute(
|
|
"INSERT INTO entity_references (project_id, source_entity_type, source_entity_id, target_entity_type, target_entity_id, target_project_path, target_entity_iid, reference_type, source_method, created_at) VALUES (?1, 'issue', ?2, 'issue', NULL, 'other/repo', 42, 'closes', 'description_parse', 1000)",
|
|
rusqlite::params![project_id, issue_id],
|
|
)
|
|
.unwrap();
|
|
|
|
let seeds = vec![make_entity_ref("issue", issue_id, 1)];
|
|
let result = expand_timeline(&conn, &seeds, 1, false, 100).unwrap();
|
|
|
|
assert!(result.expanded_entities.is_empty());
|
|
assert_eq!(result.unresolved_references.len(), 1);
|
|
assert_eq!(
|
|
result.unresolved_references[0].target_project,
|
|
Some("other/repo".to_owned())
|
|
);
|
|
assert_eq!(result.unresolved_references[0].target_iid, Some(42));
|
|
}
|
|
|
|
#[test]
|
|
fn test_expand_tracks_provenance() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
let mr_id = insert_mr(&conn, project_id, 10);
|
|
|
|
insert_ref(
|
|
&conn,
|
|
project_id,
|
|
"merge_request",
|
|
mr_id,
|
|
"issue",
|
|
Some(issue_id),
|
|
"closes",
|
|
"api",
|
|
);
|
|
|
|
let seeds = vec![make_entity_ref("issue", issue_id, 1)];
|
|
let result = expand_timeline(&conn, &seeds, 1, false, 100).unwrap();
|
|
|
|
assert_eq!(result.expanded_entities.len(), 1);
|
|
let expanded = &result.expanded_entities[0];
|
|
assert_eq!(expanded.via_reference_type, "closes");
|
|
assert_eq!(expanded.via_source_method, "api");
|
|
assert_eq!(expanded.via_from.entity_type, "issue");
|
|
assert_eq!(expanded.via_from.entity_id, issue_id);
|
|
}
|
|
|
|
#[test]
|
|
fn test_expand_no_duplicates() {
|
|
let conn = setup_test_db();
|
|
let project_id = insert_project(&conn);
|
|
let issue_id = insert_issue(&conn, project_id, 1);
|
|
let mr_id = insert_mr(&conn, project_id, 10);
|
|
|
|
// Two references from MR to same issue (different methods)
|
|
insert_ref(
|
|
&conn,
|
|
project_id,
|
|
"merge_request",
|
|
mr_id,
|
|
"issue",
|
|
Some(issue_id),
|
|
"closes",
|
|
"api",
|
|
);
|
|
insert_ref(
|
|
&conn,
|
|
project_id,
|
|
"merge_request",
|
|
mr_id,
|
|
"issue",
|
|
Some(issue_id),
|
|
"related",
|
|
"note_parse",
|
|
);
|
|
|
|
let seeds = vec![make_entity_ref("merge_request", mr_id, 10)];
|
|
let result = expand_timeline(&conn, &seeds, 1, false, 100).unwrap();
|
|
|
|
// Should only appear once (first-come wins)
|
|
assert_eq!(result.expanded_entities.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_expand_empty_seeds() {
|
|
let conn = setup_test_db();
|
|
let result = expand_timeline(&conn, &[], 1, false, 100).unwrap();
|
|
assert!(result.expanded_entities.is_empty());
|
|
}
|