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
5 changes: 2 additions & 3 deletions services/github/pulls/get_open_pull_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@


@handle_exceptions(default_return_value=[], raise_on_error=False)
def get_open_pull_requests(owner: str, repo: str, target_branch: str, token: str):
def get_open_pull_requests(owner: str, repo: str, token: str):
"""https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#list-pull-requests"""
url = f"{GITHUB_API_URL}/repos/{owner}/{repo}/pulls"
headers = create_headers(token=token)
pull_requests: list[dict] = []
page = 1

while True:
params = {
"base": target_branch,
params: dict[str, str | int] = {
"state": "open",
"per_page": PER_PAGE,
"page": page,
Expand Down
6 changes: 0 additions & 6 deletions services/github/pulls/test_get_open_pull_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ def test_get_open_pull_requests_success():
result = get_open_pull_requests(
owner="test-owner",
repo="test-repo",
target_branch="main",
token="test-token",
)

Expand Down Expand Up @@ -75,7 +74,6 @@ def test_get_open_pull_requests_pagination():
result = get_open_pull_requests(
owner="test-owner",
repo="test-repo",
target_branch="develop",
token="test-token",
)

Expand All @@ -92,7 +90,6 @@ def test_get_open_pull_requests_empty():
result = get_open_pull_requests(
owner="test-owner",
repo="test-repo",
target_branch="main",
token="test-token",
)

Expand All @@ -104,7 +101,6 @@ def test_get_open_pull_requests_error():
result = get_open_pull_requests(
owner="test-owner",
repo="test-repo",
target_branch="main",
token="test-token",
)

Expand Down Expand Up @@ -151,7 +147,6 @@ def test_get_open_pull_requests_filters_non_gitauto_prs():
result = get_open_pull_requests(
owner="test-owner",
repo="test-repo",
target_branch="main",
token="test-token",
)

Expand Down Expand Up @@ -193,7 +188,6 @@ def test_get_open_pull_requests_no_gitauto_prs():
result = get_open_pull_requests(
owner="test-owner",
repo="test-repo",
target_branch="main",
token="test-token",
)

Expand Down
4 changes: 1 addition & 3 deletions services/webhook/push_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ def handle_push(payload: PushWebhookPayload):

token = get_installation_access_token(installation_id=installation_id)

open_prs = get_open_pull_requests(
owner=owner_name, repo=repo_name, target_branch=target_branch, token=token
)
open_prs = get_open_pull_requests(owner=owner_name, repo=repo_name, token=token)

if not open_prs:
# No GitAuto PRs targeting this branch
Expand Down
16 changes: 11 additions & 5 deletions services/webhook/schedule_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,7 @@ def schedule_handler(event: EventBridgeSchedulerEvent):
)

# Get open PRs once before the loop
open_prs = get_open_pull_requests(
owner=owner_name, repo=repo_name, target_branch=target_branch, token=token
)
open_prs = get_open_pull_requests(owner=owner_name, repo=repo_name, token=token)

# Find the first suitable file from sorted list
target_item = None
Expand Down Expand Up @@ -284,8 +282,16 @@ def schedule_handler(event: EventBridgeSchedulerEvent):
continue

# Skip files that have open PRs (temporary, don't exclude)
if any(item_path in pr.get("title", "") for pr in open_prs):
logger.info("Skipping %s: has open PR", item_path)
matching_pr = next(
(pr for pr in open_prs if item_path in pr.get("title", "")), None
)
if matching_pr:
logger.info(
"Skipping %s: has open PR #%s (%s)",
item_path,
matching_pr.get("number"),
matching_pr.get("base", {}).get("ref"),
)
continue

# Final check: Use Claude AI to determine if this file should be tested (expensive, so run last)
Expand Down
8 changes: 4 additions & 4 deletions services/webhook/test_push_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def test_handle_push_no_open_prs_returns_early(
mock_get_repository.assert_called_once_with(owner_id=123, repo_id=456)
mock_get_token.assert_called_once_with(installation_id=789)
mock_get_open_prs.assert_called_once_with(
owner="test-owner", repo="test-repo", target_branch="main", token="test-token"
owner="test-owner", repo="test-repo", token="test-token"
)
mock_update_pr.assert_not_called()
mock_logger.info.assert_not_called()
Expand Down Expand Up @@ -212,7 +212,7 @@ def test_handle_push_local_main_to_remote_main_handled(
mock_get_repository.assert_called_once_with(owner_id=123, repo_id=456)
mock_get_token.assert_called_once_with(installation_id=789)
mock_get_open_prs.assert_called_once_with(
owner="test-owner", repo="test-repo", target_branch="main", token="test-token"
owner="test-owner", repo="test-repo", token="test-token"
)
assert mock_update_pr.call_count == 2
mock_update_pr.assert_any_call(
Expand Down Expand Up @@ -257,7 +257,7 @@ def test_handle_push_remote_feature_to_remote_main_handled(
mock_get_repository.assert_called_once_with(owner_id=123, repo_id=456)
mock_get_token.assert_called_once_with(installation_id=789)
mock_get_open_prs.assert_called_once_with(
owner="test-owner", repo="test-repo", target_branch="main", token="test-token"
owner="test-owner", repo="test-repo", token="test-token"
)
mock_update_pr.assert_called_once_with(
owner="test-owner", repo="test-repo", pr_number=5, token="test-token"
Expand Down Expand Up @@ -390,7 +390,7 @@ def test_handle_push_no_gitauto_prs_returns_early(
mock_get_repository.assert_called_once_with(owner_id=123, repo_id=456)
mock_get_token.assert_called_once_with(installation_id=789)
mock_get_open_prs.assert_called_once_with(
owner="test-owner", repo="test-repo", target_branch="main", token="test-token"
owner="test-owner", repo="test-repo", token="test-token"
)
mock_update_pr.assert_not_called()
mock_logger.info.assert_not_called()
174 changes: 174 additions & 0 deletions services/webhook/test_schedule_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,180 @@ def test_schedule_handler_prioritizes_zero_coverage_files(
assert "updated_at" not in coverage_record


@patch("services.webhook.schedule_handler.get_open_pull_requests")
@patch("services.webhook.schedule_handler.should_skip_test")
@patch("services.webhook.schedule_handler.is_schedule_paused")
@patch("services.webhook.schedule_handler.get_installation_access_token")
@patch("services.webhook.schedule_handler.get_repository")
@patch("services.webhook.schedule_handler.check_availability")
@patch("services.webhook.schedule_handler.get_default_branch")
@patch("services.webhook.schedule_handler.get_file_tree")
@patch("services.webhook.schedule_handler.get_all_coverages")
@patch("services.webhook.schedule_handler.get_raw_content")
def test_schedule_handler_skips_file_with_open_pr_on_different_branch(
mock_get_raw_content,
mock_get_all_coverages,
mock_get_file_tree,
mock_get_default_branch,
mock_check_availability,
mock_get_repository,
mock_get_token,
mock_is_paused,
mock_should_skip_test,
mock_get_open_pull_requests,
mock_event,
):
mock_get_token.return_value = "test-token"
mock_is_paused.return_value = False
mock_get_repository.return_value = {
"trigger_on_schedule": True,
"target_branch": "master",
}
mock_check_availability.return_value = {
"can_proceed": True,
"billing_type": "exception",
"requests_left": None,
"credit_balance_usd": 0,
"period_end_date": None,
"user_message": "",
"log_message": "Exception owner - unlimited access.",
}
mock_get_default_branch.return_value = ("master", None)
mock_get_file_tree.return_value = [
{"path": "src/app.php", "type": "blob", "size": 100},
]
mock_get_all_coverages.return_value = [
{
"id": 1,
"full_path": "src/app.php",
"owner_id": 123,
"repo_id": 456,
"branch_name": "master",
"created_by": "test-user",
"updated_by": "test-user",
"level": "file",
"file_size": 100,
"statement_coverage": 10.0,
"function_coverage": 50.0,
"branch_coverage": 100.0,
"line_coverage": 10.0,
"package_name": None,
"language": "php",
"uncovered_lines": None,
"uncovered_functions": None,
"uncovered_branches": None,
"created_at": "2024-01-01",
"updated_at": "2024-01-01",
"github_issue_url": None,
"is_excluded_from_testing": False,
},
]
mock_get_raw_content.return_value = "<?php function test() { return 1; }"
mock_should_skip_test.return_value = False
# Open PR targeting a DIFFERENT branch but for the same file
mock_get_open_pull_requests.return_value = [
{
"number": 100,
"title": "Schedule: Add unit tests to src/app.php",
"base": {"ref": "release/20260311"},
"user": {"login": "gitauto-ai[bot]"},
},
]

result = schedule_handler(mock_event)

assert result["status"] == "skipped"
assert "No suitable file found" in result["message"]


@patch("services.webhook.schedule_handler.get_open_pull_requests")
@patch("services.webhook.schedule_handler.should_skip_test")
@patch("services.webhook.schedule_handler.is_schedule_paused")
@patch("services.webhook.schedule_handler.get_installation_access_token")
@patch("services.webhook.schedule_handler.get_repository")
@patch("services.webhook.schedule_handler.check_availability")
@patch("services.webhook.schedule_handler.get_default_branch")
@patch("services.webhook.schedule_handler.get_file_tree")
@patch("services.webhook.schedule_handler.get_all_coverages")
@patch("services.webhook.schedule_handler.get_raw_content")
def test_schedule_handler_skips_file_with_open_pr_different_title_format(
mock_get_raw_content,
mock_get_all_coverages,
mock_get_file_tree,
mock_get_default_branch,
mock_check_availability,
mock_get_repository,
mock_get_token,
mock_is_paused,
mock_should_skip_test,
mock_get_open_pull_requests,
mock_event,
):
"""Dedup works even when the existing PR uses an old title format, because the
check is a file-path substring match, not an exact title match."""
mock_get_token.return_value = "test-token"
mock_is_paused.return_value = False
mock_get_repository.return_value = {
"trigger_on_schedule": True,
"target_branch": "master",
}
mock_check_availability.return_value = {
"can_proceed": True,
"billing_type": "exception",
"requests_left": None,
"credit_balance_usd": 0,
"period_end_date": None,
"user_message": "",
"log_message": "Exception owner - unlimited access.",
}
mock_get_default_branch.return_value = ("master", None)
mock_get_file_tree.return_value = [
{"path": "src/app.php", "type": "blob", "size": 100},
]
mock_get_all_coverages.return_value = [
{
"id": 1,
"full_path": "src/app.php",
"owner_id": 123,
"repo_id": 456,
"branch_name": "master",
"created_by": "test-user",
"updated_by": "test-user",
"level": "file",
"file_size": 100,
"statement_coverage": 10.0,
"function_coverage": 50.0,
"branch_coverage": 100.0,
"line_coverage": 10.0,
"package_name": None,
"language": "php",
"uncovered_lines": None,
"uncovered_functions": None,
"uncovered_branches": None,
"created_at": "2024-01-01",
"updated_at": "2024-01-01",
"github_issue_url": None,
"is_excluded_from_testing": False,
},
]
mock_get_raw_content.return_value = "<?php function test() { return 1; }"
mock_should_skip_test.return_value = False
# Old title format ("for uncovered code in") differs from current ("to"/"Achieve 100%")
mock_get_open_pull_requests.return_value = [
{
"number": 100,
"title": "Schedule: Add unit tests for uncovered code in src/app.php",
"base": {"ref": "release/20260311"},
"user": {"login": "gitauto-ai[bot]"},
},
]

result = schedule_handler(mock_event)

assert result["status"] == "skipped"
assert "No suitable file found" in result["message"]


@patch("services.webhook.schedule_handler.get_open_pull_requests")
@patch("services.webhook.schedule_handler.evaluate_condition")
@patch("services.webhook.schedule_handler.should_skip_test")
Expand Down