test(ingestion): add MR + nonzero_summary tests, close bd-1au9
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -423,59 +423,5 @@ fn parse_timestamp(ts: &str) -> Result<i64> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
#[path = "merge_requests_tests.rs"]
|
||||||
use super::*;
|
mod tests;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn result_default_has_zero_counts() {
|
|
||||||
let result = IngestMergeRequestsResult::default();
|
|
||||||
assert_eq!(result.fetched, 0);
|
|
||||||
assert_eq!(result.upserted, 0);
|
|
||||||
assert_eq!(result.labels_created, 0);
|
|
||||||
assert_eq!(result.assignees_linked, 0);
|
|
||||||
assert_eq!(result.reviewers_linked, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cursor_filter_allows_newer_mrs() {
|
|
||||||
let cursor = SyncCursor {
|
|
||||||
updated_at_cursor: Some(1705312800000),
|
|
||||||
tie_breaker_id: Some(100),
|
|
||||||
};
|
|
||||||
|
|
||||||
let later_ts = 1705399200000;
|
|
||||||
assert!(passes_cursor_filter_with_ts(101, later_ts, &cursor));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cursor_filter_blocks_older_mrs() {
|
|
||||||
let cursor = SyncCursor {
|
|
||||||
updated_at_cursor: Some(1705312800000),
|
|
||||||
tie_breaker_id: Some(100),
|
|
||||||
};
|
|
||||||
|
|
||||||
let earlier_ts = 1705226400000;
|
|
||||||
assert!(!passes_cursor_filter_with_ts(99, earlier_ts, &cursor));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cursor_filter_uses_tie_breaker_for_same_timestamp() {
|
|
||||||
let cursor = SyncCursor {
|
|
||||||
updated_at_cursor: Some(1705312800000),
|
|
||||||
tie_breaker_id: Some(100),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(passes_cursor_filter_with_ts(101, 1705312800000, &cursor));
|
|
||||||
|
|
||||||
assert!(!passes_cursor_filter_with_ts(100, 1705312800000, &cursor));
|
|
||||||
|
|
||||||
assert!(!passes_cursor_filter_with_ts(99, 1705312800000, &cursor));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cursor_filter_allows_all_when_no_cursor() {
|
|
||||||
let cursor = SyncCursor::default();
|
|
||||||
let old_ts = 1577836800000;
|
|
||||||
assert!(passes_cursor_filter_with_ts(1, old_ts, &cursor));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
704
src/ingestion/merge_requests_tests.rs
Normal file
704
src/ingestion/merge_requests_tests.rs
Normal file
@@ -0,0 +1,704 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::core::config::{
|
||||||
|
EmbeddingConfig, GitLabConfig, LoggingConfig, ProjectConfig, ScoringConfig, StorageConfig,
|
||||||
|
SyncConfig,
|
||||||
|
};
|
||||||
|
use crate::core::db::{create_connection, run_migrations};
|
||||||
|
use crate::gitlab::types::{GitLabAuthor, GitLabReferences, GitLabReviewer};
|
||||||
|
|
||||||
|
// ─── Test Helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn setup_test_db() -> Connection {
|
||||||
|
let conn = create_connection(Path::new(":memory:")).unwrap();
|
||||||
|
run_migrations(&conn).unwrap();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
conn
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_config() -> Config {
|
||||||
|
Config {
|
||||||
|
gitlab: GitLabConfig {
|
||||||
|
base_url: "https://gitlab.example.com".to_string(),
|
||||||
|
token_env_var: "GITLAB_TOKEN".to_string(),
|
||||||
|
},
|
||||||
|
projects: vec![ProjectConfig {
|
||||||
|
path: "group/project".to_string(),
|
||||||
|
}],
|
||||||
|
default_project: None,
|
||||||
|
sync: SyncConfig::default(),
|
||||||
|
storage: StorageConfig::default(),
|
||||||
|
embedding: EmbeddingConfig::default(),
|
||||||
|
logging: LoggingConfig::default(),
|
||||||
|
scoring: ScoringConfig::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_test_mr(id: i64, updated_at: &str) -> GitLabMergeRequest {
|
||||||
|
GitLabMergeRequest {
|
||||||
|
id,
|
||||||
|
iid: id,
|
||||||
|
project_id: 100,
|
||||||
|
title: format!("MR {}", id),
|
||||||
|
description: None,
|
||||||
|
state: "opened".to_string(),
|
||||||
|
draft: false,
|
||||||
|
work_in_progress: false,
|
||||||
|
source_branch: "feature".to_string(),
|
||||||
|
target_branch: "main".to_string(),
|
||||||
|
sha: Some("abc123".to_string()),
|
||||||
|
references: Some(GitLabReferences {
|
||||||
|
short: format!("!{}", id),
|
||||||
|
full: format!("group/project!{}", id),
|
||||||
|
}),
|
||||||
|
detailed_merge_status: Some("mergeable".to_string()),
|
||||||
|
merge_status_legacy: None,
|
||||||
|
created_at: "2024-01-01T00:00:00.000Z".to_string(),
|
||||||
|
updated_at: updated_at.to_string(),
|
||||||
|
merged_at: None,
|
||||||
|
closed_at: None,
|
||||||
|
author: GitLabAuthor {
|
||||||
|
id: 1,
|
||||||
|
username: "test".to_string(),
|
||||||
|
name: "Test".to_string(),
|
||||||
|
},
|
||||||
|
merge_user: None,
|
||||||
|
merged_by: None,
|
||||||
|
labels: vec![],
|
||||||
|
assignees: vec![],
|
||||||
|
reviewers: vec![],
|
||||||
|
web_url: "https://example.com".to_string(),
|
||||||
|
merge_commit_sha: None,
|
||||||
|
squash_commit_sha: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_mr_with_labels(id: i64, labels: Vec<&str>) -> GitLabMergeRequest {
|
||||||
|
let mut mr = make_test_mr(id, "2024-06-01T00:00:00.000Z");
|
||||||
|
mr.labels = labels.into_iter().map(String::from).collect();
|
||||||
|
mr
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_mr_with_assignees(id: i64, assignees: Vec<(&str, &str)>) -> GitLabMergeRequest {
|
||||||
|
let mut mr = make_test_mr(id, "2024-06-01T00:00:00.000Z");
|
||||||
|
mr.assignees = assignees
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, (username, name))| GitLabAuthor {
|
||||||
|
id: (i + 10) as i64,
|
||||||
|
username: username.to_string(),
|
||||||
|
name: name.to_string(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
mr
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_mr_with_reviewers(id: i64, reviewers: Vec<(&str, &str)>) -> GitLabMergeRequest {
|
||||||
|
let mut mr = make_test_mr(id, "2024-06-01T00:00:00.000Z");
|
||||||
|
mr.reviewers = reviewers
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, (username, name))| GitLabReviewer {
|
||||||
|
id: (i + 20) as i64,
|
||||||
|
username: username.to_string(),
|
||||||
|
name: name.to_string(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
mr
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_rows(conn: &Connection, table: &str) -> i64 {
|
||||||
|
conn.query_row(&format!("SELECT COUNT(*) FROM {table}"), [], |row| {
|
||||||
|
row.get(0)
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Cursor Filter Tests (preserved from inline) ───────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn result_default_has_zero_counts() {
|
||||||
|
let result = IngestMergeRequestsResult::default();
|
||||||
|
assert_eq!(result.fetched, 0);
|
||||||
|
assert_eq!(result.upserted, 0);
|
||||||
|
assert_eq!(result.labels_created, 0);
|
||||||
|
assert_eq!(result.assignees_linked, 0);
|
||||||
|
assert_eq!(result.reviewers_linked, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cursor_filter_allows_newer_mrs() {
|
||||||
|
let cursor = SyncCursor {
|
||||||
|
updated_at_cursor: Some(1705312800000),
|
||||||
|
tie_breaker_id: Some(100),
|
||||||
|
};
|
||||||
|
|
||||||
|
let later_ts = 1705399200000;
|
||||||
|
assert!(passes_cursor_filter_with_ts(101, later_ts, &cursor));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cursor_filter_blocks_older_mrs() {
|
||||||
|
let cursor = SyncCursor {
|
||||||
|
updated_at_cursor: Some(1705312800000),
|
||||||
|
tie_breaker_id: Some(100),
|
||||||
|
};
|
||||||
|
|
||||||
|
let earlier_ts = 1705226400000;
|
||||||
|
assert!(!passes_cursor_filter_with_ts(99, earlier_ts, &cursor));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cursor_filter_uses_tie_breaker_for_same_timestamp() {
|
||||||
|
let cursor = SyncCursor {
|
||||||
|
updated_at_cursor: Some(1705312800000),
|
||||||
|
tie_breaker_id: Some(100),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(passes_cursor_filter_with_ts(101, 1705312800000, &cursor));
|
||||||
|
assert!(!passes_cursor_filter_with_ts(100, 1705312800000, &cursor));
|
||||||
|
assert!(!passes_cursor_filter_with_ts(99, 1705312800000, &cursor));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cursor_filter_allows_all_when_no_cursor() {
|
||||||
|
let cursor = SyncCursor::default();
|
||||||
|
let old_ts = 1577836800000;
|
||||||
|
assert!(passes_cursor_filter_with_ts(1, old_ts, &cursor));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── parse_timestamp Tests ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_timestamp_valid_rfc3339() {
|
||||||
|
let ts = parse_timestamp("2024-06-15T12:30:00.000Z").unwrap();
|
||||||
|
assert_eq!(ts, 1718454600000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_timestamp_invalid_format_returns_error() {
|
||||||
|
let result = parse_timestamp("not-a-date");
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err_msg = result.unwrap_err().to_string();
|
||||||
|
assert!(err_msg.contains("not-a-date"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Sync Cursor DB Tests ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_sync_cursor_returns_default_when_no_row() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
let cursor = get_sync_cursor(&conn, 1).unwrap();
|
||||||
|
assert!(cursor.updated_at_cursor.is_none());
|
||||||
|
assert!(cursor.tie_breaker_id.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_sync_cursor_creates_and_reads_back() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
|
||||||
|
update_sync_cursor(&conn, 1, 1705312800000, 42).unwrap();
|
||||||
|
|
||||||
|
let cursor = get_sync_cursor(&conn, 1).unwrap();
|
||||||
|
assert_eq!(cursor.updated_at_cursor, Some(1705312800000));
|
||||||
|
assert_eq!(cursor.tie_breaker_id, Some(42));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_sync_cursor_upserts_on_conflict() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
|
||||||
|
update_sync_cursor(&conn, 1, 1000, 10).unwrap();
|
||||||
|
update_sync_cursor(&conn, 1, 2000, 20).unwrap();
|
||||||
|
|
||||||
|
let cursor = get_sync_cursor(&conn, 1).unwrap();
|
||||||
|
assert_eq!(cursor.updated_at_cursor, Some(2000));
|
||||||
|
assert_eq!(cursor.tie_breaker_id, Some(20));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_sync_cursor_clears_cursor() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
|
||||||
|
update_sync_cursor(&conn, 1, 1000, 10).unwrap();
|
||||||
|
reset_sync_cursor(&conn, 1).unwrap();
|
||||||
|
|
||||||
|
let cursor = get_sync_cursor(&conn, 1).unwrap();
|
||||||
|
assert!(cursor.updated_at_cursor.is_none());
|
||||||
|
assert!(cursor.tie_breaker_id.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sync_cursors_are_project_scoped() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO projects (id, gitlab_project_id, path_with_namespace, web_url)
|
||||||
|
VALUES (2, 200, 'other/project', 'https://gitlab.example.com/other/project')",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
update_sync_cursor(&conn, 1, 1000, 10).unwrap();
|
||||||
|
update_sync_cursor(&conn, 2, 2000, 20).unwrap();
|
||||||
|
|
||||||
|
let c1 = get_sync_cursor(&conn, 1).unwrap();
|
||||||
|
let c2 = get_sync_cursor(&conn, 2).unwrap();
|
||||||
|
assert_eq!(c1.updated_at_cursor, Some(1000));
|
||||||
|
assert_eq!(c2.updated_at_cursor, Some(2000));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── process_single_mr Tests ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn process_single_mr_inserts_basic_mr() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
let config = test_config();
|
||||||
|
let mr = make_test_mr(1001, "2024-06-15T12:00:00.000Z");
|
||||||
|
|
||||||
|
let result = process_single_mr(&conn, &config, 1, &mr).unwrap();
|
||||||
|
assert_eq!(result.labels_created, 0);
|
||||||
|
assert_eq!(result.assignees_linked, 0);
|
||||||
|
assert_eq!(result.reviewers_linked, 0);
|
||||||
|
|
||||||
|
let (title, state, author, source_branch): (String, String, String, String) = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT title, state, author_username, source_branch
|
||||||
|
FROM merge_requests WHERE gitlab_id = 1001",
|
||||||
|
[],
|
||||||
|
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(title, "MR 1001");
|
||||||
|
assert_eq!(state, "opened");
|
||||||
|
assert_eq!(author, "test");
|
||||||
|
assert_eq!(source_branch, "feature");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn process_single_mr_upserts_on_conflict() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
let config = test_config();
|
||||||
|
|
||||||
|
let mr_v1 = make_test_mr(1001, "2024-06-15T12:00:00.000Z");
|
||||||
|
process_single_mr(&conn, &config, 1, &mr_v1).unwrap();
|
||||||
|
|
||||||
|
let mut mr_v2 = make_test_mr(1001, "2024-06-16T12:00:00.000Z");
|
||||||
|
mr_v2.title = "Updated MR title".to_string();
|
||||||
|
mr_v2.state = "merged".to_string();
|
||||||
|
process_single_mr(&conn, &config, 1, &mr_v2).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(count_rows(&conn, "merge_requests"), 1);
|
||||||
|
|
||||||
|
let (title, state): (String, String) = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT title, state FROM merge_requests WHERE gitlab_id = 1001",
|
||||||
|
[],
|
||||||
|
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(title, "Updated MR title");
|
||||||
|
assert_eq!(state, "merged");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn process_single_mr_creates_labels() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
let config = test_config();
|
||||||
|
let mr = make_mr_with_labels(1001, vec!["bug", "critical"]);
|
||||||
|
|
||||||
|
let result = process_single_mr(&conn, &config, 1, &mr).unwrap();
|
||||||
|
assert_eq!(result.labels_created, 2);
|
||||||
|
|
||||||
|
assert_eq!(count_rows(&conn, "labels"), 2);
|
||||||
|
|
||||||
|
let label_count: i64 = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*) FROM mr_labels ml
|
||||||
|
JOIN merge_requests m ON ml.merge_request_id = m.id
|
||||||
|
WHERE m.gitlab_id = 1001",
|
||||||
|
[],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(label_count, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn process_single_mr_label_upsert_idempotent() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
let config = test_config();
|
||||||
|
|
||||||
|
let mr1 = make_mr_with_labels(1001, vec!["bug"]);
|
||||||
|
let created1 = process_single_mr(&conn, &config, 1, &mr1).unwrap();
|
||||||
|
assert_eq!(created1.labels_created, 1);
|
||||||
|
|
||||||
|
// Second MR with same label
|
||||||
|
let mr2 = make_mr_with_labels(1002, vec!["bug"]);
|
||||||
|
let created2 = process_single_mr(&conn, &config, 1, &mr2).unwrap();
|
||||||
|
assert_eq!(created2.labels_created, 0); // label already exists
|
||||||
|
|
||||||
|
// Only 1 label row, but 2 junction rows
|
||||||
|
assert_eq!(count_rows(&conn, "labels"), 1);
|
||||||
|
assert_eq!(count_rows(&conn, "mr_labels"), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn process_single_mr_replaces_labels_on_update() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
let config = test_config();
|
||||||
|
|
||||||
|
let mr_v1 = make_mr_with_labels(1001, vec!["bug", "critical"]);
|
||||||
|
process_single_mr(&conn, &config, 1, &mr_v1).unwrap();
|
||||||
|
|
||||||
|
let mut mr_v2 = make_mr_with_labels(1001, vec!["bug", "fixed"]);
|
||||||
|
mr_v2.updated_at = "2024-06-02T00:00:00.000Z".to_string();
|
||||||
|
process_single_mr(&conn, &config, 1, &mr_v2).unwrap();
|
||||||
|
|
||||||
|
let labels: Vec<String> = {
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare(
|
||||||
|
"SELECT l.name FROM labels l
|
||||||
|
JOIN mr_labels ml ON l.id = ml.label_id
|
||||||
|
JOIN merge_requests m ON ml.merge_request_id = m.id
|
||||||
|
WHERE m.gitlab_id = 1001
|
||||||
|
ORDER BY l.name",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
stmt.query_map([], |row| row.get(0))
|
||||||
|
.unwrap()
|
||||||
|
.collect::<std::result::Result<Vec<_>, _>>()
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
assert_eq!(labels, vec!["bug", "fixed"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn process_single_mr_creates_assignees() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
let config = test_config();
|
||||||
|
let mr = make_mr_with_assignees(1001, vec![("alice", "Alice"), ("bob", "Bob")]);
|
||||||
|
|
||||||
|
let result = process_single_mr(&conn, &config, 1, &mr).unwrap();
|
||||||
|
assert_eq!(result.assignees_linked, 2);
|
||||||
|
|
||||||
|
let count: i64 = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*) FROM mr_assignees ma
|
||||||
|
JOIN merge_requests m ON ma.merge_request_id = m.id
|
||||||
|
WHERE m.gitlab_id = 1001",
|
||||||
|
[],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(count, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn process_single_mr_replaces_assignees_on_update() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
let config = test_config();
|
||||||
|
|
||||||
|
let mr_v1 = make_mr_with_assignees(1001, vec![("alice", "Alice"), ("bob", "Bob")]);
|
||||||
|
process_single_mr(&conn, &config, 1, &mr_v1).unwrap();
|
||||||
|
|
||||||
|
let mut mr_v2 = make_mr_with_assignees(1001, vec![("alice", "Alice"), ("charlie", "Charlie")]);
|
||||||
|
mr_v2.updated_at = "2024-06-02T00:00:00.000Z".to_string();
|
||||||
|
process_single_mr(&conn, &config, 1, &mr_v2).unwrap();
|
||||||
|
|
||||||
|
let assignees: Vec<String> = {
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare(
|
||||||
|
"SELECT ma.username FROM mr_assignees ma
|
||||||
|
JOIN merge_requests m ON ma.merge_request_id = m.id
|
||||||
|
WHERE m.gitlab_id = 1001
|
||||||
|
ORDER BY ma.username",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
stmt.query_map([], |row| row.get(0))
|
||||||
|
.unwrap()
|
||||||
|
.collect::<std::result::Result<Vec<_>, _>>()
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
assert_eq!(assignees, vec!["alice", "charlie"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn process_single_mr_creates_reviewers() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
let config = test_config();
|
||||||
|
let mr = make_mr_with_reviewers(1001, vec![("reviewer1", "Reviewer One")]);
|
||||||
|
|
||||||
|
let result = process_single_mr(&conn, &config, 1, &mr).unwrap();
|
||||||
|
assert_eq!(result.reviewers_linked, 1);
|
||||||
|
|
||||||
|
let count: i64 = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*) FROM mr_reviewers mr
|
||||||
|
JOIN merge_requests m ON mr.merge_request_id = m.id
|
||||||
|
WHERE m.gitlab_id = 1001",
|
||||||
|
[],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(count, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn process_single_mr_replaces_reviewers_on_update() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
let config = test_config();
|
||||||
|
|
||||||
|
let mr_v1 = make_mr_with_reviewers(1001, vec![("alice", "Alice"), ("bob", "Bob")]);
|
||||||
|
process_single_mr(&conn, &config, 1, &mr_v1).unwrap();
|
||||||
|
|
||||||
|
let mut mr_v2 = make_mr_with_reviewers(1001, vec![("charlie", "Charlie")]);
|
||||||
|
mr_v2.updated_at = "2024-06-02T00:00:00.000Z".to_string();
|
||||||
|
process_single_mr(&conn, &config, 1, &mr_v2).unwrap();
|
||||||
|
|
||||||
|
let reviewers: Vec<String> = {
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare(
|
||||||
|
"SELECT mr.username FROM mr_reviewers mr
|
||||||
|
JOIN merge_requests m ON mr.merge_request_id = m.id
|
||||||
|
WHERE m.gitlab_id = 1001
|
||||||
|
ORDER BY mr.username",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
stmt.query_map([], |row| row.get(0))
|
||||||
|
.unwrap()
|
||||||
|
.collect::<std::result::Result<Vec<_>, _>>()
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
assert_eq!(reviewers, vec!["charlie"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn process_single_mr_marks_dirty() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
let config = test_config();
|
||||||
|
let mr = make_test_mr(1001, "2024-06-15T12:00:00.000Z");
|
||||||
|
|
||||||
|
process_single_mr(&conn, &config, 1, &mr).unwrap();
|
||||||
|
|
||||||
|
let local_id: i64 = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT id FROM merge_requests WHERE gitlab_id = 1001",
|
||||||
|
[],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let dirty_count: i64 = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*) FROM dirty_sources
|
||||||
|
WHERE source_type = 'merge_request' AND source_id = ?",
|
||||||
|
[local_id],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(dirty_count, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn process_single_mr_stores_raw_payload() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
let config = test_config();
|
||||||
|
let mr = make_test_mr(1001, "2024-06-15T12:00:00.000Z");
|
||||||
|
|
||||||
|
process_single_mr(&conn, &config, 1, &mr).unwrap();
|
||||||
|
|
||||||
|
let payload_id: Option<i64> = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT raw_payload_id FROM merge_requests WHERE gitlab_id = 1001",
|
||||||
|
[],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(payload_id.is_some());
|
||||||
|
|
||||||
|
let payload_count: i64 = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*) FROM raw_payloads WHERE id = ?",
|
||||||
|
[payload_id.unwrap()],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(payload_count, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn process_single_mr_stores_merge_commit_sha() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
let config = test_config();
|
||||||
|
let mut mr = make_test_mr(1001, "2024-06-15T12:00:00.000Z");
|
||||||
|
mr.state = "merged".to_string();
|
||||||
|
mr.merge_commit_sha = Some("deadbeef1234".to_string());
|
||||||
|
mr.squash_commit_sha = Some("cafebabe5678".to_string());
|
||||||
|
|
||||||
|
process_single_mr(&conn, &config, 1, &mr).unwrap();
|
||||||
|
|
||||||
|
let (merge_sha, squash_sha): (Option<String>, Option<String>) = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT merge_commit_sha, squash_commit_sha
|
||||||
|
FROM merge_requests WHERE gitlab_id = 1001",
|
||||||
|
[],
|
||||||
|
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(merge_sha.as_deref(), Some("deadbeef1234"));
|
||||||
|
assert_eq!(squash_sha.as_deref(), Some("cafebabe5678"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Discussion Sync Queue Tests ───────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_mrs_needing_discussion_sync_detects_updated() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
let config = test_config();
|
||||||
|
|
||||||
|
let mr = make_test_mr(1001, "2024-06-15T12:00:00.000Z");
|
||||||
|
process_single_mr(&conn, &config, 1, &mr).unwrap();
|
||||||
|
|
||||||
|
// MR was just upserted, discussions_synced_for_updated_at is NULL
|
||||||
|
let needing_sync = get_mrs_needing_discussion_sync(&conn, 1).unwrap();
|
||||||
|
assert_eq!(needing_sync.len(), 1);
|
||||||
|
assert_eq!(needing_sync[0].iid, 1001);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_mrs_needing_discussion_sync_skips_already_synced() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
let config = test_config();
|
||||||
|
|
||||||
|
let mr = make_test_mr(1001, "2024-06-15T12:00:00.000Z");
|
||||||
|
process_single_mr(&conn, &config, 1, &mr).unwrap();
|
||||||
|
|
||||||
|
// Simulate discussion sync
|
||||||
|
let updated_at: i64 = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT updated_at FROM merge_requests WHERE gitlab_id = 1001",
|
||||||
|
[],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE merge_requests SET discussions_synced_for_updated_at = ? WHERE gitlab_id = 1001",
|
||||||
|
[updated_at],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let needing_sync = get_mrs_needing_discussion_sync(&conn, 1).unwrap();
|
||||||
|
assert!(needing_sync.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_mrs_needing_discussion_sync_is_project_scoped() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
let config = test_config();
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO projects (id, gitlab_project_id, path_with_namespace, web_url)
|
||||||
|
VALUES (2, 200, 'other/project', 'https://gitlab.example.com/other/project')",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mr1 = make_test_mr(1001, "2024-06-15T12:00:00.000Z");
|
||||||
|
process_single_mr(&conn, &config, 1, &mr1).unwrap();
|
||||||
|
|
||||||
|
let mut mr2 = make_test_mr(1002, "2024-06-15T12:00:00.000Z");
|
||||||
|
mr2.project_id = 200;
|
||||||
|
process_single_mr(&conn, &config, 2, &mr2).unwrap();
|
||||||
|
|
||||||
|
// Only project 1's MR should appear
|
||||||
|
let needing_sync = get_mrs_needing_discussion_sync(&conn, 1).unwrap();
|
||||||
|
assert_eq!(needing_sync.len(), 1);
|
||||||
|
assert_eq!(needing_sync[0].iid, 1001);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Reset / Full Sync Tests ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reset_discussion_watermarks_clears_all_for_project() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
let config = test_config();
|
||||||
|
|
||||||
|
let mr = make_test_mr(1001, "2024-06-15T12:00:00.000Z");
|
||||||
|
process_single_mr(&conn, &config, 1, &mr).unwrap();
|
||||||
|
|
||||||
|
// Set some watermarks
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE merge_requests SET
|
||||||
|
discussions_synced_for_updated_at = 1000,
|
||||||
|
resource_events_synced_for_updated_at = 2000,
|
||||||
|
closes_issues_synced_for_updated_at = 3000,
|
||||||
|
diffs_synced_for_updated_at = 4000
|
||||||
|
WHERE gitlab_id = 1001",
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
reset_discussion_watermarks(&conn, 1).unwrap();
|
||||||
|
|
||||||
|
let (disc_wm, events_wm, closes_wm, diffs_wm): (
|
||||||
|
Option<i64>,
|
||||||
|
Option<i64>,
|
||||||
|
Option<i64>,
|
||||||
|
Option<i64>,
|
||||||
|
) = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT discussions_synced_for_updated_at,
|
||||||
|
resource_events_synced_for_updated_at,
|
||||||
|
closes_issues_synced_for_updated_at,
|
||||||
|
diffs_synced_for_updated_at
|
||||||
|
FROM merge_requests WHERE gitlab_id = 1001",
|
||||||
|
[],
|
||||||
|
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(disc_wm.is_none());
|
||||||
|
assert!(events_wm.is_none());
|
||||||
|
assert!(closes_wm.is_none());
|
||||||
|
assert!(diffs_wm.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn process_single_mr_draft_and_references_stored() {
|
||||||
|
let conn = setup_test_db();
|
||||||
|
let config = test_config();
|
||||||
|
let mut mr = make_test_mr(1001, "2024-06-15T12:00:00.000Z");
|
||||||
|
mr.draft = true;
|
||||||
|
mr.references = Some(GitLabReferences {
|
||||||
|
short: "!1001".to_string(),
|
||||||
|
full: "group/project!1001".to_string(),
|
||||||
|
});
|
||||||
|
mr.detailed_merge_status = Some("checking".to_string());
|
||||||
|
|
||||||
|
process_single_mr(&conn, &config, 1, &mr).unwrap();
|
||||||
|
|
||||||
|
let (draft, refs_short, refs_full, merge_status): (
|
||||||
|
bool,
|
||||||
|
Option<String>,
|
||||||
|
Option<String>,
|
||||||
|
Option<String>,
|
||||||
|
) = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT draft, references_short, references_full, detailed_merge_status
|
||||||
|
FROM merge_requests WHERE gitlab_id = 1001",
|
||||||
|
[],
|
||||||
|
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(draft);
|
||||||
|
assert_eq!(refs_short.as_deref(), Some("!1001"));
|
||||||
|
assert_eq!(refs_full.as_deref(), Some("group/project!1001"));
|
||||||
|
assert_eq!(merge_status.as_deref(), Some("checking"));
|
||||||
|
}
|
||||||
@@ -37,3 +37,38 @@ pub use orchestrator::{
|
|||||||
ingest_project_issues, ingest_project_issues_with_progress, ingest_project_merge_requests,
|
ingest_project_issues, ingest_project_issues_with_progress, ingest_project_merge_requests,
|
||||||
ingest_project_merge_requests_with_progress,
|
ingest_project_merge_requests_with_progress,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nonzero_summary_all_zero_returns_nothing() {
|
||||||
|
let result = nonzero_summary(&[("fetched", 0), ("upserted", 0)]);
|
||||||
|
assert_eq!(result, "nothing to update");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nonzero_summary_empty_input_returns_nothing() {
|
||||||
|
let result = nonzero_summary(&[]);
|
||||||
|
assert_eq!(result, "nothing to update");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nonzero_summary_single_nonzero() {
|
||||||
|
let result = nonzero_summary(&[("fetched", 5), ("skipped", 0)]);
|
||||||
|
assert_eq!(result, "5 fetched");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nonzero_summary_multiple_nonzero_joined_with_middot() {
|
||||||
|
let result = nonzero_summary(&[("fetched", 10), ("upserted", 3), ("skipped", 0)]);
|
||||||
|
assert_eq!(result, "10 fetched \u{b7} 3 upserted");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nonzero_summary_all_nonzero() {
|
||||||
|
let result = nonzero_summary(&[("a", 1), ("b", 2), ("c", 3)]);
|
||||||
|
assert_eq!(result, "1 a \u{b7} 2 b \u{b7} 3 c");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user