- Fix MENTIONED_RE/CLOSED_BY_RE to match real GitLab format
('mentioned in issue #N' / 'mentioned in merge request !N')
- Add GITLAB_URL_RE + parse_url_refs() for full URL extraction
- Add extract_refs_from_descriptions() -> source_method='description_parse'
- Add extract_refs_from_user_notes() -> source_method='note_parse'
- Wire both into orchestrator after system note extraction
- 36 tests: regex fix, URL parsing, integration, idempotency
771 lines
25 KiB
Rust
771 lines
25 KiB
Rust
use super::*;
|
|
|
|
// --- parse_cross_refs: real GitLab system note format ---
|
|
|
|
#[test]
|
|
fn test_parse_mentioned_in_mr() {
|
|
let refs = parse_cross_refs("mentioned in merge request !567");
|
|
assert_eq!(refs.len(), 1);
|
|
assert_eq!(refs[0].reference_type, "mentioned");
|
|
assert_eq!(refs[0].target_entity_type, "merge_request");
|
|
assert_eq!(refs[0].target_iid, 567);
|
|
assert!(refs[0].target_project_path.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_mentioned_in_issue() {
|
|
let refs = parse_cross_refs("mentioned in issue #234");
|
|
assert_eq!(refs.len(), 1);
|
|
assert_eq!(refs[0].reference_type, "mentioned");
|
|
assert_eq!(refs[0].target_entity_type, "issue");
|
|
assert_eq!(refs[0].target_iid, 234);
|
|
assert!(refs[0].target_project_path.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_mentioned_cross_project() {
|
|
let refs = parse_cross_refs("mentioned in merge request group/repo!789");
|
|
assert_eq!(refs.len(), 1);
|
|
assert_eq!(refs[0].reference_type, "mentioned");
|
|
assert_eq!(refs[0].target_entity_type, "merge_request");
|
|
assert_eq!(refs[0].target_iid, 789);
|
|
assert_eq!(refs[0].target_project_path.as_deref(), Some("group/repo"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_mentioned_cross_project_issue() {
|
|
let refs = parse_cross_refs("mentioned in issue group/repo#123");
|
|
assert_eq!(refs.len(), 1);
|
|
assert_eq!(refs[0].reference_type, "mentioned");
|
|
assert_eq!(refs[0].target_entity_type, "issue");
|
|
assert_eq!(refs[0].target_iid, 123);
|
|
assert_eq!(refs[0].target_project_path.as_deref(), Some("group/repo"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_closed_by_mr() {
|
|
let refs = parse_cross_refs("closed by merge request !567");
|
|
assert_eq!(refs.len(), 1);
|
|
assert_eq!(refs[0].reference_type, "closes");
|
|
assert_eq!(refs[0].target_entity_type, "merge_request");
|
|
assert_eq!(refs[0].target_iid, 567);
|
|
assert!(refs[0].target_project_path.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_closed_by_cross_project() {
|
|
let refs = parse_cross_refs("closed by merge request group/repo!789");
|
|
assert_eq!(refs.len(), 1);
|
|
assert_eq!(refs[0].reference_type, "closes");
|
|
assert_eq!(refs[0].target_entity_type, "merge_request");
|
|
assert_eq!(refs[0].target_iid, 789);
|
|
assert_eq!(refs[0].target_project_path.as_deref(), Some("group/repo"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_multiple_refs() {
|
|
let refs = parse_cross_refs("mentioned in merge request !123 and mentioned in issue #456");
|
|
assert_eq!(refs.len(), 2);
|
|
assert_eq!(refs[0].target_entity_type, "merge_request");
|
|
assert_eq!(refs[0].target_iid, 123);
|
|
assert_eq!(refs[1].target_entity_type, "issue");
|
|
assert_eq!(refs[1].target_iid, 456);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_no_refs() {
|
|
let refs = parse_cross_refs("Updated the description");
|
|
assert!(refs.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_non_english_note() {
|
|
let refs = parse_cross_refs("a ajout\u{00e9} l'\u{00e9}tiquette ~bug");
|
|
assert!(refs.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_multi_level_group_path() {
|
|
let refs = parse_cross_refs("mentioned in issue top/sub/project#123");
|
|
assert_eq!(refs.len(), 1);
|
|
assert_eq!(
|
|
refs[0].target_project_path.as_deref(),
|
|
Some("top/sub/project")
|
|
);
|
|
assert_eq!(refs[0].target_iid, 123);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_deeply_nested_group_path() {
|
|
let refs = parse_cross_refs("mentioned in merge request a/b/c/d/e!42");
|
|
assert_eq!(refs.len(), 1);
|
|
assert_eq!(refs[0].target_project_path.as_deref(), Some("a/b/c/d/e"));
|
|
assert_eq!(refs[0].target_iid, 42);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_hyphenated_project_path() {
|
|
let refs = parse_cross_refs("mentioned in issue my-group/my-project#99");
|
|
assert_eq!(refs.len(), 1);
|
|
assert_eq!(
|
|
refs[0].target_project_path.as_deref(),
|
|
Some("my-group/my-project")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_dotted_project_path() {
|
|
let refs = parse_cross_refs("mentioned in issue visiostack.io/backend#123");
|
|
assert_eq!(refs.len(), 1);
|
|
assert_eq!(
|
|
refs[0].target_project_path.as_deref(),
|
|
Some("visiostack.io/backend")
|
|
);
|
|
assert_eq!(refs[0].target_iid, 123);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_dotted_nested_project_path() {
|
|
let refs = parse_cross_refs("closed by merge request my.org/sub.group/my.project!42");
|
|
assert_eq!(refs.len(), 1);
|
|
assert_eq!(
|
|
refs[0].target_project_path.as_deref(),
|
|
Some("my.org/sub.group/my.project")
|
|
);
|
|
assert_eq!(refs[0].target_entity_type, "merge_request");
|
|
assert_eq!(refs[0].target_iid, 42);
|
|
}
|
|
|
|
// Bare-sigil fallback (no "issue"/"merge request" word) still works
|
|
#[test]
|
|
fn test_parse_bare_sigil_fallback() {
|
|
let refs = parse_cross_refs("mentioned in #123");
|
|
assert_eq!(refs.len(), 1);
|
|
assert_eq!(refs[0].target_iid, 123);
|
|
assert_eq!(refs[0].target_entity_type, "issue");
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_bare_sigil_closed_by() {
|
|
let refs = parse_cross_refs("closed by !567");
|
|
assert_eq!(refs.len(), 1);
|
|
assert_eq!(refs[0].reference_type, "closes");
|
|
assert_eq!(refs[0].target_entity_type, "merge_request");
|
|
assert_eq!(refs[0].target_iid, 567);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_mixed_mentioned_and_closed() {
|
|
let refs = parse_cross_refs("mentioned in merge request !10 and closed by merge request !20");
|
|
assert_eq!(refs.len(), 2);
|
|
assert_eq!(refs[0].reference_type, "mentioned");
|
|
assert_eq!(refs[0].target_iid, 10);
|
|
assert_eq!(refs[1].reference_type, "closes");
|
|
assert_eq!(refs[1].target_iid, 20);
|
|
}
|
|
|
|
// --- parse_url_refs ---
|
|
|
|
#[test]
|
|
fn test_url_ref_same_project_issue() {
|
|
let refs = parse_url_refs(
|
|
"See https://gitlab.visiostack.com/vs/typescript-code/-/issues/3537 for details",
|
|
);
|
|
assert_eq!(refs.len(), 1);
|
|
assert_eq!(refs[0].target_entity_type, "issue");
|
|
assert_eq!(refs[0].target_iid, 3537);
|
|
assert_eq!(
|
|
refs[0].target_project_path.as_deref(),
|
|
Some("vs/typescript-code")
|
|
);
|
|
assert_eq!(refs[0].reference_type, "mentioned");
|
|
}
|
|
|
|
#[test]
|
|
fn test_url_ref_merge_request() {
|
|
let refs =
|
|
parse_url_refs("https://gitlab.visiostack.com/vs/typescript-code/-/merge_requests/3548");
|
|
assert_eq!(refs.len(), 1);
|
|
assert_eq!(refs[0].target_entity_type, "merge_request");
|
|
assert_eq!(refs[0].target_iid, 3548);
|
|
assert_eq!(
|
|
refs[0].target_project_path.as_deref(),
|
|
Some("vs/typescript-code")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_url_ref_cross_project() {
|
|
let refs = parse_url_refs(
|
|
"Related: https://gitlab.visiostack.com/vs/python-code/-/merge_requests/5203",
|
|
);
|
|
assert_eq!(refs.len(), 1);
|
|
assert_eq!(refs[0].target_entity_type, "merge_request");
|
|
assert_eq!(refs[0].target_iid, 5203);
|
|
assert_eq!(
|
|
refs[0].target_project_path.as_deref(),
|
|
Some("vs/python-code")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_url_ref_with_anchor() {
|
|
let refs =
|
|
parse_url_refs("https://gitlab.visiostack.com/vs/typescript-code/-/issues/123#note_456");
|
|
assert_eq!(refs.len(), 1);
|
|
assert_eq!(refs[0].target_entity_type, "issue");
|
|
assert_eq!(refs[0].target_iid, 123);
|
|
}
|
|
|
|
#[test]
|
|
fn test_url_ref_markdown_link() {
|
|
let refs = parse_url_refs(
|
|
"Check [this MR](https://gitlab.visiostack.com/vs/typescript-code/-/merge_requests/100) for context",
|
|
);
|
|
assert_eq!(refs.len(), 1);
|
|
assert_eq!(refs[0].target_entity_type, "merge_request");
|
|
assert_eq!(refs[0].target_iid, 100);
|
|
}
|
|
|
|
#[test]
|
|
fn test_url_ref_multiple_urls() {
|
|
let body =
|
|
"See https://gitlab.com/a/b/-/issues/1 and https://gitlab.com/a/b/-/merge_requests/2";
|
|
let refs = parse_url_refs(body);
|
|
assert_eq!(refs.len(), 2);
|
|
assert_eq!(refs[0].target_entity_type, "issue");
|
|
assert_eq!(refs[0].target_iid, 1);
|
|
assert_eq!(refs[1].target_entity_type, "merge_request");
|
|
assert_eq!(refs[1].target_iid, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_url_ref_deduplicates() {
|
|
let body = "See https://gitlab.com/a/b/-/issues/1 and again https://gitlab.com/a/b/-/issues/1";
|
|
let refs = parse_url_refs(body);
|
|
assert_eq!(
|
|
refs.len(),
|
|
1,
|
|
"Duplicate URLs in same body should be deduplicated"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_url_ref_non_gitlab_urls_ignored() {
|
|
let refs = parse_url_refs(
|
|
"Check https://google.com/search?q=test and https://github.com/org/repo/issues/1",
|
|
);
|
|
assert!(refs.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_url_ref_deeply_nested_project() {
|
|
let refs = parse_url_refs("https://gitlab.com/org/sub/deep/project/-/issues/42");
|
|
assert_eq!(refs.len(), 1);
|
|
assert_eq!(
|
|
refs[0].target_project_path.as_deref(),
|
|
Some("org/sub/deep/project")
|
|
);
|
|
assert_eq!(refs[0].target_iid, 42);
|
|
}
|
|
|
|
// --- Integration tests: system notes (updated for real format) ---
|
|
|
|
fn setup_test_db() -> Connection {
|
|
use crate::core::db::{create_connection, run_migrations};
|
|
|
|
let conn = create_connection(std::path::Path::new(":memory:")).unwrap();
|
|
run_migrations(&conn).unwrap();
|
|
conn
|
|
}
|
|
|
|
fn seed_test_data(conn: &Connection) -> i64 {
|
|
let now = now_ms();
|
|
|
|
conn.execute(
|
|
"INSERT INTO projects (id, gitlab_project_id, path_with_namespace, web_url, created_at, updated_at)
|
|
VALUES (1, 100, 'group/test-project', 'https://gitlab.com/group/test-project', ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
conn.execute(
|
|
"INSERT INTO issues (id, gitlab_id, project_id, iid, title, state, created_at, updated_at, last_seen_at)
|
|
VALUES (10, 1000, 1, 123, 'Test Issue', 'opened', ?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
conn.execute(
|
|
"INSERT INTO issues (id, gitlab_id, project_id, iid, title, state, created_at, updated_at, last_seen_at)
|
|
VALUES (11, 1001, 1, 456, 'Another Issue', 'opened', ?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
conn.execute(
|
|
"INSERT INTO merge_requests (id, gitlab_id, project_id, iid, title, state, source_branch, target_branch, author_username, created_at, updated_at, last_seen_at)
|
|
VALUES (20, 2000, 1, 789, 'Test MR', 'opened', 'feat', 'main', 'dev', ?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
conn.execute(
|
|
"INSERT INTO discussions (id, gitlab_discussion_id, project_id, issue_id, noteable_type, last_seen_at)
|
|
VALUES (30, 'disc-aaa', 1, 10, 'Issue', ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
conn.execute(
|
|
"INSERT INTO discussions (id, gitlab_discussion_id, project_id, merge_request_id, noteable_type, last_seen_at)
|
|
VALUES (31, 'disc-bbb', 1, 20, 'MergeRequest', ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
// System note: real GitLab format "mentioned in merge request !789"
|
|
conn.execute(
|
|
"INSERT INTO notes (id, gitlab_id, discussion_id, project_id, is_system, body, created_at, updated_at, last_seen_at)
|
|
VALUES (40, 4000, 30, 1, 1, 'mentioned in merge request !789', ?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
// System note: real GitLab format "mentioned in issue #456"
|
|
conn.execute(
|
|
"INSERT INTO notes (id, gitlab_id, discussion_id, project_id, is_system, body, created_at, updated_at, last_seen_at)
|
|
VALUES (41, 4001, 31, 1, 1, 'mentioned in issue #456', ?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
// User note (is_system=0) — should NOT be processed by system note extractor
|
|
conn.execute(
|
|
"INSERT INTO notes (id, gitlab_id, discussion_id, project_id, is_system, body, created_at, updated_at, last_seen_at)
|
|
VALUES (42, 4002, 30, 1, 0, 'mentioned in merge request !999', ?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
// System note with no cross-ref pattern
|
|
conn.execute(
|
|
"INSERT INTO notes (id, gitlab_id, discussion_id, project_id, is_system, body, created_at, updated_at, last_seen_at)
|
|
VALUES (43, 4003, 30, 1, 1, 'added label ~bug', ?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
// System note: cross-project ref
|
|
conn.execute(
|
|
"INSERT INTO notes (id, gitlab_id, discussion_id, project_id, is_system, body, created_at, updated_at, last_seen_at)
|
|
VALUES (44, 4004, 30, 1, 1, 'mentioned in issue other/project#999', ?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
1
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_refs_from_system_notes_integration() {
|
|
let conn = setup_test_db();
|
|
let project_id = seed_test_data(&conn);
|
|
|
|
let result = extract_refs_from_system_notes(&conn, project_id).unwrap();
|
|
|
|
assert_eq!(result.inserted, 2, "Two same-project refs should resolve");
|
|
assert_eq!(
|
|
result.skipped_unresolvable, 1,
|
|
"One cross-project ref should be unresolvable"
|
|
);
|
|
assert_eq!(
|
|
result.parse_failures, 1,
|
|
"One system note has no cross-ref pattern"
|
|
);
|
|
|
|
let ref_count: i64 = conn
|
|
.query_row(
|
|
"SELECT COUNT(*) FROM entity_references WHERE project_id = ?1 AND source_method = 'note_parse'",
|
|
[project_id],
|
|
|row| row.get(0),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(ref_count, 3, "Should have 3 entity_references rows total");
|
|
|
|
let unresolved_count: i64 = conn
|
|
.query_row(
|
|
"SELECT COUNT(*) FROM entity_references WHERE target_entity_id IS NULL AND source_method = 'note_parse'",
|
|
[],
|
|
|row| row.get(0),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
unresolved_count, 1,
|
|
"Should have 1 unresolved cross-project ref"
|
|
);
|
|
|
|
let (path, iid): (String, i64) = conn
|
|
.query_row(
|
|
"SELECT target_project_path, target_entity_iid FROM entity_references WHERE target_entity_id IS NULL",
|
|
[],
|
|
|row| Ok((row.get(0)?, row.get(1)?)),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(path, "other/project");
|
|
assert_eq!(iid, 999);
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_refs_idempotent() {
|
|
let conn = setup_test_db();
|
|
let project_id = seed_test_data(&conn);
|
|
|
|
let result1 = extract_refs_from_system_notes(&conn, project_id).unwrap();
|
|
let result2 = extract_refs_from_system_notes(&conn, project_id).unwrap();
|
|
|
|
assert_eq!(result2.inserted, 0);
|
|
assert_eq!(result2.skipped_unresolvable, 0);
|
|
|
|
let total: i64 = conn
|
|
.query_row(
|
|
"SELECT COUNT(*) FROM entity_references WHERE source_method = 'note_parse'",
|
|
[],
|
|
|row| row.get(0),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
total,
|
|
(result1.inserted + result1.skipped_unresolvable) as i64
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_refs_empty_project() {
|
|
let conn = setup_test_db();
|
|
let result = extract_refs_from_system_notes(&conn, 999).unwrap();
|
|
assert_eq!(result.inserted, 0);
|
|
assert_eq!(result.skipped_unresolvable, 0);
|
|
assert_eq!(result.parse_failures, 0);
|
|
}
|
|
|
|
// --- Integration tests: description extraction ---
|
|
|
|
#[test]
|
|
fn test_extract_refs_from_descriptions_issue() {
|
|
let conn = setup_test_db();
|
|
let now = now_ms();
|
|
|
|
conn.execute(
|
|
"INSERT INTO projects (id, gitlab_project_id, path_with_namespace, web_url, created_at, updated_at)
|
|
VALUES (1, 100, 'vs/typescript-code', 'https://gitlab.com/vs/typescript-code', ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
// Issue with MR reference in description
|
|
conn.execute(
|
|
"INSERT INTO issues (id, gitlab_id, project_id, iid, title, state, description, created_at, updated_at, last_seen_at)
|
|
VALUES (10, 1000, 1, 3537, 'Test Issue', 'opened',
|
|
'Related to https://gitlab.com/vs/typescript-code/-/merge_requests/3548',
|
|
?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
// The target MR so it resolves
|
|
conn.execute(
|
|
"INSERT INTO merge_requests (id, gitlab_id, project_id, iid, title, state, source_branch, target_branch, author_username, created_at, updated_at, last_seen_at)
|
|
VALUES (20, 2000, 1, 3548, 'Fix MR', 'merged', 'fix', 'main', 'dev', ?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
let result = extract_refs_from_descriptions(&conn, 1).unwrap();
|
|
|
|
assert_eq!(result.inserted, 1, "Should insert 1 description ref");
|
|
assert_eq!(result.skipped_unresolvable, 0);
|
|
|
|
let method: String = conn
|
|
.query_row(
|
|
"SELECT source_method FROM entity_references WHERE project_id = 1",
|
|
[],
|
|
|row| row.get(0),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(method, "description_parse");
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_refs_from_descriptions_mr() {
|
|
let conn = setup_test_db();
|
|
let now = now_ms();
|
|
|
|
conn.execute(
|
|
"INSERT INTO projects (id, gitlab_project_id, path_with_namespace, web_url, created_at, updated_at)
|
|
VALUES (1, 100, 'vs/typescript-code', 'https://gitlab.com/vs/typescript-code', ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
conn.execute(
|
|
"INSERT INTO issues (id, gitlab_id, project_id, iid, title, state, created_at, updated_at, last_seen_at)
|
|
VALUES (10, 1000, 1, 100, 'Target Issue', 'opened', ?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
conn.execute(
|
|
"INSERT INTO merge_requests (id, gitlab_id, project_id, iid, title, state, source_branch, target_branch, author_username, description, created_at, updated_at, last_seen_at)
|
|
VALUES (20, 2000, 1, 200, 'Fixing MR', 'merged', 'fix', 'main', 'dev',
|
|
'Fixes https://gitlab.com/vs/typescript-code/-/issues/100',
|
|
?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
let result = extract_refs_from_descriptions(&conn, 1).unwrap();
|
|
|
|
assert_eq!(result.inserted, 1);
|
|
|
|
let (src_type, tgt_type): (String, String) = conn
|
|
.query_row(
|
|
"SELECT source_entity_type, target_entity_type FROM entity_references WHERE project_id = 1",
|
|
[],
|
|
|row| Ok((row.get(0)?, row.get(1)?)),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(src_type, "merge_request");
|
|
assert_eq!(tgt_type, "issue");
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_refs_from_descriptions_idempotent() {
|
|
let conn = setup_test_db();
|
|
let now = now_ms();
|
|
|
|
conn.execute(
|
|
"INSERT INTO projects (id, gitlab_project_id, path_with_namespace, web_url, created_at, updated_at)
|
|
VALUES (1, 100, 'vs/code', 'https://gitlab.com/vs/code', ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
conn.execute(
|
|
"INSERT INTO issues (id, gitlab_id, project_id, iid, title, state, description, created_at, updated_at, last_seen_at)
|
|
VALUES (10, 1000, 1, 1, 'Issue', 'opened',
|
|
'See https://gitlab.com/vs/code/-/merge_requests/2', ?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
conn.execute(
|
|
"INSERT INTO merge_requests (id, gitlab_id, project_id, iid, title, state, source_branch, target_branch, author_username, created_at, updated_at, last_seen_at)
|
|
VALUES (20, 2000, 1, 2, 'MR', 'opened', 'x', 'main', 'dev', ?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
let r1 = extract_refs_from_descriptions(&conn, 1).unwrap();
|
|
assert_eq!(r1.inserted, 1);
|
|
|
|
let r2 = extract_refs_from_descriptions(&conn, 1).unwrap();
|
|
assert_eq!(r2.inserted, 0, "Second run should insert 0 (idempotent)");
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_refs_from_descriptions_cross_project_unresolved() {
|
|
let conn = setup_test_db();
|
|
let now = now_ms();
|
|
|
|
conn.execute(
|
|
"INSERT INTO projects (id, gitlab_project_id, path_with_namespace, web_url, created_at, updated_at)
|
|
VALUES (1, 100, 'vs/typescript-code', 'https://gitlab.com/vs/typescript-code', ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
conn.execute(
|
|
"INSERT INTO issues (id, gitlab_id, project_id, iid, title, state, description, created_at, updated_at, last_seen_at)
|
|
VALUES (10, 1000, 1, 1, 'Issue', 'opened',
|
|
'See https://gitlab.com/vs/other-project/-/merge_requests/99', ?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
let result = extract_refs_from_descriptions(&conn, 1).unwrap();
|
|
|
|
assert_eq!(result.inserted, 0);
|
|
assert_eq!(
|
|
result.skipped_unresolvable, 1,
|
|
"Cross-project ref with no matching project should be unresolvable"
|
|
);
|
|
|
|
let (path, iid): (String, i64) = conn
|
|
.query_row(
|
|
"SELECT target_project_path, target_entity_iid FROM entity_references WHERE target_entity_id IS NULL",
|
|
[],
|
|
|row| Ok((row.get(0)?, row.get(1)?)),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(path, "vs/other-project");
|
|
assert_eq!(iid, 99);
|
|
}
|
|
|
|
// --- Integration tests: user note extraction ---
|
|
|
|
#[test]
|
|
fn test_extract_refs_from_user_notes_with_url() {
|
|
let conn = setup_test_db();
|
|
let now = now_ms();
|
|
|
|
conn.execute(
|
|
"INSERT INTO projects (id, gitlab_project_id, path_with_namespace, web_url, created_at, updated_at)
|
|
VALUES (1, 100, 'vs/code', 'https://gitlab.com/vs/code', ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
conn.execute(
|
|
"INSERT INTO issues (id, gitlab_id, project_id, iid, title, state, created_at, updated_at, last_seen_at)
|
|
VALUES (10, 1000, 1, 50, 'Source Issue', 'opened', ?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
conn.execute(
|
|
"INSERT INTO merge_requests (id, gitlab_id, project_id, iid, title, state, source_branch, target_branch, author_username, created_at, updated_at, last_seen_at)
|
|
VALUES (20, 2000, 1, 60, 'Target MR', 'opened', 'x', 'main', 'dev', ?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
conn.execute(
|
|
"INSERT INTO discussions (id, gitlab_discussion_id, project_id, issue_id, noteable_type, last_seen_at)
|
|
VALUES (30, 'disc-user', 1, 10, 'Issue', ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
// User note with a URL
|
|
conn.execute(
|
|
"INSERT INTO notes (id, gitlab_id, discussion_id, project_id, is_system, body, created_at, updated_at, last_seen_at)
|
|
VALUES (40, 4000, 30, 1, 0,
|
|
'This is related to https://gitlab.com/vs/code/-/merge_requests/60',
|
|
?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
let result = extract_refs_from_user_notes(&conn, 1).unwrap();
|
|
|
|
assert_eq!(result.inserted, 1);
|
|
|
|
let method: String = conn
|
|
.query_row(
|
|
"SELECT source_method FROM entity_references WHERE project_id = 1",
|
|
[],
|
|
|row| row.get(0),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(method, "note_parse");
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_refs_from_user_notes_no_system_note_patterns() {
|
|
let conn = setup_test_db();
|
|
let now = now_ms();
|
|
|
|
conn.execute(
|
|
"INSERT INTO projects (id, gitlab_project_id, path_with_namespace, web_url, created_at, updated_at)
|
|
VALUES (1, 100, 'vs/code', 'https://gitlab.com/vs/code', ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
conn.execute(
|
|
"INSERT INTO issues (id, gitlab_id, project_id, iid, title, state, created_at, updated_at, last_seen_at)
|
|
VALUES (10, 1000, 1, 50, 'Source', 'opened', ?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
conn.execute(
|
|
"INSERT INTO merge_requests (id, gitlab_id, project_id, iid, title, state, source_branch, target_branch, author_username, created_at, updated_at, last_seen_at)
|
|
VALUES (20, 2000, 1, 999, 'Target', 'opened', 'x', 'main', 'dev', ?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
conn.execute(
|
|
"INSERT INTO discussions (id, gitlab_discussion_id, project_id, issue_id, noteable_type, last_seen_at)
|
|
VALUES (30, 'disc-x', 1, 10, 'Issue', ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
// User note with system-note-like text but no URL — should NOT extract
|
|
// (user notes only use URL parsing, not system note pattern matching)
|
|
conn.execute(
|
|
"INSERT INTO notes (id, gitlab_id, discussion_id, project_id, is_system, body, created_at, updated_at, last_seen_at)
|
|
VALUES (40, 4000, 30, 1, 0, 'mentioned in merge request !999', ?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
let result = extract_refs_from_user_notes(&conn, 1).unwrap();
|
|
|
|
assert_eq!(
|
|
result.inserted, 0,
|
|
"User notes should only parse URLs, not system note patterns"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_refs_from_user_notes_idempotent() {
|
|
let conn = setup_test_db();
|
|
let now = now_ms();
|
|
|
|
conn.execute(
|
|
"INSERT INTO projects (id, gitlab_project_id, path_with_namespace, web_url, created_at, updated_at)
|
|
VALUES (1, 100, 'vs/code', 'https://gitlab.com/vs/code', ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
conn.execute(
|
|
"INSERT INTO issues (id, gitlab_id, project_id, iid, title, state, created_at, updated_at, last_seen_at)
|
|
VALUES (10, 1000, 1, 1, 'Src', 'opened', ?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
conn.execute(
|
|
"INSERT INTO merge_requests (id, gitlab_id, project_id, iid, title, state, source_branch, target_branch, author_username, created_at, updated_at, last_seen_at)
|
|
VALUES (20, 2000, 1, 2, 'Tgt', 'opened', 'x', 'main', 'dev', ?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
conn.execute(
|
|
"INSERT INTO discussions (id, gitlab_discussion_id, project_id, issue_id, noteable_type, last_seen_at)
|
|
VALUES (30, 'disc-y', 1, 10, 'Issue', ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
conn.execute(
|
|
"INSERT INTO notes (id, gitlab_id, discussion_id, project_id, is_system, body, created_at, updated_at, last_seen_at)
|
|
VALUES (40, 4000, 30, 1, 0,
|
|
'See https://gitlab.com/vs/code/-/merge_requests/2', ?1, ?1, ?1)",
|
|
[now],
|
|
)
|
|
.unwrap();
|
|
|
|
let r1 = extract_refs_from_user_notes(&conn, 1).unwrap();
|
|
assert_eq!(r1.inserted, 1);
|
|
|
|
let r2 = extract_refs_from_user_notes(&conn, 1).unwrap();
|
|
assert_eq!(r2.inserted, 0, "Second extraction should be idempotent");
|
|
}
|