diff --git a/bounty-notes/issue-82-duplicate-approval.md b/bounty-notes/issue-82-duplicate-approval.md new file mode 100644 index 0000000..0db39ba --- /dev/null +++ b/bounty-notes/issue-82-duplicate-approval.md @@ -0,0 +1,37 @@ +# CodeBounty issue #82 duplicate-approval fixture + +Issue: https://github.com/CodeBountyOrg/BountyTestRepository/issues/82 + +## Snapshot + +- State: open +- Locked: false +- Assignees: none +- Labels: `💰 Bounty Available` +- Visible CodeBounty amount: `$10 USD` +- Amount source: CodeBounty bot comments +- Required PR syntax from bot announcement: `fixes #82` +- Duplicate approval evidence: two pending-approval comments were posted before two later CodeBounty bot announcements: + - Pending approval: https://github.com/CodeBountyOrg/BountyTestRepository/issues/82#issuecomment-2781231507 + - Pending approval: https://github.com/CodeBountyOrg/BountyTestRepository/issues/82#issuecomment-2781231508 + - Bounty announcement: https://github.com/CodeBountyOrg/BountyTestRepository/issues/82#issuecomment-2808231115 + - Bounty announcement: https://github.com/CodeBountyOrg/BountyTestRepository/issues/82#issuecomment-2808231684 +- Distinct local bounty IDs preserved: + - `http://localhost:3000/bounty/67f21306730fd07f850f2d19` + - `http://localhost:3000/bounty/67f21306730fd07f850f2d1d` +- Collision searches run immediately before this branch: `82`, `#82`, `fixes #82`, `issue 82`, `test new issue apr 6 1235am`, and `test issue 1235am`. +- Exact same-scope open PR count: `0`. + +## Payout boundary + +This is a submitted-visible CodeBounty action only. The bot announcement says a developer must submit an application through CodeBounty to be eligible, so this PR must not be treated as verified-payable or paid until platform/maintainer acceptance is confirmed. + +## Local validation + +```bash +python3 -m py_compile scripts/validate-codebounty-issue-82.py +python3 scripts/validate-codebounty-issue-82.py test-fixtures/codebounty-issue-82-duplicate-approval.json +git diff --check +``` + +The PR body should include `fixes #82` to satisfy the CodeBounty linkage rule. diff --git a/scripts/validate-codebounty-issue-82.py b/scripts/validate-codebounty-issue-82.py new file mode 100644 index 0000000..b0a80c0 --- /dev/null +++ b/scripts/validate-codebounty-issue-82.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Validate the CodeBounty issue #82 duplicate-approval fixture. + +The fixture records the live GitHub + CodeBounty snapshot used for this PR. +It preserves a specific platform edge case: two pending-approval comments +were followed by two CodeBounty bot announcement comments for the same issue +and $10 amount, with two local bounty IDs. The payout boundary stays explicit: +this remains submitted-visible only until the required CodeBounty application +and maintainer/platform acceptance are verified. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + + +EXPECTED_BOUNTY_LINKS = { + "http://localhost:3000/bounty/67f21306730fd07f850f2d19", + "http://localhost:3000/bounty/67f21306730fd07f850f2d1d", +} +EXPECTED_PENDING_COMMENTS = { + "https://github.com/CodeBountyOrg/BountyTestRepository/issues/82#issuecomment-2781231507", + "https://github.com/CodeBountyOrg/BountyTestRepository/issues/82#issuecomment-2781231508", +} +EXPECTED_ANNOUNCEMENT_COMMENTS = { + "https://github.com/CodeBountyOrg/BountyTestRepository/issues/82#issuecomment-2808231115", + "https://github.com/CodeBountyOrg/BountyTestRepository/issues/82#issuecomment-2808231684", +} + + +def fail(message: str) -> None: + raise SystemExit(f"issue-82 fixture invalid: {message}") + + +def main() -> None: + if len(sys.argv) != 2: + fail("usage: validate-codebounty-issue-82.py ") + + path = Path(sys.argv[1]) + if not path.is_file(): + fail(f"missing fixture: {path}") + + data = json.loads(path.read_text(encoding="utf-8")) + issue = data.get("issue") or {} + bounty = data.get("bounty") or {} + duplicate = data.get("duplicate_approval_snapshot") or {} + collision = data.get("collision_snapshot") or {} + expected_pr = data.get("expected_pr") or {} + + if issue.get("number") != 82: + fail("issue number must be 82") + if issue.get("state") != "open": + fail("issue must be open at snapshot time") + if issue.get("locked") is not False: + fail("locked must be false at snapshot time") + if issue.get("assignees") != []: + fail("issue must be unassigned at snapshot time") + labels = set(issue.get("labels", [])) + if "💰 Bounty Available" not in labels: + fail("bounty-available label missing from live snapshot") + if "testing" not in issue.get("body", "").lower(): + fail("issue body snapshot should preserve the original testing marker") + + if bounty.get("platform") != "CodeBounty": + fail("platform must be CodeBounty") + if bounty.get("visible_amount_usd") != 10: + fail("visible bounty amount must be $10") + if bounty.get("visible_amount_source") != "bot_comments": + fail("visible amount source must stay scoped to bot comments") + if bounty.get("pending_approval_comment_count") != 2: + fail("expected two pending-approval comments") + if bounty.get("announcement_comment_count") != 2: + fail("expected two CodeBounty bot announcement comments") + if set(bounty.get("pending_approval_comments", [])) != EXPECTED_PENDING_COMMENTS: + fail("pending-approval comment URLs must match the live snapshot") + if set(bounty.get("announcement_comments", [])) != EXPECTED_ANNOUNCEMENT_COMMENTS: + fail("announcement comment URLs must match the live snapshot") + if set(bounty.get("bounty_links", [])) != EXPECTED_BOUNTY_LINKS: + fail("bounty links should match both distinct local bounty IDs") + if bounty.get("required_pr_linkage", "").lower() != "fixes #82": + fail("required PR linkage must be `fixes #82`") + if bounty.get("developer_application_required") is not True: + fail("developer application requirement must stay explicit") + if bounty.get("verified_payable_at_snapshot") is not False: + fail("fixture must not claim verified-payable payout") + + if duplicate.get("duplicate_pending_approval_comments") is not True: + fail("duplicate pending-approval flag must be true") + if duplicate.get("duplicate_bot_announcements") is not True: + fail("duplicate bot-announcement flag must be true") + if duplicate.get("unique_bounty_link_count") != 2: + fail("duplicate snapshot must preserve both distinct bounty links") + + if collision.get("repo_archived") is not False: + fail("repo must be non-archived at snapshot time") + if collision.get("repo_visibility") != "PUBLIC": + fail("repo visibility must be PUBLIC") + if collision.get("same_scope_open_pr_count") != 0: + fail("same-scope open PR count must be zero at snapshot time") + terms = {term.lower() for term in collision.get("open_pr_search_terms", [])} + if {"82", "#82", "fixes #82", "issue 82"} - terms: + fail("collision-search terms are incomplete") + + required_body = {item.lower() for item in expected_pr.get("body_must_include", [])} + for required in { + "fixes #82", + "codebounty", + "$10", + "developer_application_required", + "duplicate_pending_approval_comments", + "verified_payable=false", + }: + if required not in required_body: + fail(f"expected PR body requirement missing: {required}") + + print( + json.dumps( + { + "ok": True, + "issue": issue["number"], + "visible_amount_usd": bounty["visible_amount_usd"], + "visible_amount_source": bounty["visible_amount_source"], + "required_pr_linkage": bounty["required_pr_linkage"], + "duplicate_pending_approval_comments": duplicate["duplicate_pending_approval_comments"], + "duplicate_bot_announcements": duplicate["duplicate_bot_announcements"], + "verified_payable_at_snapshot": bounty["verified_payable_at_snapshot"], + "same_scope_open_pr_count": collision["same_scope_open_pr_count"], + }, + sort_keys=True, + ) + ) + + +if __name__ == "__main__": + main() diff --git a/test-fixtures/codebounty-issue-82-duplicate-approval.json b/test-fixtures/codebounty-issue-82-duplicate-approval.json new file mode 100644 index 0000000..00b7073 --- /dev/null +++ b/test-fixtures/codebounty-issue-82-duplicate-approval.json @@ -0,0 +1,62 @@ +{ + "issue": { + "number": 82, + "url": "https://github.com/CodeBountyOrg/BountyTestRepository/issues/82", + "title": "test new issue apr 6 1235am", + "state": "open", + "body": "testing.", + "labels": ["💰 Bounty Available"], + "assignees": [], + "locked": false + }, + "bounty": { + "platform": "CodeBounty", + "visible_amount_usd": 10, + "visible_amount_source": "bot_comments", + "status_at_snapshot": "announced_by_two_codebounty_bot_comments_after_two_pending_approval_comments", + "pending_approval_comment_count": 2, + "pending_approval_comments": [ + "https://github.com/CodeBountyOrg/BountyTestRepository/issues/82#issuecomment-2781231507", + "https://github.com/CodeBountyOrg/BountyTestRepository/issues/82#issuecomment-2781231508" + ], + "announcement_comment_count": 2, + "announcement_comments": [ + "https://github.com/CodeBountyOrg/BountyTestRepository/issues/82#issuecomment-2808231115", + "https://github.com/CodeBountyOrg/BountyTestRepository/issues/82#issuecomment-2808231684" + ], + "bounty_links": [ + "http://localhost:3000/bounty/67f21306730fd07f850f2d19", + "http://localhost:3000/bounty/67f21306730fd07f850f2d1d" + ], + "required_pr_linkage": "fixes #82", + "developer_application_required": true, + "verified_payable_at_snapshot": false, + "blocker": "The CodeBounty announcement requires a platform application; a GitHub PR is submitted-visible only until CodeBounty/maintainer acceptance and payout eligibility are verified." + }, + "duplicate_approval_snapshot": { + "duplicate_pending_approval_comments": true, + "duplicate_bot_announcements": true, + "unique_bounty_link_count": 2, + "note": "Issue #82 has two pending-approval comments and two later CodeBounty bot announcement comments for the same $10 issue, preserving two distinct local bounty IDs. This fixture records that sequence without claiming verified payout eligibility." + }, + "collision_snapshot": { + "checked_at": "2026-05-14T00:02:57Z", + "repo_archived": false, + "repo_visibility": "PUBLIC", + "open_pr_search_terms": ["82", "#82", "fixes #82", "issue 82", "test new issue apr 6 1235am", "test issue 1235am"], + "same_scope_open_pr_count": 0, + "raw_open_pr_search_hits": 0, + "false_positive_open_pr_hits": [] + }, + "expected_pr": { + "title": "test: add CodeBounty issue 82 duplicate-approval fixture", + "body_must_include": [ + "fixes #82", + "CodeBounty", + "$10", + "developer_application_required", + "duplicate_pending_approval_comments", + "verified_payable=false" + ] + } +}