Skip to content
Open
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
32 changes: 32 additions & 0 deletions bounty-notes/issue-100-duplicate-announcement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# CodeBounty issue #100 duplicate-announcement fixture

Issue: https://github.com/CodeBountyOrg/BountyTestRepository/issues/100

## Snapshot

- State: open
- Locked: false
- Assignees: none
- Labels: `bug`, `💰 Bounty Available`
- Visible CodeBounty amount: `$50 USD`
- Amount source: two CodeBounty bot announcement comments
- Required PR syntax from bot announcement: `fixes #100`
- Duplicate announcement evidence: two distinct local bounty links were posted for the same issue:
- `http://localhost:3000/bounty/6800af954e2040a18be603c7`
- `http://localhost:3000/bounty/6800af954e2040a18be603cc`
- Collision searches run immediately before this branch: `100`, `fixes #100`, `issue 100`, and `apr 17 235`.
- Exact same-scope open PR count: `0`. Three GitHub search hits were false positives from `$100` / issue `#103` contexts, not issue #100 linkage.

## 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-100.py
python3 scripts/validate-codebounty-issue-100.py test-fixtures/codebounty-issue-100-duplicate-announcement.json
git diff --check
```

The PR body should include `fixes #100` to satisfy the CodeBounty linkage rule.
131 changes: 131 additions & 0 deletions scripts/validate-codebounty-issue-100.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""Validate the CodeBounty issue #100 duplicate-announcement fixture.

The fixture records the live GitHub + CodeBounty snapshot used for this PR.
It keeps the payout boundary explicit: the visible amount comes from bot
announcement comments and 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/6800af954e2040a18be603c7",
"http://localhost:3000/bounty/6800af954e2040a18be603cc",
}
EXPECTED_ANNOUNCEMENT_COMMENTS = {
"https://github.com/CodeBountyOrg/BountyTestRepository/issues/100#issuecomment-2812057916",
"https://github.com/CodeBountyOrg/BountyTestRepository/issues/100#issuecomment-2812058021",
}


def fail(message: str) -> None:
raise SystemExit(f"issue-100 fixture invalid: {message}")


def main() -> None:
if len(sys.argv) != 2:
fail("usage: validate-codebounty-issue-100.py <fixture.json>")

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_announcement_snapshot") or {}
collision = data.get("collision_snapshot") or {}
expected_pr = data.get("expected_pr") or {}

if issue.get("number") != 100:
fail("issue number must be 100")
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 "bug" not in labels:
fail("bug label missing from live snapshot")
if "💰 Bounty Available" not in labels:
fail("bounty-available label missing from live snapshot")
if "235 am" not in issue.get("body", ""):
fail("issue body snapshot should preserve the original 235 am marker")

if bounty.get("platform") != "CodeBounty":
fail("platform must be CodeBounty")
if bounty.get("visible_amount_usd") != 50:
fail("visible bounty amount must be $50")
if bounty.get("visible_amount_source") != "bot_comments":
fail("visible amount source must stay scoped to bot comments")
if bounty.get("announcement_comment_count") != 2:
fail("expected two CodeBounty bot announcement comments")
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 announcement comment links")
if bounty.get("required_pr_linkage", "").lower() != "fixes #100":
fail("required PR linkage must be `fixes #100`")
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_bot_announcements") is not True:
fail("duplicate announcement flag must be true")
if duplicate.get("unique_bounty_link_count") != 2:
fail("duplicate snapshot must preserve both distinct bounty links")
if duplicate.get("human_comment_present") is not True:
fail("human follow-up comment should be preserved in the snapshot")

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 {"100", "fixes #100", "issue 100", "apr 17 235"} - terms:
fail("collision-search terms are incomplete")
for hit in collision.get("false_positive_open_pr_hits", []):
if "#100" in (hit.get("match_reason", "")):
fail("false-positive reasons must not claim a same-scope #100 match")

required_body = {item.lower() for item in expected_pr.get("body_must_include", [])}
for required in {
"fixes #100",
"codebounty",
"$50",
"developer_application_required",
"duplicate_bot_announcements",
"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_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()
74 changes: 74 additions & 0 deletions test-fixtures/codebounty-issue-100-duplicate-announcement.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"issue": {
"number": 100,
"url": "https://github.com/CodeBountyOrg/BountyTestRepository/issues/100",
"title": "issue apr 17 235",
"state": "open",
"body": "235 am",
"labels": ["bug", "💰 Bounty Available"],
"assignees": [],
"locked": false
},
"bounty": {
"platform": "CodeBounty",
"visible_amount_usd": 50,
"visible_amount_source": "bot_comments",
"status_at_snapshot": "announced_by_two_bot_comments_bounty_label_present",
"announcement_comment_count": 2,
"announcement_comments": [
"https://github.com/CodeBountyOrg/BountyTestRepository/issues/100#issuecomment-2812057916",
"https://github.com/CodeBountyOrg/BountyTestRepository/issues/100#issuecomment-2812058021"
],
"bounty_links": [
"http://localhost:3000/bounty/6800af954e2040a18be603c7",
"http://localhost:3000/bounty/6800af954e2040a18be603cc"
],
"required_pr_linkage": "fixes #100",
"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_announcement_snapshot": {
"duplicate_bot_announcements": true,
"unique_bounty_link_count": 2,
"human_comment_present": true,
"human_comment_url": "https://github.com/CodeBountyOrg/BountyTestRepository/issues/100#issuecomment-2840500225",
"note": "Issue #100 has two CodeBounty bot announcement comments for the same $50 bounty. This fixture preserves both distinct local bounty links and the maintainer/user follow-up comment so downstream validation can distinguish duplicate announcement evidence from payout verification."
},
"collision_snapshot": {
"checked_at": "2026-05-13T23:23:01Z",
"repo_archived": false,
"repo_visibility": "PUBLIC",
"open_pr_search_terms": ["100", "fixes #100", "issue 100", "apr 17 235"],
"same_scope_open_pr_count": 0,
"raw_open_pr_search_hits": 3,
"false_positive_open_pr_hits": [
{
"pr": "https://github.com/CodeBountyOrg/BountyTestRepository/pull/115",
"title": "docs: add issue #103 bounty note",
"match_reason": "GitHub search hit from issue #103 / $100 context; no exact issue-100 linkage."
},
{
"pr": "https://github.com/CodeBountyOrg/BountyTestRepository/pull/113",
"title": "docs: add issue #8 bounty note",
"match_reason": "GitHub search hit from $100 bounty context; no exact issue-100 linkage."
},
{
"pr": "https://github.com/CodeBountyOrg/BountyTestRepository/pull/119",
"title": "test: add issue 12 functionality smoke fixture",
"match_reason": "GitHub search hit from $100 bounty context; no exact issue-100 linkage."
}
]
},
"expected_pr": {
"title": "test: add CodeBounty issue 100 duplicate-announcement fixture",
"body_must_include": [
"fixes #100",
"CodeBounty",
"$50",
"developer_application_required",
"duplicate_bot_announcements",
"verified_payable=false"
]
}
}