From 644832fd35e6f5d4d63c9336902d6491f9fff10b Mon Sep 17 00:00:00 2001 From: Wilson Freitas Date: Wed, 2 Sep 2026 05:48:00 -0300 Subject: [PATCH] fix: make README audit deterministic --- scripts/audit_readme.py | 11 ++++++- tests/test_audit_readme.py | 61 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/scripts/audit_readme.py b/scripts/audit_readme.py index 07b99cb..476a794 100644 --- a/scripts/audit_readme.py +++ b/scripts/audit_readme.py @@ -72,6 +72,8 @@ def collect_targets(readme_path: str | Path) -> list[AuditTarget]: def _parse_github_repository_url(url: str) -> str | None: + if "?" in url or "#" in url: + return None try: parsed = urlsplit(url) port = parsed.port @@ -247,7 +249,14 @@ def _candidate_repositories(entry: ReadmeEntry, github_client: Any) -> tuple[Can ) ) - matches.sort(key=lambda candidate: (-candidate.stars, candidate.full_name.casefold())) + matches.sort( + key=lambda candidate: ( + -candidate.stars, + candidate.full_name.casefold(), + candidate.full_name, + candidate.url, + ) + ) unique = [] seen = set() for candidate in matches: diff --git a/tests/test_audit_readme.py b/tests/test_audit_readme.py index 0eb0b84..efee304 100644 --- a/tests/test_audit_readme.py +++ b/tests/test_audit_readme.py @@ -199,6 +199,35 @@ class AuditReadmeTests(unittest.TestCase): self.assertEqual(findings, []) + def test_empty_query_and_fragment_delimiters_are_not_github_repository_urls(self): + urls = [ + "https://github.com/owner/query?", + "https://github.com/owner/fragment#", + "https://github.com/owner/slash-query/?", + "https://github.com/owner/slash-fragment/#", + ] + readme = "## Trading & Backtesting\n" + "".join( + f"- [Project {index}]({url}) - `Python` - Entry.\n" + for index, url in enumerate(urls) + ) + client = FakeGithubClient( + { + "owner/query": FakeRepository("owner/query"), + "owner/fragment": FakeRepository("owner/fragment"), + "owner/slash-query": FakeRepository("owner/slash-query"), + "owner/slash-fragment": FakeRepository("owner/slash-fragment"), + } + ) + + findings = self.audit( + readme, + client, + {url: observation(url) for url in urls}, + ) + + self.assertEqual(findings, []) + self.assertEqual(client.repo_calls, []) + def test_github_404_adds_no_metadata_finding_and_other_api_errors_escape(self): missing_url = "https://github.com/owner/missing" broken_url = "https://github.com/owner/broken" @@ -289,6 +318,38 @@ class AuditReadmeTests(unittest.TestCase): [{"query": "Dead Tool in:name", "sort": "stars", "order": "desc"}], ) + def test_equal_star_case_variant_candidates_are_independent_of_api_order(self): + url = "https://dead.example/tool" + readme = ( + "## Trading & Backtesting\n" + f"- [Dead Tool]({url}) - `Python` - Entry.\n" + ) + upper = FakeRepository("OWNER/Dead-Tool", name="dead-tool", stars=40) + lower = FakeRepository("owner/dead-tool", name="dead-tool", stars=40) + + selections = [] + for repositories in ((lower, upper), (upper, lower)): + client = FakeGithubClient( + searches={"Dead Tool in:name": repositories} + ) + findings = self.audit( + readme, + client, + {url: observation(url, Outcome.DEAD, status=404)}, + ) + selections.append( + next( + finding.candidates + for finding in findings + if finding.kind == FindingKind.CANDIDATES + ) + ) + + expected = ( + Candidate("OWNER/Dead-Tool", "https://github.com/OWNER/Dead-Tool", 40), + ) + self.assertEqual(selections, [expected, expected]) + def test_all_finding_kinds_are_mapped_and_sorted_deterministically(self): urls = { "dead": "https://dead.example",