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>
1304 lines
34 KiB
Rust
1304 lines
34 KiB
Rust
use super::*;
|
|
|
|
#[test]
|
|
fn test_source_type_parse_aliases() {
|
|
assert_eq!(SourceType::parse("issue"), Some(SourceType::Issue));
|
|
assert_eq!(SourceType::parse("issues"), Some(SourceType::Issue));
|
|
assert_eq!(SourceType::parse("mr"), Some(SourceType::MergeRequest));
|
|
assert_eq!(SourceType::parse("mrs"), Some(SourceType::MergeRequest));
|
|
assert_eq!(
|
|
SourceType::parse("merge_request"),
|
|
Some(SourceType::MergeRequest)
|
|
);
|
|
assert_eq!(
|
|
SourceType::parse("merge_requests"),
|
|
Some(SourceType::MergeRequest)
|
|
);
|
|
assert_eq!(
|
|
SourceType::parse("discussion"),
|
|
Some(SourceType::Discussion)
|
|
);
|
|
assert_eq!(
|
|
SourceType::parse("discussions"),
|
|
Some(SourceType::Discussion)
|
|
);
|
|
assert_eq!(SourceType::parse("invalid"), None);
|
|
assert_eq!(SourceType::parse("ISSUE"), Some(SourceType::Issue));
|
|
}
|
|
|
|
#[test]
|
|
fn test_source_type_parse_note() {
|
|
assert_eq!(SourceType::parse("note"), Some(SourceType::Note));
|
|
}
|
|
|
|
#[test]
|
|
fn test_source_type_note_as_str() {
|
|
assert_eq!(SourceType::Note.as_str(), "note");
|
|
}
|
|
|
|
#[test]
|
|
fn test_source_type_note_display() {
|
|
assert_eq!(format!("{}", SourceType::Note), "note");
|
|
}
|
|
|
|
#[test]
|
|
fn test_source_type_parse_notes_alias() {
|
|
assert_eq!(SourceType::parse("notes"), Some(SourceType::Note));
|
|
}
|
|
|
|
#[test]
|
|
fn test_source_type_as_str() {
|
|
assert_eq!(SourceType::Issue.as_str(), "issue");
|
|
assert_eq!(SourceType::MergeRequest.as_str(), "merge_request");
|
|
assert_eq!(SourceType::Discussion.as_str(), "discussion");
|
|
}
|
|
|
|
#[test]
|
|
fn test_source_type_display() {
|
|
assert_eq!(format!("{}", SourceType::Issue), "issue");
|
|
assert_eq!(format!("{}", SourceType::MergeRequest), "merge_request");
|
|
assert_eq!(format!("{}", SourceType::Discussion), "discussion");
|
|
}
|
|
|
|
#[test]
|
|
fn test_content_hash_deterministic() {
|
|
let hash1 = compute_content_hash("hello");
|
|
let hash2 = compute_content_hash("hello");
|
|
assert_eq!(hash1, hash2);
|
|
assert!(!hash1.is_empty());
|
|
assert_eq!(hash1.len(), 64);
|
|
}
|
|
|
|
#[test]
|
|
fn test_content_hash_different_inputs() {
|
|
let hash1 = compute_content_hash("hello");
|
|
let hash2 = compute_content_hash("world");
|
|
assert_ne!(hash1, hash2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_content_hash_empty() {
|
|
let hash = compute_content_hash("");
|
|
assert_eq!(hash.len(), 64);
|
|
}
|
|
|
|
#[test]
|
|
fn test_list_hash_order_independent() {
|
|
let hash1 = compute_list_hash(&["b".to_string(), "a".to_string()]);
|
|
let hash2 = compute_list_hash(&["a".to_string(), "b".to_string()]);
|
|
assert_eq!(hash1, hash2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_list_hash_empty() {
|
|
let hash = compute_list_hash(&[]);
|
|
assert_eq!(hash.len(), 64);
|
|
let hash2 = compute_list_hash(&[]);
|
|
assert_eq!(hash, hash2);
|
|
}
|
|
|
|
fn setup_test_db() -> Connection {
|
|
let conn = Connection::open_in_memory().unwrap();
|
|
conn.execute_batch(
|
|
"
|
|
CREATE TABLE projects (
|
|
id INTEGER PRIMARY KEY,
|
|
gitlab_project_id INTEGER UNIQUE NOT NULL,
|
|
path_with_namespace TEXT NOT NULL,
|
|
default_branch TEXT,
|
|
web_url TEXT,
|
|
created_at INTEGER,
|
|
updated_at INTEGER,
|
|
raw_payload_id INTEGER
|
|
);
|
|
CREATE TABLE issues (
|
|
id INTEGER PRIMARY KEY,
|
|
gitlab_id INTEGER UNIQUE NOT NULL,
|
|
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
iid INTEGER NOT NULL,
|
|
title TEXT,
|
|
description TEXT,
|
|
state TEXT NOT NULL,
|
|
author_username TEXT,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
last_seen_at INTEGER NOT NULL,
|
|
discussions_synced_for_updated_at INTEGER,
|
|
resource_events_synced_for_updated_at INTEGER,
|
|
web_url TEXT,
|
|
raw_payload_id INTEGER
|
|
);
|
|
CREATE TABLE labels (
|
|
id INTEGER PRIMARY KEY,
|
|
gitlab_id INTEGER,
|
|
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
name TEXT NOT NULL,
|
|
color TEXT,
|
|
description TEXT
|
|
);
|
|
CREATE TABLE issue_labels (
|
|
issue_id INTEGER NOT NULL REFERENCES issues(id),
|
|
label_id INTEGER NOT NULL REFERENCES labels(id),
|
|
PRIMARY KEY(issue_id, label_id)
|
|
);
|
|
",
|
|
)
|
|
.unwrap();
|
|
|
|
conn.execute(
|
|
"INSERT INTO projects (id, gitlab_project_id, path_with_namespace, web_url) VALUES (1, 100, 'group/project-one', 'https://gitlab.example.com/group/project-one')",
|
|
[],
|
|
).unwrap();
|
|
|
|
conn
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn insert_issue(
|
|
conn: &Connection,
|
|
id: i64,
|
|
iid: i64,
|
|
title: Option<&str>,
|
|
description: Option<&str>,
|
|
state: &str,
|
|
author: Option<&str>,
|
|
web_url: Option<&str>,
|
|
) {
|
|
conn.execute(
|
|
"INSERT INTO issues (id, gitlab_id, project_id, iid, title, description, state, author_username, created_at, updated_at, last_seen_at, web_url) VALUES (?1, ?2, 1, ?3, ?4, ?5, ?6, ?7, 1000, 2000, 3000, ?8)",
|
|
rusqlite::params![id, id * 10, iid, title, description, state, author, web_url],
|
|
).unwrap();
|
|
}
|
|
|
|
fn insert_label(conn: &Connection, id: i64, name: &str) {
|
|
conn.execute(
|
|
"INSERT INTO labels (id, project_id, name) VALUES (?1, 1, ?2)",
|
|
rusqlite::params![id, name],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
fn link_issue_label(conn: &Connection, issue_id: i64, label_id: i64) {
|
|
conn.execute(
|
|
"INSERT INTO issue_labels (issue_id, label_id) VALUES (?1, ?2)",
|
|
rusqlite::params![issue_id, label_id],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_issue_document_format() {
|
|
let conn = setup_test_db();
|
|
insert_issue(
|
|
&conn,
|
|
1,
|
|
234,
|
|
Some("Authentication redesign"),
|
|
Some("We need to modernize our authentication system..."),
|
|
"opened",
|
|
Some("johndoe"),
|
|
Some("https://gitlab.example.com/group/project-one/-/issues/234"),
|
|
);
|
|
insert_label(&conn, 1, "auth");
|
|
insert_label(&conn, 2, "bug");
|
|
link_issue_label(&conn, 1, 1);
|
|
link_issue_label(&conn, 1, 2);
|
|
|
|
let doc = extract_issue_document(&conn, 1).unwrap().unwrap();
|
|
assert_eq!(doc.source_type, SourceType::Issue);
|
|
assert_eq!(doc.source_id, 1);
|
|
assert_eq!(doc.project_id, 1);
|
|
assert_eq!(doc.author_username, Some("johndoe".to_string()));
|
|
assert!(
|
|
doc.content_text
|
|
.starts_with("[[Issue]] #234: Authentication redesign\n")
|
|
);
|
|
assert!(doc.content_text.contains("Project: group/project-one\n"));
|
|
assert!(
|
|
doc.content_text
|
|
.contains("URL: https://gitlab.example.com/group/project-one/-/issues/234\n")
|
|
);
|
|
assert!(doc.content_text.contains("Labels: [\"auth\",\"bug\"]\n"));
|
|
assert!(doc.content_text.contains("State: opened\n"));
|
|
assert!(doc.content_text.contains("Author: @johndoe\n"));
|
|
assert!(
|
|
doc.content_text
|
|
.contains("--- Description ---\n\nWe need to modernize our authentication system...")
|
|
);
|
|
assert!(!doc.is_truncated);
|
|
assert!(doc.paths.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_issue_not_found() {
|
|
let conn = setup_test_db();
|
|
let result = extract_issue_document(&conn, 999).unwrap();
|
|
assert!(result.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_issue_no_description() {
|
|
let conn = setup_test_db();
|
|
insert_issue(
|
|
&conn,
|
|
1,
|
|
10,
|
|
Some("Quick fix"),
|
|
None,
|
|
"opened",
|
|
Some("alice"),
|
|
None,
|
|
);
|
|
|
|
let doc = extract_issue_document(&conn, 1).unwrap().unwrap();
|
|
assert!(!doc.content_text.contains("--- Description ---"));
|
|
assert!(doc.content_text.contains("[[Issue]] #10: Quick fix\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_issue_labels_sorted() {
|
|
let conn = setup_test_db();
|
|
insert_issue(
|
|
&conn,
|
|
1,
|
|
10,
|
|
Some("Test"),
|
|
Some("Body"),
|
|
"opened",
|
|
Some("bob"),
|
|
None,
|
|
);
|
|
insert_label(&conn, 1, "zeta");
|
|
insert_label(&conn, 2, "alpha");
|
|
insert_label(&conn, 3, "middle");
|
|
link_issue_label(&conn, 1, 1);
|
|
link_issue_label(&conn, 1, 2);
|
|
link_issue_label(&conn, 1, 3);
|
|
|
|
let doc = extract_issue_document(&conn, 1).unwrap().unwrap();
|
|
assert_eq!(doc.labels, vec!["alpha", "middle", "zeta"]);
|
|
assert!(
|
|
doc.content_text
|
|
.contains("Labels: [\"alpha\",\"middle\",\"zeta\"]")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_issue_no_labels() {
|
|
let conn = setup_test_db();
|
|
insert_issue(
|
|
&conn,
|
|
1,
|
|
10,
|
|
Some("Test"),
|
|
Some("Body"),
|
|
"opened",
|
|
None,
|
|
None,
|
|
);
|
|
|
|
let doc = extract_issue_document(&conn, 1).unwrap().unwrap();
|
|
assert!(doc.labels.is_empty());
|
|
assert!(doc.content_text.contains("Labels: []\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_issue_hash_deterministic() {
|
|
let conn = setup_test_db();
|
|
insert_issue(
|
|
&conn,
|
|
1,
|
|
10,
|
|
Some("Test"),
|
|
Some("Body"),
|
|
"opened",
|
|
Some("alice"),
|
|
None,
|
|
);
|
|
|
|
let doc1 = extract_issue_document(&conn, 1).unwrap().unwrap();
|
|
let doc2 = extract_issue_document(&conn, 1).unwrap().unwrap();
|
|
assert_eq!(doc1.content_hash, doc2.content_hash);
|
|
assert_eq!(doc1.labels_hash, doc2.labels_hash);
|
|
assert_eq!(doc1.content_hash.len(), 64);
|
|
}
|
|
|
|
#[test]
|
|
fn test_issue_empty_description() {
|
|
let conn = setup_test_db();
|
|
insert_issue(&conn, 1, 10, Some("Test"), Some(""), "opened", None, None);
|
|
|
|
let doc = extract_issue_document(&conn, 1).unwrap().unwrap();
|
|
assert!(doc.content_text.contains("--- Description ---\n\n"));
|
|
}
|
|
|
|
fn setup_mr_test_db() -> Connection {
|
|
let conn = setup_test_db();
|
|
conn.execute_batch(
|
|
"
|
|
CREATE TABLE merge_requests (
|
|
id INTEGER PRIMARY KEY,
|
|
gitlab_id INTEGER UNIQUE NOT NULL,
|
|
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
iid INTEGER NOT NULL,
|
|
title TEXT,
|
|
description TEXT,
|
|
state TEXT,
|
|
draft INTEGER NOT NULL DEFAULT 0,
|
|
author_username TEXT,
|
|
source_branch TEXT,
|
|
target_branch TEXT,
|
|
head_sha TEXT,
|
|
references_short TEXT,
|
|
references_full TEXT,
|
|
detailed_merge_status TEXT,
|
|
merge_user_username TEXT,
|
|
created_at INTEGER,
|
|
updated_at INTEGER,
|
|
merged_at INTEGER,
|
|
closed_at INTEGER,
|
|
last_seen_at INTEGER NOT NULL,
|
|
discussions_synced_for_updated_at INTEGER,
|
|
discussions_sync_last_attempt_at INTEGER,
|
|
discussions_sync_attempts INTEGER DEFAULT 0,
|
|
discussions_sync_last_error TEXT,
|
|
resource_events_synced_for_updated_at INTEGER,
|
|
web_url TEXT,
|
|
raw_payload_id INTEGER
|
|
);
|
|
CREATE TABLE mr_labels (
|
|
merge_request_id INTEGER REFERENCES merge_requests(id),
|
|
label_id INTEGER REFERENCES labels(id),
|
|
PRIMARY KEY(merge_request_id, label_id)
|
|
);
|
|
",
|
|
)
|
|
.unwrap();
|
|
conn
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn insert_mr(
|
|
conn: &Connection,
|
|
id: i64,
|
|
iid: i64,
|
|
title: Option<&str>,
|
|
description: Option<&str>,
|
|
state: Option<&str>,
|
|
author: Option<&str>,
|
|
source_branch: Option<&str>,
|
|
target_branch: Option<&str>,
|
|
web_url: Option<&str>,
|
|
) {
|
|
conn.execute(
|
|
"INSERT INTO merge_requests (id, gitlab_id, project_id, iid, title, description, state, author_username, source_branch, target_branch, created_at, updated_at, last_seen_at, web_url) VALUES (?1, ?2, 1, ?3, ?4, ?5, ?6, ?7, ?8, ?9, 1000, 2000, 3000, ?10)",
|
|
rusqlite::params![id, id * 10, iid, title, description, state, author, source_branch, target_branch, web_url],
|
|
).unwrap();
|
|
}
|
|
|
|
fn link_mr_label(conn: &Connection, mr_id: i64, label_id: i64) {
|
|
conn.execute(
|
|
"INSERT INTO mr_labels (merge_request_id, label_id) VALUES (?1, ?2)",
|
|
rusqlite::params![mr_id, label_id],
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_mr_document_format() {
|
|
let conn = setup_mr_test_db();
|
|
insert_mr(
|
|
&conn,
|
|
1,
|
|
456,
|
|
Some("Implement JWT authentication"),
|
|
Some("This MR implements JWT-based authentication..."),
|
|
Some("opened"),
|
|
Some("johndoe"),
|
|
Some("feature/jwt-auth"),
|
|
Some("main"),
|
|
Some("https://gitlab.example.com/group/project-one/-/merge_requests/456"),
|
|
);
|
|
insert_label(&conn, 1, "auth");
|
|
insert_label(&conn, 2, "feature");
|
|
link_mr_label(&conn, 1, 1);
|
|
link_mr_label(&conn, 1, 2);
|
|
|
|
let doc = extract_mr_document(&conn, 1).unwrap().unwrap();
|
|
assert_eq!(doc.source_type, SourceType::MergeRequest);
|
|
assert_eq!(doc.source_id, 1);
|
|
assert!(
|
|
doc.content_text
|
|
.starts_with("[[MergeRequest]] !456: Implement JWT authentication\n")
|
|
);
|
|
assert!(doc.content_text.contains("Project: group/project-one\n"));
|
|
assert!(
|
|
doc.content_text
|
|
.contains("Labels: [\"auth\",\"feature\"]\n")
|
|
);
|
|
assert!(doc.content_text.contains("State: opened\n"));
|
|
assert!(doc.content_text.contains("Author: @johndoe\n"));
|
|
assert!(
|
|
doc.content_text
|
|
.contains("Source: feature/jwt-auth -> main\n")
|
|
);
|
|
assert!(
|
|
doc.content_text
|
|
.contains("--- Description ---\n\nThis MR implements JWT-based authentication...")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mr_not_found() {
|
|
let conn = setup_mr_test_db();
|
|
let result = extract_mr_document(&conn, 999).unwrap();
|
|
assert!(result.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_mr_no_description() {
|
|
let conn = setup_mr_test_db();
|
|
insert_mr(
|
|
&conn,
|
|
1,
|
|
10,
|
|
Some("Quick fix"),
|
|
None,
|
|
Some("merged"),
|
|
Some("alice"),
|
|
Some("fix/bug"),
|
|
Some("main"),
|
|
None,
|
|
);
|
|
|
|
let doc = extract_mr_document(&conn, 1).unwrap().unwrap();
|
|
assert!(!doc.content_text.contains("--- Description ---"));
|
|
assert!(
|
|
doc.content_text
|
|
.contains("[[MergeRequest]] !10: Quick fix\n")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mr_branch_info() {
|
|
let conn = setup_mr_test_db();
|
|
insert_mr(
|
|
&conn,
|
|
1,
|
|
10,
|
|
Some("Test"),
|
|
Some("Body"),
|
|
Some("opened"),
|
|
None,
|
|
Some("feature/foo"),
|
|
Some("develop"),
|
|
None,
|
|
);
|
|
|
|
let doc = extract_mr_document(&conn, 1).unwrap().unwrap();
|
|
assert!(
|
|
doc.content_text
|
|
.contains("Source: feature/foo -> develop\n")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mr_no_branches() {
|
|
let conn = setup_mr_test_db();
|
|
insert_mr(
|
|
&conn,
|
|
1,
|
|
10,
|
|
Some("Test"),
|
|
None,
|
|
Some("opened"),
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
);
|
|
|
|
let doc = extract_mr_document(&conn, 1).unwrap().unwrap();
|
|
assert!(!doc.content_text.contains("Source:"));
|
|
}
|
|
|
|
fn setup_discussion_test_db() -> Connection {
|
|
let conn = setup_mr_test_db();
|
|
conn.execute_batch(
|
|
"
|
|
CREATE TABLE discussions (
|
|
id INTEGER PRIMARY KEY,
|
|
gitlab_discussion_id TEXT NOT NULL,
|
|
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
issue_id INTEGER REFERENCES issues(id),
|
|
merge_request_id INTEGER,
|
|
noteable_type TEXT NOT NULL,
|
|
individual_note INTEGER NOT NULL DEFAULT 0,
|
|
first_note_at INTEGER,
|
|
last_note_at INTEGER,
|
|
last_seen_at INTEGER NOT NULL,
|
|
resolvable INTEGER NOT NULL DEFAULT 0,
|
|
resolved INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
CREATE TABLE notes (
|
|
id INTEGER PRIMARY KEY,
|
|
gitlab_id INTEGER UNIQUE NOT NULL,
|
|
discussion_id INTEGER NOT NULL REFERENCES discussions(id),
|
|
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
note_type TEXT,
|
|
is_system INTEGER NOT NULL DEFAULT 0,
|
|
author_username TEXT,
|
|
body TEXT,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
last_seen_at INTEGER NOT NULL,
|
|
position INTEGER,
|
|
resolvable INTEGER NOT NULL DEFAULT 0,
|
|
resolved INTEGER NOT NULL DEFAULT 0,
|
|
resolved_by TEXT,
|
|
resolved_at INTEGER,
|
|
position_old_path TEXT,
|
|
position_new_path TEXT,
|
|
position_old_line INTEGER,
|
|
position_new_line INTEGER,
|
|
raw_payload_id INTEGER
|
|
);
|
|
",
|
|
)
|
|
.unwrap();
|
|
conn
|
|
}
|
|
|
|
fn insert_discussion(
|
|
conn: &Connection,
|
|
id: i64,
|
|
noteable_type: &str,
|
|
issue_id: Option<i64>,
|
|
mr_id: Option<i64>,
|
|
) {
|
|
conn.execute(
|
|
"INSERT INTO discussions (id, gitlab_discussion_id, project_id, issue_id, merge_request_id, noteable_type, last_seen_at) VALUES (?1, ?2, 1, ?3, ?4, ?5, 3000)",
|
|
rusqlite::params![id, format!("disc_{}", id), issue_id, mr_id, noteable_type],
|
|
).unwrap();
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn insert_note(
|
|
conn: &Connection,
|
|
id: i64,
|
|
gitlab_id: i64,
|
|
discussion_id: i64,
|
|
author: Option<&str>,
|
|
body: Option<&str>,
|
|
created_at: i64,
|
|
is_system: bool,
|
|
old_path: Option<&str>,
|
|
new_path: Option<&str>,
|
|
) {
|
|
conn.execute(
|
|
"INSERT INTO notes (id, gitlab_id, discussion_id, project_id, author_username, body, created_at, updated_at, last_seen_at, is_system, position_old_path, position_new_path) VALUES (?1, ?2, ?3, 1, ?4, ?5, ?6, ?6, ?6, ?7, ?8, ?9)",
|
|
rusqlite::params![id, gitlab_id, discussion_id, author, body, created_at, is_system as i32, old_path, new_path],
|
|
).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_discussion_document_format() {
|
|
let conn = setup_discussion_test_db();
|
|
insert_issue(
|
|
&conn,
|
|
1,
|
|
234,
|
|
Some("Authentication redesign"),
|
|
Some("desc"),
|
|
"opened",
|
|
Some("johndoe"),
|
|
Some("https://gitlab.example.com/group/project-one/-/issues/234"),
|
|
);
|
|
insert_label(&conn, 1, "auth");
|
|
insert_label(&conn, 2, "bug");
|
|
link_issue_label(&conn, 1, 1);
|
|
link_issue_label(&conn, 1, 2);
|
|
insert_discussion(&conn, 1, "Issue", Some(1), None);
|
|
insert_note(
|
|
&conn,
|
|
1,
|
|
12345,
|
|
1,
|
|
Some("johndoe"),
|
|
Some("I think we should move to JWT-based auth..."),
|
|
1710460800000,
|
|
false,
|
|
None,
|
|
None,
|
|
);
|
|
insert_note(
|
|
&conn,
|
|
2,
|
|
12346,
|
|
1,
|
|
Some("janedoe"),
|
|
Some("Agreed. What about refresh token strategy?"),
|
|
1710460800000,
|
|
false,
|
|
None,
|
|
None,
|
|
);
|
|
|
|
let doc = extract_discussion_document(&conn, 1).unwrap().unwrap();
|
|
assert_eq!(doc.source_type, SourceType::Discussion);
|
|
assert!(
|
|
doc.content_text
|
|
.starts_with("[[Discussion]] Issue #234: Authentication redesign\n")
|
|
);
|
|
assert!(doc.content_text.contains("Project: group/project-one\n"));
|
|
assert!(
|
|
doc.content_text.contains(
|
|
"URL: https://gitlab.example.com/group/project-one/-/issues/234#note_12345\n"
|
|
)
|
|
);
|
|
assert!(doc.content_text.contains("Labels: [\"auth\",\"bug\"]\n"));
|
|
assert!(doc.content_text.contains("--- Thread ---"));
|
|
assert!(
|
|
doc.content_text
|
|
.contains("@johndoe (2024-03-15):\nI think we should move to JWT-based auth...")
|
|
);
|
|
assert!(
|
|
doc.content_text
|
|
.contains("@janedoe (2024-03-15):\nAgreed. What about refresh token strategy?")
|
|
);
|
|
assert_eq!(doc.author_username, Some("johndoe".to_string()));
|
|
assert!(doc.title.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_discussion_not_found() {
|
|
let conn = setup_discussion_test_db();
|
|
let result = extract_discussion_document(&conn, 999).unwrap();
|
|
assert!(result.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_discussion_parent_deleted() {
|
|
let conn = setup_discussion_test_db();
|
|
insert_issue(
|
|
&conn,
|
|
99,
|
|
10,
|
|
Some("To be deleted"),
|
|
None,
|
|
"opened",
|
|
None,
|
|
None,
|
|
);
|
|
insert_discussion(&conn, 1, "Issue", Some(99), None);
|
|
insert_note(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
Some("alice"),
|
|
Some("Hello"),
|
|
1000,
|
|
false,
|
|
None,
|
|
None,
|
|
);
|
|
conn.execute("PRAGMA foreign_keys = OFF", []).unwrap();
|
|
conn.execute("DELETE FROM issues WHERE id = 99", [])
|
|
.unwrap();
|
|
conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
|
|
|
|
let result = extract_discussion_document(&conn, 1).unwrap();
|
|
assert!(result.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_discussion_system_notes_excluded() {
|
|
let conn = setup_discussion_test_db();
|
|
insert_issue(
|
|
&conn,
|
|
1,
|
|
10,
|
|
Some("Test"),
|
|
Some("desc"),
|
|
"opened",
|
|
Some("alice"),
|
|
None,
|
|
);
|
|
insert_discussion(&conn, 1, "Issue", Some(1), None);
|
|
insert_note(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
Some("alice"),
|
|
Some("Real comment"),
|
|
1000,
|
|
false,
|
|
None,
|
|
None,
|
|
);
|
|
insert_note(
|
|
&conn,
|
|
2,
|
|
101,
|
|
1,
|
|
Some("bot"),
|
|
Some("assigned to @alice"),
|
|
2000,
|
|
true,
|
|
None,
|
|
None,
|
|
);
|
|
insert_note(
|
|
&conn,
|
|
3,
|
|
102,
|
|
1,
|
|
Some("bob"),
|
|
Some("Follow-up"),
|
|
3000,
|
|
false,
|
|
None,
|
|
None,
|
|
);
|
|
|
|
let doc = extract_discussion_document(&conn, 1).unwrap().unwrap();
|
|
assert!(doc.content_text.contains("@alice"));
|
|
assert!(doc.content_text.contains("@bob"));
|
|
assert!(!doc.content_text.contains("assigned to"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_discussion_diffnote_paths() {
|
|
let conn = setup_discussion_test_db();
|
|
insert_issue(
|
|
&conn,
|
|
1,
|
|
10,
|
|
Some("Test"),
|
|
Some("desc"),
|
|
"opened",
|
|
None,
|
|
None,
|
|
);
|
|
insert_discussion(&conn, 1, "Issue", Some(1), None);
|
|
insert_note(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
Some("alice"),
|
|
Some("Comment on code"),
|
|
1000,
|
|
false,
|
|
Some("src/old.rs"),
|
|
Some("src/new.rs"),
|
|
);
|
|
insert_note(
|
|
&conn,
|
|
2,
|
|
101,
|
|
1,
|
|
Some("bob"),
|
|
Some("Reply"),
|
|
2000,
|
|
false,
|
|
Some("src/old.rs"),
|
|
Some("src/new.rs"),
|
|
);
|
|
|
|
let doc = extract_discussion_document(&conn, 1).unwrap().unwrap();
|
|
assert_eq!(doc.paths, vec!["src/new.rs", "src/old.rs"]);
|
|
assert!(
|
|
doc.content_text
|
|
.contains("Files: [\"src/new.rs\",\"src/old.rs\"]")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_discussion_url_construction() {
|
|
let conn = setup_discussion_test_db();
|
|
insert_issue(
|
|
&conn,
|
|
1,
|
|
10,
|
|
Some("Test"),
|
|
Some("desc"),
|
|
"opened",
|
|
None,
|
|
Some("https://gitlab.example.com/group/project-one/-/issues/10"),
|
|
);
|
|
insert_discussion(&conn, 1, "Issue", Some(1), None);
|
|
insert_note(
|
|
&conn,
|
|
1,
|
|
54321,
|
|
1,
|
|
Some("alice"),
|
|
Some("Hello"),
|
|
1000,
|
|
false,
|
|
None,
|
|
None,
|
|
);
|
|
|
|
let doc = extract_discussion_document(&conn, 1).unwrap().unwrap();
|
|
assert_eq!(
|
|
doc.url,
|
|
Some("https://gitlab.example.com/group/project-one/-/issues/10#note_54321".to_string())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_discussion_uses_parent_labels() {
|
|
let conn = setup_discussion_test_db();
|
|
insert_issue(
|
|
&conn,
|
|
1,
|
|
10,
|
|
Some("Test"),
|
|
Some("desc"),
|
|
"opened",
|
|
None,
|
|
None,
|
|
);
|
|
insert_label(&conn, 1, "backend");
|
|
insert_label(&conn, 2, "api");
|
|
link_issue_label(&conn, 1, 1);
|
|
link_issue_label(&conn, 1, 2);
|
|
insert_discussion(&conn, 1, "Issue", Some(1), None);
|
|
insert_note(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
Some("alice"),
|
|
Some("Comment"),
|
|
1000,
|
|
false,
|
|
None,
|
|
None,
|
|
);
|
|
|
|
let doc = extract_discussion_document(&conn, 1).unwrap().unwrap();
|
|
assert_eq!(doc.labels, vec!["api", "backend"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_discussion_on_mr() {
|
|
let conn = setup_discussion_test_db();
|
|
insert_mr(
|
|
&conn,
|
|
1,
|
|
456,
|
|
Some("JWT Auth"),
|
|
Some("desc"),
|
|
Some("opened"),
|
|
Some("johndoe"),
|
|
Some("feature/jwt"),
|
|
Some("main"),
|
|
Some("https://gitlab.example.com/group/project-one/-/merge_requests/456"),
|
|
);
|
|
insert_discussion(&conn, 1, "MergeRequest", None, Some(1));
|
|
insert_note(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
Some("alice"),
|
|
Some("LGTM"),
|
|
1000,
|
|
false,
|
|
None,
|
|
None,
|
|
);
|
|
|
|
let doc = extract_discussion_document(&conn, 1).unwrap().unwrap();
|
|
assert!(
|
|
doc.content_text
|
|
.contains("[[Discussion]] MR !456: JWT Auth\n")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_discussion_all_system_notes() {
|
|
let conn = setup_discussion_test_db();
|
|
insert_issue(
|
|
&conn,
|
|
1,
|
|
10,
|
|
Some("Test"),
|
|
Some("desc"),
|
|
"opened",
|
|
None,
|
|
None,
|
|
);
|
|
insert_discussion(&conn, 1, "Issue", Some(1), None);
|
|
insert_note(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
Some("bot"),
|
|
Some("assigned to @alice"),
|
|
1000,
|
|
true,
|
|
None,
|
|
None,
|
|
);
|
|
|
|
let result = extract_discussion_document(&conn, 1).unwrap();
|
|
assert!(result.is_none());
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn insert_note_with_type(
|
|
conn: &Connection,
|
|
id: i64,
|
|
gitlab_id: i64,
|
|
discussion_id: i64,
|
|
author: Option<&str>,
|
|
body: Option<&str>,
|
|
created_at: i64,
|
|
is_system: bool,
|
|
old_path: Option<&str>,
|
|
new_path: Option<&str>,
|
|
old_line: Option<i64>,
|
|
new_line: Option<i64>,
|
|
note_type: Option<&str>,
|
|
resolvable: bool,
|
|
resolved: bool,
|
|
) {
|
|
conn.execute(
|
|
"INSERT INTO notes (id, gitlab_id, discussion_id, project_id, author_username, body, created_at, updated_at, last_seen_at, is_system, position_old_path, position_new_path, position_old_line, position_new_line, note_type, resolvable, resolved) VALUES (?1, ?2, ?3, 1, ?4, ?5, ?6, ?6, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
|
|
rusqlite::params![id, gitlab_id, discussion_id, author, body, created_at, is_system as i32, old_path, new_path, old_line, new_line, note_type, resolvable as i32, resolved as i32],
|
|
).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn test_note_document_basic_format() {
|
|
let conn = setup_discussion_test_db();
|
|
insert_issue(
|
|
&conn,
|
|
1,
|
|
42,
|
|
Some("Fix login bug"),
|
|
Some("desc"),
|
|
"opened",
|
|
Some("johndoe"),
|
|
Some("https://gitlab.example.com/group/project-one/-/issues/42"),
|
|
);
|
|
insert_discussion(&conn, 1, "Issue", Some(1), None);
|
|
insert_note(
|
|
&conn,
|
|
1,
|
|
12345,
|
|
1,
|
|
Some("alice"),
|
|
Some("This looks like a race condition"),
|
|
1710460800000,
|
|
false,
|
|
None,
|
|
None,
|
|
);
|
|
|
|
let doc = extract_note_document(&conn, 1).unwrap().unwrap();
|
|
assert_eq!(doc.source_type, SourceType::Note);
|
|
assert_eq!(doc.source_id, 1);
|
|
assert_eq!(doc.project_id, 1);
|
|
assert_eq!(doc.author_username, Some("alice".to_string()));
|
|
assert!(doc.content_text.contains("[[Note]]"));
|
|
assert!(doc.content_text.contains("source_type: note"));
|
|
assert!(doc.content_text.contains("note_gitlab_id: 12345"));
|
|
assert!(doc.content_text.contains("project: group/project-one"));
|
|
assert!(doc.content_text.contains("parent_type: Issue"));
|
|
assert!(doc.content_text.contains("parent_iid: 42"));
|
|
assert!(doc.content_text.contains("parent_title: Fix login bug"));
|
|
assert!(doc.content_text.contains("author: @alice"));
|
|
assert!(doc.content_text.contains("--- Body ---"));
|
|
assert!(
|
|
doc.content_text
|
|
.contains("This looks like a race condition")
|
|
);
|
|
assert_eq!(
|
|
doc.title,
|
|
Some("Note by @alice on Issue #42: Fix login bug".to_string())
|
|
);
|
|
assert_eq!(
|
|
doc.url,
|
|
Some("https://gitlab.example.com/group/project-one/-/issues/42#note_12345".to_string())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_note_document_diffnote_with_path() {
|
|
let conn = setup_discussion_test_db();
|
|
insert_issue(
|
|
&conn,
|
|
1,
|
|
10,
|
|
Some("Refactor auth"),
|
|
Some("desc"),
|
|
"opened",
|
|
None,
|
|
Some("https://gitlab.example.com/group/project-one/-/issues/10"),
|
|
);
|
|
insert_discussion(&conn, 1, "Issue", Some(1), None);
|
|
insert_note_with_type(
|
|
&conn,
|
|
1,
|
|
555,
|
|
1,
|
|
Some("bob"),
|
|
Some("Unused variable here"),
|
|
1000,
|
|
false,
|
|
Some("src/old_auth.rs"),
|
|
Some("src/auth.rs"),
|
|
Some(10),
|
|
Some(25),
|
|
Some("DiffNote"),
|
|
true,
|
|
false,
|
|
);
|
|
|
|
let doc = extract_note_document(&conn, 1).unwrap().unwrap();
|
|
assert!(doc.content_text.contains("note_type: DiffNote"));
|
|
assert!(doc.content_text.contains("path: src/auth.rs:25"));
|
|
assert!(doc.content_text.contains("resolved: false"));
|
|
assert_eq!(doc.paths, vec!["src/auth.rs", "src/old_auth.rs"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_note_document_inherits_parent_labels() {
|
|
let conn = setup_discussion_test_db();
|
|
insert_issue(
|
|
&conn,
|
|
1,
|
|
10,
|
|
Some("Test"),
|
|
Some("desc"),
|
|
"opened",
|
|
None,
|
|
None,
|
|
);
|
|
insert_label(&conn, 1, "backend");
|
|
insert_label(&conn, 2, "api");
|
|
link_issue_label(&conn, 1, 1);
|
|
link_issue_label(&conn, 1, 2);
|
|
insert_discussion(&conn, 1, "Issue", Some(1), None);
|
|
insert_note(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
Some("alice"),
|
|
Some("Note body"),
|
|
1000,
|
|
false,
|
|
None,
|
|
None,
|
|
);
|
|
|
|
let doc = extract_note_document(&conn, 1).unwrap().unwrap();
|
|
assert_eq!(doc.labels, vec!["api", "backend"]);
|
|
assert!(doc.content_text.contains("labels: api, backend"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_note_document_mr_parent() {
|
|
let conn = setup_discussion_test_db();
|
|
insert_mr(
|
|
&conn,
|
|
1,
|
|
456,
|
|
Some("JWT Auth"),
|
|
Some("desc"),
|
|
Some("opened"),
|
|
Some("johndoe"),
|
|
Some("feature/jwt"),
|
|
Some("main"),
|
|
Some("https://gitlab.example.com/group/project-one/-/merge_requests/456"),
|
|
);
|
|
insert_discussion(&conn, 1, "MergeRequest", None, Some(1));
|
|
insert_note(
|
|
&conn,
|
|
1,
|
|
200,
|
|
1,
|
|
Some("reviewer"),
|
|
Some("Needs tests"),
|
|
1000,
|
|
false,
|
|
None,
|
|
None,
|
|
);
|
|
|
|
let doc = extract_note_document(&conn, 1).unwrap().unwrap();
|
|
assert!(doc.content_text.contains("parent_type: MergeRequest"));
|
|
assert!(doc.content_text.contains("parent_iid: 456"));
|
|
assert_eq!(
|
|
doc.title,
|
|
Some("Note by @reviewer on MR !456: JWT Auth".to_string())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_note_document_system_note_returns_none() {
|
|
let conn = setup_discussion_test_db();
|
|
insert_issue(
|
|
&conn,
|
|
1,
|
|
10,
|
|
Some("Test"),
|
|
Some("desc"),
|
|
"opened",
|
|
None,
|
|
None,
|
|
);
|
|
insert_discussion(&conn, 1, "Issue", Some(1), None);
|
|
insert_note(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
Some("bot"),
|
|
Some("assigned to @alice"),
|
|
1000,
|
|
true,
|
|
None,
|
|
None,
|
|
);
|
|
|
|
let result = extract_note_document(&conn, 1).unwrap();
|
|
assert!(result.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_note_document_not_found() {
|
|
let conn = setup_discussion_test_db();
|
|
let result = extract_note_document(&conn, 999).unwrap();
|
|
assert!(result.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_note_document_orphaned_discussion() {
|
|
let conn = setup_discussion_test_db();
|
|
insert_discussion(&conn, 1, "Issue", None, None);
|
|
insert_note(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
Some("alice"),
|
|
Some("Comment"),
|
|
1000,
|
|
false,
|
|
None,
|
|
None,
|
|
);
|
|
|
|
let result = extract_note_document(&conn, 1).unwrap();
|
|
assert!(result.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_note_document_hash_deterministic() {
|
|
let conn = setup_discussion_test_db();
|
|
insert_issue(
|
|
&conn,
|
|
1,
|
|
10,
|
|
Some("Test"),
|
|
Some("desc"),
|
|
"opened",
|
|
None,
|
|
None,
|
|
);
|
|
insert_discussion(&conn, 1, "Issue", Some(1), None);
|
|
insert_note(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
Some("alice"),
|
|
Some("Comment"),
|
|
1000,
|
|
false,
|
|
None,
|
|
None,
|
|
);
|
|
|
|
let doc1 = extract_note_document(&conn, 1).unwrap().unwrap();
|
|
let doc2 = extract_note_document(&conn, 1).unwrap().unwrap();
|
|
assert_eq!(doc1.content_hash, doc2.content_hash);
|
|
assert_eq!(doc1.labels_hash, doc2.labels_hash);
|
|
assert_eq!(doc1.paths_hash, doc2.paths_hash);
|
|
assert_eq!(doc1.content_hash.len(), 64);
|
|
}
|
|
|
|
#[test]
|
|
fn test_note_document_empty_body() {
|
|
let conn = setup_discussion_test_db();
|
|
insert_issue(
|
|
&conn,
|
|
1,
|
|
10,
|
|
Some("Test"),
|
|
Some("desc"),
|
|
"opened",
|
|
None,
|
|
None,
|
|
);
|
|
insert_discussion(&conn, 1, "Issue", Some(1), None);
|
|
insert_note(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
Some("alice"),
|
|
Some(""),
|
|
1000,
|
|
false,
|
|
None,
|
|
None,
|
|
);
|
|
|
|
let doc = extract_note_document(&conn, 1).unwrap().unwrap();
|
|
assert!(doc.content_text.contains("--- Body ---\n\n"));
|
|
assert!(!doc.is_truncated);
|
|
}
|
|
|
|
#[test]
|
|
fn test_note_document_null_body() {
|
|
let conn = setup_discussion_test_db();
|
|
insert_issue(
|
|
&conn,
|
|
1,
|
|
10,
|
|
Some("Test"),
|
|
Some("desc"),
|
|
"opened",
|
|
None,
|
|
None,
|
|
);
|
|
insert_discussion(&conn, 1, "Issue", Some(1), None);
|
|
insert_note(
|
|
&conn,
|
|
1,
|
|
100,
|
|
1,
|
|
Some("alice"),
|
|
None,
|
|
1000,
|
|
false,
|
|
None,
|
|
None,
|
|
);
|
|
|
|
let doc = extract_note_document(&conn, 1).unwrap().unwrap();
|
|
assert!(doc.content_text.contains("--- Body ---\n\n"));
|
|
assert!(doc.content_text.ends_with("--- Body ---\n\n"));
|
|
}
|