use gitea_pr_review::gitea::dto::*; use gitea_pr_review::normalize::normalize_bundle; #[test] fn normalize_groups_replies_and_sorts_threads_by_time() { let bundle = PullBundleDto { pull: PullDto { number: 42, title: "Fix parser".into(), state: "open".into(), body: Some("desc".into()), user: UserDto { login: "alice".into(), }, base: PullBranchDto { ref_name: "main".into(), }, head: PullBranchDto { ref_name: "feature/x".into(), }, created_at: "2026-04-08T10:00:00Z".into(), updated_at: "2026-04-08T11:00:00Z".into(), merged_at: None, additions: Some(99), deletions: Some(33), changed_files: Some(7), }, reviews: vec![ ReviewDto { id: 8, state: "APPROVED".into(), user: UserDto { login: "carol".into(), }, submitted_at: Some("2026-04-08T13:00:00Z".into()), }, ReviewDto { id: 7, state: "COMMENT".into(), user: UserDto { login: "bob".into(), }, submitted_at: Some("2026-04-08T12:00:00Z".into()), }, ], comments: vec![ ReviewCommentDto { id: 2, body: "reply".into(), created_at: "2026-04-08T12:02:00Z".into(), updated_at: Some("2026-04-08T12:03:00Z".into()), user: UserDto { login: "bob".into(), }, path: Some("src/main.rs".into()), line: Some(10), pull_request_review_id: Some(7), in_reply_to: Some(1), original_position: Some(10), position: Some(10), commit_id: Some("abc123".into()), original_commit_id: Some("abc123".into()), diff_hunk: Some("@@ -1 +1 @@".into()), }, ReviewCommentDto { id: 1, body: "root".into(), created_at: "2026-04-08T12:01:00Z".into(), updated_at: Some("2026-04-08T12:02:00Z".into()), user: UserDto { login: "bob".into(), }, path: Some("src/main.rs".into()), line: Some(10), pull_request_review_id: Some(7), in_reply_to: None, original_position: Some(10), position: Some(10), commit_id: Some("abc123".into()), original_commit_id: Some("abc123".into()), diff_hunk: Some("@@ -1 +1 @@".into()), }, ReviewCommentDto { id: 3, body: "other thread".into(), created_at: "2026-04-08T11:59:00Z".into(), updated_at: None, user: UserDto { login: "dave".into(), }, path: Some("src/lib.rs".into()), line: Some(22), pull_request_review_id: Some(8), in_reply_to: None, original_position: Some(22), position: Some(22), commit_id: Some("def456".into()), original_commit_id: Some("def456".into()), diff_hunk: Some("@@ -2 +2 @@".into()), }, ], commits: vec![ CommitDto { sha: "bbbbbbbccccccc".into(), commit: RepoCommitDto { message: "fix: later commit\n\nbody".into(), author: Some(CommitUserDto { name: "Bob".into(), date: "2026-04-08T12:30:00Z".into(), }), }, }, CommitDto { sha: "aaaaaaabbbbbbb".into(), commit: RepoCommitDto { message: "feat: earlier commit".into(), author: Some(CommitUserDto { name: "Alice".into(), date: "2026-04-08T11:30:00Z".into(), }), }, }, ], files: vec![ ChangedFileDto { filename: "src/main.rs".into(), additions: 5, deletions: 1, changes: Some(6), status: Some("modified".into()), previous_filename: None, }, ChangedFileDto { filename: "src/lib.rs".into(), additions: 3, deletions: 2, changes: Some(5), status: Some("modified".into()), previous_filename: None, }, ], }; let doc = normalize_bundle("org/repo", "2026-04-08T12:34:56Z".into(), bundle); assert_eq!(doc.meta.repo, "org/repo"); assert_eq!(doc.meta.pr_index, 42); assert_eq!(doc.meta.fetched_at.as_deref(), Some("2026-04-08T12:34:56Z")); assert_eq!(doc.meta.base_branch, "main"); assert_eq!(doc.meta.head_branch, "feature/x"); assert_eq!(doc.commits.len(), 2); assert_eq!(doc.commits[0].title, "feat: earlier commit"); assert_eq!(doc.commits[0].author, "Alice"); assert_eq!(doc.commits[1].title, "fix: later commit"); assert_eq!(doc.reviews.len(), 2); assert_eq!(doc.reviews[0].id, 7); assert_eq!(doc.reviews[1].id, 8); assert_eq!(doc.diff_stat.files_changed, 2); assert_eq!(doc.diff_stat.additions, 8); assert_eq!(doc.diff_stat.deletions, 3); assert_eq!(doc.diff_stat.files[0].path, "src/main.rs"); assert_eq!(doc.diff_stat.files[1].path, "src/lib.rs"); assert_eq!(doc.threads.len(), 2); assert_eq!(doc.threads[0].root_comment.id, 3); assert_eq!(doc.threads[0].file_path.as_deref(), Some("src/lib.rs")); assert_eq!(doc.threads[0].line, Some(22)); assert_eq!(doc.threads[1].root_comment.id, 1); assert_eq!(doc.threads[1].replies.len(), 1); assert_eq!(doc.threads[1].replies[0].id, 2); assert_eq!(doc.threads[1].file_path.as_deref(), Some("src/main.rs")); assert_eq!(doc.threads[1].line, Some(10)); } #[test] fn normalize_does_not_merge_unrelated_comments_on_same_file() { let bundle = PullBundleDto { pull: PullDto { number: 100, title: "T".into(), state: "open".into(), body: None, user: UserDto { login: "alice".into(), }, base: PullBranchDto { ref_name: "main".into(), }, head: PullBranchDto { ref_name: "feature/y".into(), }, created_at: "2026-04-08T10:00:00Z".into(), updated_at: "2026-04-08T10:00:00Z".into(), merged_at: None, additions: None, deletions: None, changed_files: None, }, reviews: vec![], comments: vec![ ReviewCommentDto { id: 10, body: "first line comment".into(), created_at: "2026-04-08T12:01:00Z".into(), updated_at: None, user: UserDto { login: "bob".into(), }, path: Some("src/main.rs".into()), line: Some(10), pull_request_review_id: Some(1), in_reply_to: None, original_position: Some(10), position: Some(10), commit_id: Some("abc123".into()), original_commit_id: Some("abc123".into()), diff_hunk: None, }, ReviewCommentDto { id: 11, body: "second line comment".into(), created_at: "2026-04-08T12:02:00Z".into(), updated_at: None, user: UserDto { login: "bob".into(), }, path: Some("src/main.rs".into()), line: Some(20), pull_request_review_id: Some(1), in_reply_to: None, original_position: Some(20), position: Some(20), commit_id: Some("abc123".into()), original_commit_id: Some("abc123".into()), diff_hunk: None, }, ], commits: vec![], files: vec![], }; let doc = normalize_bundle("org/repo", "2026-04-08T12:34:56Z".into(), bundle); assert_eq!(doc.threads.len(), 2); assert_eq!(doc.threads[0].root_comment.id, 10); assert_eq!(doc.threads[0].replies.len(), 0); assert_eq!(doc.threads[1].root_comment.id, 11); assert_eq!(doc.threads[1].replies.len(), 0); } #[test] fn normalize_uses_non_zero_position_when_line_is_missing() { let bundle = PullBundleDto { pull: PullDto { number: 101, title: "T".into(), state: "open".into(), body: None, user: UserDto { login: "alice".into(), }, base: PullBranchDto { ref_name: "main".into(), }, head: PullBranchDto { ref_name: "feature/z".into(), }, created_at: "2026-04-08T10:00:00Z".into(), updated_at: "2026-04-08T10:00:00Z".into(), merged_at: None, additions: None, deletions: None, changed_files: None, }, reviews: vec![], comments: vec![ReviewCommentDto { id: 1001, body: "position based comment".into(), created_at: "2026-04-08T12:00:00Z".into(), updated_at: None, user: UserDto { login: "bob".into(), }, path: Some("src/main.rs".into()), line: None, pull_request_review_id: Some(1), in_reply_to: None, original_position: Some(42), position: Some(41), commit_id: Some("abc123".into()), original_commit_id: Some("abc123".into()), diff_hunk: None, }], commits: vec![], files: vec![], }; let doc = normalize_bundle("org/repo", "2026-04-08T12:34:56Z".into(), bundle); assert_eq!(doc.threads.len(), 1); assert_eq!(doc.threads[0].root_comment.path.as_deref(), Some("src/main.rs")); assert_eq!(doc.threads[0].root_comment.line, Some(41)); }