diff --git a/CHANGELOG.md b/CHANGELOG.md index 58d50eea..fea83611 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,15 @@ PyPI version — not the changelog header. relays. Stall logic extracted into a `StallTracker` for clarity; +9 tests (142 green), devpulse audit 100%. (devpulse) +- **`is_owner()` now case-folds — `is_owner('DEVPULSE')` matches `is_owner('devpulse')` + (issue #679).** The spawn-registry resolver (`registry.py:382`) `@`-normalized the + email but never lowercased, so a mixed-case branch name (registry names are + mixed-case: `DEVPULSE` vs `devpulse`) returned `False` against the seated owner. + Harmless today (the only caller lowercases first) but the frozen TDPLAN-0012 + contract promises normalization, and PART-4 owner-gating may pass a raw name. + Now lowercases both sides; verified live (every case variant of the owner → True, + non-owners → False) + a case-insensitivity test (316 green). (@spawn, verified devpulse) + - **`aipass install` no longer hard-fails (exit 2, silently) when it can't create global symlinks (issue #660 follow-up).** `setup.sh` runs under `set -euo pipefail`; the #660 `safe_symlink` refactor returns `2` on `ln` diff --git a/src/aipass/spawn/apps/handlers/registry.py b/src/aipass/spawn/apps/handlers/registry.py index 43409ae4..78c03fb2 100644 --- a/src/aipass/spawn/apps/handlers/registry.py +++ b/src/aipass/spawn/apps/handlers/registry.py @@ -379,10 +379,10 @@ def is_owner(email, start_path=None): """ if not email: return False - normalized = email if email.startswith("@") else f"@{email}" + normalized = (email if email.startswith("@") else f"@{email}").lower() owner = get_owner(start_path=start_path) if owner is None: return False owner_email = owner.get("email", "") - owner_normalized = owner_email if owner_email.startswith("@") else f"@{owner_email}" + owner_normalized = (owner_email if owner_email.startswith("@") else f"@{owner_email}").lower() return normalized == owner_normalized diff --git a/src/aipass/spawn/tests/test_owner_resolver.py b/src/aipass/spawn/tests/test_owner_resolver.py index 7fdc219a..75e3507b 100644 --- a/src/aipass/spawn/tests/test_owner_resolver.py +++ b/src/aipass/spawn/tests/test_owner_resolver.py @@ -154,6 +154,15 @@ class TestIsOwner: with patch("aipass.spawn.apps.handlers.registry.find_registry", return_value=registry_with_owner): assert is_owner("devpulse", start_path=tmp_path) is True + def test_case_insensitive(self, registry_with_owner, tmp_path): + from aipass.spawn.apps.handlers.registry import is_owner + + with patch("aipass.spawn.apps.handlers.registry.find_registry", return_value=registry_with_owner): + assert is_owner("DEVPULSE", start_path=tmp_path) is True + assert is_owner("@DEVPULSE", start_path=tmp_path) is True + assert is_owner("DevPulse", start_path=tmp_path) is True + assert is_owner("ALPHA", start_path=tmp_path) is False + def test_false_for_non_owner(self, registry_with_owner, tmp_path): from aipass.spawn.apps.handlers.registry import is_owner