Skip to content

Commit d795843

Browse files
author
Test User
committed
fix(merge-coordinator): raise list_open_prs limit from 50 to 300 Refs #2850
Gitea's default page size is 50 open PRs. With limit=50, any PR beyond position 50 was silently excluded from the evaluation loop, creating a predictable bypass vector for the contamination gate. Changes: - Extract OPEN_PRS_LIMIT const = 300 (Gitea max page size) in gitea.rs - Use OPEN_PRS_LIMIT in list_open_prs URL query string - Add three unit tests: limit > 50 assertion, limit <= 300 assertion, and 51-item deserialisation round-trip proving no artificial truncation - Add evaluator regression test: evaluate_one processes all 51 PRs with PR at position 51 receiving a correct Merge verdict
1 parent 24bc4f5 commit d795843

2 files changed

Lines changed: 63 additions & 2 deletions

File tree

crates/terraphim_merge_coordinator/src/evaluator.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,4 +161,24 @@ mod tests {
161161
assert_eq!(e.verdict, EvalVerdict::Merge);
162162
assert!(e.fixes_issues.is_empty());
163163
}
164+
165+
#[test]
166+
fn evaluate_one_processes_51_prs_without_truncation() {
167+
// Regression test for issue #2850: ensure the evaluation loop does not
168+
// impose an artificial cap at position 50. With list_open_prs returning
169+
// up to OPEN_PRS_LIMIT (300) items, all PRs must receive a verdict.
170+
let prs: Vec<PrSummary> = (1u64..=51)
171+
.map(|n| pr(n, &format!("Fixes #{n}"), true))
172+
.collect();
173+
let evaluations: Vec<_> = prs.iter().map(evaluate_one).collect();
174+
assert_eq!(
175+
evaluations.len(),
176+
51,
177+
"all 51 PRs must receive an evaluation verdict"
178+
);
179+
// Spot-check: the PR at position 51 gets Merge (it is mergeable, clean)
180+
let last = &evaluations[50];
181+
assert_eq!(last.pr_index, 51);
182+
assert_eq!(last.verdict, EvalVerdict::Merge);
183+
}
164184
}

crates/terraphim_merge_coordinator/src/gitea.rs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ use crate::types::MergeCoordinatorError;
1313

1414
const RETRY_DELAYS_SECS: &[u64] = &[1, 2, 4];
1515

16+
/// Maximum number of open PRs fetched per `list_open_prs` call.
17+
///
18+
/// Gitea's hard cap is 50 when no explicit limit is set; the API accepts up
19+
/// to 300. Using 300 ensures PRs beyond position 50 are not silently skipped
20+
/// by the evaluation loop (issue #2850).
21+
const OPEN_PRS_LIMIT: u32 = 300;
22+
1623
/// Minimal Gitea API client. Caller supplies the API token via env or
1724
/// secure storage; it is never written to logs.
1825
pub struct GiteaClient {
@@ -52,8 +59,8 @@ impl GiteaClient {
5259
repo: &str,
5360
) -> Result<Vec<PrSummary>, MergeCoordinatorError> {
5461
let url = format!(
55-
"{}/api/v1/repos/{}/{}/pulls?state=open&limit=50",
56-
self.base_url, owner, repo
62+
"{}/api/v1/repos/{}/{}/pulls?state=open&limit={}",
63+
self.base_url, owner, repo, OPEN_PRS_LIMIT
5764
);
5865
let resp = self.get_with_retry(&url).await?;
5966
let prs = resp
@@ -197,4 +204,38 @@ mod tests {
197204
fn retry_delays_are_one_two_four_seconds() {
198205
assert_eq!(RETRY_DELAYS_SECS, &[1u64, 2, 4]);
199206
}
207+
208+
#[test]
209+
fn open_prs_limit_exceeds_gitea_default_of_50() {
210+
assert!(
211+
OPEN_PRS_LIMIT > 50,
212+
"OPEN_PRS_LIMIT must exceed 50 so PRs beyond position 50 are not silently dropped"
213+
);
214+
}
215+
216+
#[test]
217+
fn open_prs_limit_within_gitea_max_page_size() {
218+
assert!(
219+
OPEN_PRS_LIMIT <= 300,
220+
"Gitea max page size is 300; limit must not exceed it"
221+
);
222+
}
223+
224+
#[test]
225+
fn pr_summary_vec_of_51_items_deserialises() {
226+
// Construct JSON array with 51 items to verify no artificial truncation
227+
// happens at the deserialization layer.
228+
let items: String = (1u64..=51)
229+
.map(|n| format!(r#"{{"number":{n},"title":"PR {n}","state":"open"}}"#))
230+
.collect::<Vec<_>>()
231+
.join(",");
232+
let json = format!("[{items}]");
233+
let prs: Vec<PrSummary> = serde_json::from_str(&json).unwrap();
234+
assert_eq!(
235+
prs.len(),
236+
51,
237+
"all 51 PRs must be present after deserialisation"
238+
);
239+
assert_eq!(prs[50].number, 51, "PR at position 51 must be present");
240+
}
200241
}

0 commit comments

Comments
 (0)