Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 229 additions & 0 deletions crates/travelagent-forge-github/tests/fixture_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

use travelagent_core::forge::*;
use travelagent_core::model::FileStatus;
use travelagent_forge_github::GitHubForge;

fn load_fixture(name: &str) -> String {
std::fs::read_to_string(format!("tests/fixtures/{name}")).unwrap()
}

fn ripgrep_pr_id() -> PrId {
PrId {
owner: "BurntSushi".into(),
repo: "ripgrep".into(),
number: 2900,
}
}

fn ruff_pr_id() -> PrId {
PrId {
owner: "astral-sh".into(),
repo: "ruff".into(),
number: 16000,
}
}

async fn setup() -> (MockServer, GitHubForge) {
let server = MockServer::start().await;
let forge = GitHubForge::with_token(&server.uri(), "test-token".into()).unwrap();
(server, forge)
}

#[tokio::test]
async fn ripgrep_pr_metadata_parsed_correctly() {
let (server, forge) = setup().await;
let body: serde_json::Value =
serde_json::from_str(&load_fixture("ripgrep_2900_pr.json")).unwrap();

Mock::given(method("GET"))
.and(path("/repos/BurntSushi/ripgrep/pulls/2900"))
.respond_with(ResponseTemplate::new(200).set_body_json(&body))
.mount(&server)
.await;

let pr = forge.get_pr(&ripgrep_pr_id()).await.unwrap();
assert_eq!(pr.title, "globset: add matches_all method");
assert_eq!(pr.author, "tmccombs");
assert_eq!(pr.state, PrState::Closed);
assert_eq!(pr.base_branch, "master");
assert_eq!(pr.head_branch, "matches-all");
assert!(!pr.is_draft);
}

#[tokio::test]
async fn ripgrep_commits_parsed_correctly() {
let (server, forge) = setup().await;
let body: serde_json::Value =
serde_json::from_str(&load_fixture("ripgrep_2900_commits.json")).unwrap();

Mock::given(method("GET"))
.and(path("/repos/BurntSushi/ripgrep/pulls/2900/commits"))
.respond_with(ResponseTemplate::new(200).set_body_json(&body))
.mount(&server)
.await;

let commits = forge.get_pr_commits(&ripgrep_pr_id()).await.unwrap();
assert_eq!(commits.len(), 2);
assert_eq!(commits[0].id, "3c17c22ef64e78064d8c621b118d7cdb3652fa76");
assert_eq!(commits[0].short_id, "3c17c22");
assert_eq!(commits[0].summary, "globset: add matches_all method");
assert_eq!(commits[0].author, "tmccombs");
assert_eq!(commits[1].summary, "fixup! globset: add matches_all method");
}

#[tokio::test]
async fn ripgrep_files_with_patches_parsed_correctly() {
let (server, forge) = setup().await;
let body: serde_json::Value =
serde_json::from_str(&load_fixture("ripgrep_2900_files.json")).unwrap();

Mock::given(method("GET"))
.and(path("/repos/BurntSushi/ripgrep/pulls/2900/files"))
.respond_with(ResponseTemplate::new(200).set_body_json(&body))
.mount(&server)
.await;

let files = forge.get_pr_files(&ripgrep_pr_id()).await.unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].status, FileStatus::Modified);
assert_eq!(
files[0].new_path,
Some(std::path::PathBuf::from("crates/globset/src/lib.rs"))
);
// The patch has 4 hunks (4 @@ sections in the fixture)
assert!(
!files[0].hunks.is_empty(),
"Expected hunks to be parsed from the patch"
);
// Verify line numbers from the first hunk (@@ -351,6 +351,43 @@)
let first_hunk = &files[0].hunks[0];
assert_eq!(first_hunk.old_start, 351);
assert_eq!(first_hunk.new_start, 351);
}

#[tokio::test]
async fn ripgrep_review_comments_parsed_correctly() {
let (server, forge) = setup().await;
let comments_body: serde_json::Value =
serde_json::from_str(&load_fixture("ripgrep_2900_comments.json")).unwrap();

Mock::given(method("GET"))
.and(path("/repos/BurntSushi/ripgrep/pulls/2900/comments"))
.respond_with(ResponseTemplate::new(200).set_body_json(&comments_body))
.mount(&server)
.await;

// Also mock empty issue comments
Mock::given(method("GET"))
.and(path("/repos/BurntSushi/ripgrep/issues/2900/comments"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
.mount(&server)
.await;

let comments = forge.get_comments(&ripgrep_pr_id()).await.unwrap();
assert_eq!(comments.len(), 3);

// First comment by BurntSushi
assert_eq!(comments[0].author, "BurntSushi");
assert_eq!(comments[0].path, Some("crates/globset/src/lib.rs".into()));
assert_eq!(comments[0].in_reply_to, None);

// Second comment also by BurntSushi, different thread
assert_eq!(comments[1].author, "BurntSushi");
assert_eq!(comments[1].in_reply_to, None);

// Third comment is a reply to the second (in_reply_to_id = 1766859217)
assert_eq!(comments[2].author, "BurntSushi");
assert_eq!(comments[2].in_reply_to, Some(1766859217));
}

#[tokio::test]
async fn ruff_medium_pr_13_files_parsed_correctly() {
let (server, forge) = setup().await;
let body: serde_json::Value =
serde_json::from_str(&load_fixture("ruff_16000_files.json")).unwrap();

Mock::given(method("GET"))
.and(path("/repos/astral-sh/ruff/pulls/16000/files"))
.respond_with(ResponseTemplate::new(200).set_body_json(&body))
.mount(&server)
.await;

let files = forge.get_pr_files(&ruff_pr_id()).await.unwrap();
assert_eq!(files.len(), 13);

// Check statuses: 12 modified + 1 added
let added_count = files
.iter()
.filter(|f| f.status == FileStatus::Added)
.count();
let modified_count = files
.iter()
.filter(|f| f.status == FileStatus::Modified)
.count();
assert_eq!(added_count, 1);
assert_eq!(modified_count, 12);

// Verify the added file
let added = files
.iter()
.find(|f| f.status == FileStatus::Added)
.unwrap();
assert_eq!(
added.new_path,
Some(std::path::PathBuf::from(
"crates/red_knot_project/src/metadata/settings.rs"
))
);
assert!(added.old_path.is_none());

// Verify patches are parsed into hunks
for file in &files {
assert!(
!file.hunks.is_empty(),
"File {:?} should have hunks parsed from patch",
file.new_path
);
}
}

#[tokio::test]
async fn ruff_3_review_comments_with_replies() {
let (server, forge) = setup().await;
let comments_body: serde_json::Value =
serde_json::from_str(&load_fixture("ruff_16000_comments.json")).unwrap();

Mock::given(method("GET"))
.and(path("/repos/astral-sh/ruff/pulls/16000/comments"))
.respond_with(ResponseTemplate::new(200).set_body_json(&comments_body))
.mount(&server)
.await;

Mock::given(method("GET"))
.and(path("/repos/astral-sh/ruff/issues/16000/comments"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
.mount(&server)
.await;

let comments = forge.get_comments(&ruff_pr_id()).await.unwrap();
assert_eq!(comments.len(), 3);

// First comment starts a thread (no in_reply_to)
assert_eq!(comments[0].author, "dhruvmanila");
assert_eq!(comments[0].id, 1946208119);
assert_eq!(comments[0].in_reply_to, None);
assert_eq!(
comments[0].path,
Some("crates/red_knot_project/src/metadata/settings.rs".into())
);

// Second comment replies to the first
assert_eq!(comments[1].author, "MichaReiser");
assert_eq!(comments[1].in_reply_to, Some(1946208119));

// Third comment also replies to the first
assert_eq!(comments[2].author, "dhruvmanila");
assert_eq!(comments[2].in_reply_to, Some(1946208119));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/comments/1766848243","pull_request_review_id":2315570568,"id":1766848243,"node_id":"PRRC_kwDOAzJbyc5pT_bz","diff_hunk":"@@ -351,6 +351,27 @@ impl GlobSet {\n false\n }\n \n+ /// Returns true if ALL globs in this set match the path given.","path":"crates/globset/src/lib.rs","commit_id":"c26831ce39d3cfa36db8f913998ab8b52be4d38c","original_commit_id":"3c17c22ef64e78064d8c621b118d7cdb3652fa76","user":{"login":"BurntSushi","id":456674,"node_id":"MDQ6VXNlcjQ1NjY3NA==","avatar_url":"https://avatars.githubusercontent.com/u/456674?v=4","gravatar_id":"","url":"https://api.github.com/users/BurntSushi","html_url":"https://github.com/BurntSushi","followers_url":"https://api.github.com/users/BurntSushi/followers","following_url":"https://api.github.com/users/BurntSushi/following{/other_user}","gists_url":"https://api.github.com/users/BurntSushi/gists{/gist_id}","starred_url":"https://api.github.com/users/BurntSushi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/BurntSushi/subscriptions","organizations_url":"https://api.github.com/users/BurntSushi/orgs","repos_url":"https://api.github.com/users/BurntSushi/repos","events_url":"https://api.github.com/users/BurntSushi/events{/privacy}","received_events_url":"https://api.github.com/users/BurntSushi/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"I don't think we need to use caps for `ALL` here. Just \"all\" is fine.","created_at":"2024-09-19T13:34:43Z","updated_at":"2024-09-19T13:41:27Z","html_url":"https://github.com/BurntSushi/ripgrep/pull/2900#discussion_r1766848243","pull_request_url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/2900","_links":{"self":{"href":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/comments/1766848243"},"html":{"href":"https://github.com/BurntSushi/ripgrep/pull/2900#discussion_r1766848243"},"pull_request":{"href":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/2900"}},"reactions":{"url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/comments/1766848243/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":null,"original_line":354,"side":"RIGHT","author_association":"OWNER","original_position":4,"position":1,"subject_type":"line"},{"url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/comments/1766859217","pull_request_review_id":2315570568,"id":1766859217,"node_id":"PRRC_kwDOAzJbyc5pUCHR","diff_hunk":"@@ -351,6 +351,27 @@ impl GlobSet {\n false\n }\n \n+ /// Returns true if ALL globs in this set match the path given.\n+ pub fn matches_all<P: AsRef<Path>>(&self, path: P) -> bool {\n+ self.matches_all_candidate(&Candidate::new(path.as_ref()))\n+ }\n+\n+ /// Returns ture if all globs in this set match the path given.\n+ ///\n+ /// This takes a Candidate as input, which can be used to amortize the\n+ /// cost of peparing a path for matching.\n+ ///\n+ /// This will return true if the set of globs is empty, as in that case all `0` of\n+ /// the globs will match.","path":"crates/globset/src/lib.rs","commit_id":"c26831ce39d3cfa36db8f913998ab8b52be4d38c","original_commit_id":"3c17c22ef64e78064d8c621b118d7cdb3652fa76","user":{"login":"BurntSushi","id":456674,"node_id":"MDQ6VXNlcjQ1NjY3NA==","avatar_url":"https://avatars.githubusercontent.com/u/456674?v=4","gravatar_id":"","url":"https://api.github.com/users/BurntSushi","html_url":"https://github.com/BurntSushi","followers_url":"https://api.github.com/users/BurntSushi/followers","following_url":"https://api.github.com/users/BurntSushi/following{/other_user}","gists_url":"https://api.github.com/users/BurntSushi/gists{/gist_id}","starred_url":"https://api.github.com/users/BurntSushi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/BurntSushi/subscriptions","organizations_url":"https://api.github.com/users/BurntSushi/orgs","repos_url":"https://api.github.com/users/BurntSushi/repos","events_url":"https://api.github.com/users/BurntSushi/events{/privacy}","received_events_url":"https://api.github.com/users/BurntSushi/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"Please wrap lines to 79 columns (inclusive).","created_at":"2024-09-19T13:39:26Z","updated_at":"2024-09-19T13:41:27Z","html_url":"https://github.com/BurntSushi/ripgrep/pull/2900#discussion_r1766859217","pull_request_url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/2900","_links":{"self":{"href":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/comments/1766859217"},"html":{"href":"https://github.com/BurntSushi/ripgrep/pull/2900#discussion_r1766859217"},"pull_request":{"href":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/2900"}},"reactions":{"url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/comments/1766859217/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":null,"original_line":365,"side":"RIGHT","author_association":"OWNER","original_position":15,"position":1,"subject_type":"line"},{"url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/comments/1766860858","pull_request_review_id":2315570568,"id":1766860858,"node_id":"PRRC_kwDOAzJbyc5pUCg6","diff_hunk":"@@ -351,6 +351,27 @@ impl GlobSet {\n false\n }\n \n+ /// Returns true if ALL globs in this set match the path given.\n+ pub fn matches_all<P: AsRef<Path>>(&self, path: P) -> bool {\n+ self.matches_all_candidate(&Candidate::new(path.as_ref()))\n+ }\n+\n+ /// Returns ture if all globs in this set match the path given.\n+ ///\n+ /// This takes a Candidate as input, which can be used to amortize the\n+ /// cost of peparing a path for matching.\n+ ///\n+ /// This will return true if the set of globs is empty, as in that case all `0` of\n+ /// the globs will match.","path":"crates/globset/src/lib.rs","commit_id":"c26831ce39d3cfa36db8f913998ab8b52be4d38c","original_commit_id":"3c17c22ef64e78064d8c621b118d7cdb3652fa76","user":{"login":"BurntSushi","id":456674,"node_id":"MDQ6VXNlcjQ1NjY3NA==","avatar_url":"https://avatars.githubusercontent.com/u/456674?v=4","gravatar_id":"","url":"https://api.github.com/users/BurntSushi","html_url":"https://github.com/BurntSushi","followers_url":"https://api.github.com/users/BurntSushi/followers","following_url":"https://api.github.com/users/BurntSushi/following{/other_user}","gists_url":"https://api.github.com/users/BurntSushi/gists{/gist_id}","starred_url":"https://api.github.com/users/BurntSushi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/BurntSushi/subscriptions","organizations_url":"https://api.github.com/users/BurntSushi/orgs","repos_url":"https://api.github.com/users/BurntSushi/repos","events_url":"https://api.github.com/users/BurntSushi/events{/privacy}","received_events_url":"https://api.github.com/users/BurntSushi/received_events","type":"User","user_view_type":"public","site_admin":false},"body":"Also, this note should be added to `matches_all` too.","created_at":"2024-09-19T13:39:52Z","updated_at":"2024-09-19T13:41:27Z","html_url":"https://github.com/BurntSushi/ripgrep/pull/2900#discussion_r1766860858","pull_request_url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/2900","_links":{"self":{"href":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/comments/1766860858"},"html":{"href":"https://github.com/BurntSushi/ripgrep/pull/2900#discussion_r1766860858"},"pull_request":{"href":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/2900"}},"reactions":{"url":"https://api.github.com/repos/BurntSushi/ripgrep/pulls/comments/1766860858/reactions","total_count":0,"+1":0,"-1":0,"laugh":0,"hooray":0,"confused":0,"heart":0,"rocket":0,"eyes":0},"start_line":null,"original_start_line":null,"start_side":null,"line":null,"original_line":365,"side":"RIGHT","in_reply_to_id":1766859217,"author_association":"OWNER","original_position":15,"position":1,"subject_type":"line"}]
Loading
Loading