From 9c8df899734b267dd13430f213a6f54700315b47 Mon Sep 17 00:00:00 2001 From: Ashvin <76151462+ashvinctrl@users.noreply.github.com> Date: Wed, 10 Jun 2026 20:50:36 +0530 Subject: [PATCH] fix(auth): case-insensitive skill owner match on rename (#3614) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SKILL.md files written with mixed-case owner (e.g. 'owner: Alice') were skipped because the regex had no IGNORECASE flag. _usage.json keys like 'Alice::skill-name' were missed by the startswith prefix check for the same reason. Both comparisons now match the same way the deep_research and memory blocks do — case-insensitively against old_username. Fixes #3611 --- routes/auth_routes.py | 9 ++++---- tests/test_rename_user_owner_sync.py | 31 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/routes/auth_routes.py b/routes/auth_routes.py index 853958d35..6e0ae8a5a 100644 --- a/routes/auth_routes.py +++ b/routes/auth_routes.py @@ -391,7 +391,8 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter: skills_root = Path(SKILLS_DIR) if skills_root.is_dir(): _owner_re = re.compile( - r'(?m)^(owner:\s*)' + re.escape(old_username) + r'\s*$' + r'(?m)^(owner:\s*)' + re.escape(old_username) + r'\s*$', + re.IGNORECASE, ) for p in skills_root.rglob("SKILL.md"): try: @@ -406,12 +407,12 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter: try: usage = json.loads(usage_path.read_text(encoding="utf-8")) if isinstance(usage, dict): - prefix = old_username + "::" new_usage = {} changed = False for k, v in usage.items(): - if k.startswith(prefix): - new_usage[new_username + "::" + k[len(prefix):]] = v + owner_part, sep, skill_part = k.partition("::") + if sep and owner_part.lower() == old_username: + new_usage[new_username + "::" + skill_part] = v changed = True else: new_usage[k] = v diff --git a/tests/test_rename_user_owner_sync.py b/tests/test_rename_user_owner_sync.py index 16d91c512..1de14f31a 100644 --- a/tests/test_rename_user_owner_sync.py +++ b/tests/test_rename_user_owner_sync.py @@ -333,6 +333,37 @@ def test_rename_no_skills_dir_does_not_crash(rename_endpoint): assert res["ok"] is True +def test_rename_skill_md_owner_case_insensitive(rename_endpoint): + """SKILL.md written with owner: Alice (mixed case) must be updated when + renaming alice — the regex was missing re.IGNORECASE.""" + endpoint, _am, tmp_path = rename_endpoint + + skill_dir = tmp_path / "skills" / "general" / "s" + skill_dir.mkdir(parents=True) + (skill_dir / "SKILL.md").write_text(_SKILL_MD.format(owner="Alice"), encoding="utf-8") + + asyncio.run(endpoint("alice", SimpleNamespace(username="alice2"), _request(tmp_path))) + + assert "owner: alice2" in (skill_dir / "SKILL.md").read_text(encoding="utf-8") + + +def test_rename_usage_keys_case_insensitive(rename_endpoint): + """_usage.json keys stored as Alice::skill-name must be migrated when + renaming alice — the old startswith check was not lowercasing.""" + endpoint, _am, tmp_path = rename_endpoint + + skills_root = tmp_path / "skills" + skills_root.mkdir(parents=True) + usage = {"Alice::my-skill": {"uses": 5, "last_used": 999}} + (skills_root / "_usage.json").write_text(json.dumps(usage), encoding="utf-8") + + asyncio.run(endpoint("alice", SimpleNamespace(username="alice2"), _request(tmp_path))) + + updated = json.loads((skills_root / "_usage.json").read_text(encoding="utf-8")) + assert "alice2::my-skill" in updated + assert "Alice::my-skill" not in updated + + # --------------------------------------------------------------------------- # 5. P1 regression: rejected auth rename must not mutate file-backed stores # ---------------------------------------------------------------------------