Skip to content

Commit 426d28e

Browse files
iamaeroplaneclaude
andcommitted
fix(extensions): check SKILL collision for auto-corrected aliases
After auto-correcting an alias (e.g. 'speckit.test-ext.hello' → 'test-ext.hello'), the corrected name was written back to aliases[i] and rename_map but never checked against seen_skill_names nor added to it. This allowed a corrected alias to silently produce the same SKILL output name as an existing primary or alias. Now the corrected name is collision-checked before the warning is emitted and registered in seen_skill_names immediately after. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 9a48e78 commit 426d28e

2 files changed

Lines changed: 26 additions & 0 deletions

File tree

src/specify_cli/extensions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,13 +265,23 @@ def _validate(self):
265265
else:
266266
corrected = self._try_correct_alias_name(alias, ext["id"])
267267
if corrected:
268+
corrected_skill = self._skill_output_name(corrected)
269+
if corrected_skill in seen_skill_names:
270+
raise ValidationError(
271+
f"Alias '{alias}' (corrected to '{corrected}') on command "
272+
f"'{cmd['name']}' would produce SKILL output name "
273+
f"'{corrected_skill}' which is already claimed by "
274+
f"{seen_skill_names[corrected_skill]}. "
275+
f"Choose a distinct alias name."
276+
)
268277
self.warnings.append(
269278
f"Alias '{alias}' does not follow the required pattern "
270279
f"'{{extension}}.{{command}}'. Registering as '{corrected}'. "
271280
f"The extension author should update the manifest to use this name."
272281
)
273282
rename_map[alias] = corrected
274283
aliases[i] = corrected
284+
seen_skill_names[corrected_skill] = f"alias '{corrected}' on command '{cmd['name']}'"
275285
else:
276286
raise ValidationError(
277287
f"Invalid alias '{alias}': "

tests/test_extensions.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,22 @@ def test_cross_command_skill_name_collision_rejected(self, temp_dir, valid_manif
397397
with pytest.raises(ValidationError, match="would produce SKILL output name"):
398398
ExtensionManifest(manifest_path)
399399

400+
def test_corrected_alias_skill_name_collision_rejected(self, temp_dir, valid_manifest_data):
401+
"""Auto-corrected alias that collides with an existing primary's SKILL name is rejected."""
402+
import yaml
403+
404+
# Primary 'speckit.test-ext.hello' → skill 'speckit-test-ext-hello'.
405+
# Alias 'speckit.test-ext.hello' gets auto-corrected to 'test-ext.hello'
406+
# which also maps to 'speckit-test-ext-hello' — collision.
407+
valid_manifest_data["provides"]["commands"][0]["aliases"] = ["speckit.test-ext.hello"]
408+
409+
manifest_path = temp_dir / "extension.yml"
410+
with open(manifest_path, "w") as f:
411+
yaml.dump(valid_manifest_data, f)
412+
413+
with pytest.raises(ValidationError, match="would produce SKILL output name"):
414+
ExtensionManifest(manifest_path)
415+
400416
def test_hook_alias_ref_canonicalized_to_speckit_form(self, temp_dir, valid_manifest_data):
401417
"""Hook command refs in alias form are lifted to canonical speckit.ext.cmd form."""
402418
import yaml

0 commit comments

Comments
 (0)