Skip to content

Commit 41ed86c

Browse files
committed
fix(extensions): let a disabled extension's manifest claim its files
A disabled extension claimed only the names its registry entry lists for the agent, because its manifest's commands aren't registered. When that entry can't be read (registered_commands or its per-agent value isn't a list of names), it claimed nothing, so integration use, switch or upgrade registered another extension's command over its prompt, e.g. speckit.foo.bar-baz over a disabled speckit.foo-bar.baz. It now claims its manifest's names in that case, and registration skips the pair. Refs #4797 Assisted-by: Claude Code (model: claude-opus-5-5, autonomous)
1 parent c68b384 commit 41ed86c

2 files changed

Lines changed: 45 additions & 4 deletions

File tree

‎src/specify_cli/extensions/__init__.py‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4175,7 +4175,8 @@ def _shared_command_files(
41754175
Every extension claims the names already registered for the agent,
41764176
because their files stay on disk, including names its manifest no
41774177
longer declares. An enabled extension with a readable manifest also
4178-
claims every name it declares. A command, its own aliases, and a
4178+
claims every name it declares, and so does a disabled one whose
4179+
registered names can't be read. A command, its own aliases, and a
41794180
registered name that writes the same file are one owner.
41804181
41814182
Removal checks core files separately using the integration manifest
@@ -4202,10 +4203,16 @@ def _shared_command_files(
42024203
for name in [command["name"], *(command.get("aliases") or [])]:
42034204
primary_of[name] = command["name"]
42044205
recorded = metadata.get("registered_commands")
4205-
names = self._valid_name_list(
4206-
recorded.get(agent_name) if isinstance(recorded, dict) else None
4206+
entry = recorded.get(agent_name) if isinstance(recorded, dict) else None
4207+
unreadable = (recorded is not None and not isinstance(recorded, dict)) or (
4208+
entry is not None
4209+
and (
4210+
not isinstance(entry, list)
4211+
or not all(isinstance(name, str) for name in entry)
4212+
)
42074213
)
4208-
if manifest is not None and metadata.get("enabled", True):
4214+
names = self._valid_name_list(entry)
4215+
if manifest is not None and (metadata.get("enabled", True) or unreadable):
42094216
names = list(primary_of) + [n for n in names if n not in primary_of]
42104217
primary_of_stem = {
42114218
os.path.normcase(

‎tests/specify_cli/integrations/test_command_upgrade.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,40 @@ def test_inactive_extension_keeps_its_claim_on_a_shared_kiro_prompt(
848848
)
849849
assert not (prompts / "speckit.foo.bar-baz.md").exists()
850850

851+
@pytest.mark.parametrize(
852+
"damage",
853+
[[], "speckit.foo-bar.baz", {"kiro-cli": "speckit.foo-bar.baz"}, {"kiro-cli": [1]}],
854+
)
855+
def test_disabled_extension_with_unreadable_registration_keeps_its_prompt(
856+
self, tmp_path, damage
857+
):
858+
"""A disabled extension's prompt stays on disk. When its registry
859+
entry can't be read, its manifest still names the command, so another
860+
extension that writes the same file is not registered over it
861+
(#4797)."""
862+
project = _init_project(tmp_path, "kiro-cli")
863+
self._plant_extension(project, "foo-bar", [
864+
{"name": "speckit.foo-bar.baz", "body": "BAR-BODY\n"},
865+
], enabled=False)
866+
shared = project / ".kiro" / "prompts" / "speckit-foo-bar-baz.md"
867+
body = b"---\ndescription: Bar\n---\n\n<!-- Extension: foo-bar -->\nBAR-BODY\n"
868+
shared.write_bytes(body)
869+
registry = project / ".specify" / "extensions" / ".registry"
870+
data = json.loads(registry.read_text(encoding="utf-8"))
871+
data["extensions"]["foo-bar"]["registered_commands"] = damage
872+
registry.write_text(json.dumps(data), encoding="utf-8")
873+
self._plant_extension(project, "foo", [
874+
{"name": "speckit.foo.bar-baz", "body": "FOO-BODY\n"},
875+
])
876+
877+
result = _run_in_project(project, ["integration", "upgrade", "kiro-cli"])
878+
assert result.exit_code == 0, result.output
879+
assert (
880+
"speckit.foo.bar-baz (speckit-foo-bar-baz, also written by "
881+
"extension 'foo-bar')"
882+
) in " ".join(result.output.split())
883+
assert shared.read_bytes() == body
884+
851885
def test_enabling_a_colliding_extension_keeps_the_migrated_prompt(
852886
self, tmp_path, monkeypatch
853887
):

0 commit comments

Comments
 (0)