From 90111f8734c9d5bdb7e409547d511b8820e884e0 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:14:42 -0400 Subject: [PATCH] fix(babysit-prs): casefold owner/repo/login compares in babysit scripts babysit_delta.py's head_repository_scope and babysit_feedback.py's latest_reviews_by_author used .lower() for owner/repo/login identity comparisons where the sibling pr_queue_snapshot.py already casefolds the identical concept. Converts both to .casefold() and adds case-insensitivity regression tests locking in the behavior. Closes #815 Co-Authored-By: Claude Sonnet 5 --- .../source-control/.claude-plugin/plugin.json | 2 +- plugins/source-control/CHANGELOG.md | 14 ++++++++++++++ .../babysit-prs/scripts/babysit_delta.py | 10 ++++++---- .../babysit-prs/scripts/babysit_feedback.py | 2 +- .../scripts/tests/test_babysit_delta.py | 18 ++++++++++++++++++ .../scripts/tests/test_babysit_feedback.py | 16 ++++++++++++++++ 6 files changed, 56 insertions(+), 6 deletions(-) diff --git a/plugins/source-control/.claude-plugin/plugin.json b/plugins/source-control/.claude-plugin/plugin.json index 22a43617c5..4453decff1 100644 --- a/plugins/source-control/.claude-plugin/plugin.json +++ b/plugins/source-control/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "source-control", - "version": "0.26.2", + "version": "0.26.3", "description": "Git and GitHub delivery workflow: /commit (Conventional Commits + Co-Authored-By trailer via safe heredoc mechanics), /pull-request (prep, create, CI monitoring, review-comment triage, merge, CI-log fetch), /babysit-prs (self-pacing fleet loop — safe by default; opt-in worker/autopilot tiers add gate-checked merge and thread resolution behind a deterministic Python engine), /babysit-loop (the loop-lane merge lane: a standing or drain loop that invokes babysit-prs per cycle, configured through repo-scoped babysit_loop_* keys on the layered source-control.md seam, with merge authority human-only until the target repo's tracked config adopts the lane, a gate-proven C2-mechanical baseline once adopted, and merge-rung raises binding from the team-tracked layer only), /worktree (create, status, cleanup, audit for parallel-session isolation), /setup (check the effective commit-subject / PR-title convention merged across its config layers and the babysit-prs config, or apply — interview the repo and write the convention config to a chosen layer), and /resolve-conflicts (intent-first merge/rebase conflict resolution with a semantic-conflict sweep — never --abort). The commit-subject / PR-title convention is configurable via a source-control.md config written by a re-runnable setup skill, layered across a ~/.claude user-global file, the tracked team file, and a gitignored .claude/source-control.local.md personal overlay merged per key; Conventional Commits is the default when no convention is declared.", "author": { "name": "Melodic Software", diff --git a/plugins/source-control/CHANGELOG.md b/plugins/source-control/CHANGELOG.md index 126931f3fa..7532017aa4 100644 --- a/plugins/source-control/CHANGELOG.md +++ b/plugins/source-control/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to the `source-control` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.26.3] + +### Fixed + +- **`babysit_delta.py` and `babysit_feedback.py` now casefold owner/repo/login identity + comparisons, matching the already-ratified `.casefold()` convention `pr_queue_snapshot.py` + uses for the identical concept (#815).** `head_repository_scope`'s base/head owner and + same-repository checks, and `latest_reviews_by_author`'s per-reviewer login key, used + `.lower()` instead. Functionally equivalent for GitHub's ASCII-only owner/repo/login + alphabet, but a straggler against the sibling scripts' shared convention. Converted to + `.casefold()` in both files; added case-insensitivity regression tests covering a + differently-cased base repo, head repository, and allowlisted owner, and a differently-cased + reviewer login collapsing to one latest review. + ## [0.26.2] ### Fixed diff --git a/plugins/source-control/skills/babysit-prs/scripts/babysit_delta.py b/plugins/source-control/skills/babysit-prs/scripts/babysit_delta.py index 21afc9f12b..7f775e1f9c 100755 --- a/plugins/source-control/skills/babysit-prs/scripts/babysit_delta.py +++ b/plugins/source-control/skills/babysit-prs/scripts/babysit_delta.py @@ -169,7 +169,7 @@ def head_repository_scope( time -- there is no ambient owner configuration to inherit. """ repo = str(pr.get("repo") or "") - base_owner = repo.split("/", 1)[0].lower() if "/" in repo else "" + base_owner = repo.split("/", 1)[0].casefold() if "/" in repo else "" head_repo = pr.get("headRepository") head_repo_name = ( str(head_repo.get("nameWithOwner") or "") if is_json_object(head_repo) else "" @@ -179,12 +179,14 @@ def head_repository_scope( str(head_owner_value.get("login") or "") if is_json_object(head_owner_value) else str(head_owner_value or "") - ).lower() + ).casefold() cross_repository = pr.get("isCrossRepository") - same_repository = bool(head_repo_name) and head_repo_name.lower() == repo.lower() + same_repository = ( + bool(head_repo_name) and head_repo_name.casefold() == repo.casefold() + ) if same_repository and not head_owner: head_owner = base_owner - configured_owners = {owner.lower() for owner in allowed_owners} + configured_owners = {owner.casefold() for owner in allowed_owners} base_repo_allowed = base_owner in configured_owners base_repo_archived = bool(pr.get("baseRepositoryArchived")) review_trigger_allowed = base_repo_allowed and not base_repo_archived diff --git a/plugins/source-control/skills/babysit-prs/scripts/babysit_feedback.py b/plugins/source-control/skills/babysit-prs/scripts/babysit_feedback.py index 76c1cafe96..8cc1bf5921 100755 --- a/plugins/source-control/skills/babysit-prs/scripts/babysit_feedback.py +++ b/plugins/source-control/skills/babysit-prs/scripts/babysit_feedback.py @@ -122,7 +122,7 @@ def latest_reviews_by_author( continue elif state == "PENDING": continue - login = author_login(review).lower() + login = author_login(review).casefold() if not login: anonymous.append(review) continue diff --git a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_delta.py b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_delta.py index 4ad68d09ae..82768cec87 100644 --- a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_delta.py +++ b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_delta.py @@ -126,6 +126,24 @@ def test_owner_not_in_allowlist_is_not_allowed(self) -> None: policy = delta.head_repository_scope(make_pr(), frozenset({"other"})) self.assertFalse(policy["base_repo_allowed"]) + def test_owner_repo_login_comparisons_are_case_insensitive(self) -> None: + """GitHub owner/repo/login identity is case-insensitive end to end: + a differently-cased base repo, headRepository nameWithOwner, and + allowlist entry must all still resolve as matching.""" + pr = make_pr(repo="Owner/Repo", + headRepository={"nameWithOwner": "OWNER/REPO"}, + headRepositoryOwner={"login": "OwNeR"}) + policy = delta.head_repository_scope(pr, frozenset({"Owner"})) + self.assertTrue(policy["base_repo_allowed"]) + self.assertTrue(policy["branch_write_allowed"]) + + def test_cross_repo_head_owner_case_insensitive_allowlist_match(self) -> None: + pr = make_pr(isCrossRepository=True, + headRepository={"nameWithOwner": "Fork/repo"}, + headRepositoryOwner={"login": "FORK"}) + policy = delta.head_repository_scope(pr, frozenset({"owner", "fork"})) + self.assertTrue(policy["branch_write_allowed"]) + def test_incomplete_head_metadata_fails_closed(self) -> None: pr = make_pr(headRepository=None, isCrossRepository=None) policy = delta.head_repository_scope(pr, frozenset({"owner"})) diff --git a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_feedback.py b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_feedback.py index 4c7a2969fb..837a5a3d3c 100644 --- a/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_feedback.py +++ b/plugins/source-control/skills/babysit-prs/scripts/tests/test_babysit_feedback.py @@ -245,6 +245,22 @@ def test_latest_approval_supersedes_older_changes_requested(self) -> None: self.assertEqual(len(latest), 1) self.assertEqual(latest[0]["state"], "APPROVED") + def test_differently_cased_login_still_collapses_to_one_latest_review(self) -> None: + """GitHub logins are case-insensitive: 'Rev' and 'rev' are the same + actor, so their reviews must collapse under one latest-review key.""" + pr = { + "latestReviews": [], + "reviews": [ + {"id": 1, "author": {"login": "Rev", "__typename": "User"}, + "state": "CHANGES_REQUESTED", "submittedAt": "2026-01-01T00:00:00Z"}, + {"id": 2, "author": {"login": "rev", "__typename": "User"}, + "state": "APPROVED", "submittedAt": "2026-01-02T00:00:00Z"}, + ], + } + latest = fb.latest_reviews_by_author(pr) + self.assertEqual(len(latest), 1) + self.assertEqual(latest[0]["state"], "APPROVED") + def test_commented_review_does_not_clear_decisive_changes_request(self) -> None: pr = { "latestReviews": [],