@@ -178,3 +178,139 @@ def child_group() -> None:
178178
179179 assert resolved is not None
180180 assert resolved .name == "database"
181+
182+
183+ def test_alias_collision_last_wins (monkeypatch : pytest .MonkeyPatch ) -> None :
184+ """When two commands register the same alias, last registration wins."""
185+ mod = _reload_utils_click (monkeypatch , "none" )
186+
187+ @mod .group (name = "main" )
188+ def main_group () -> None :
189+ pass
190+
191+ @mod .command (name = "command-one" , aliases = ("c" ,))
192+ def cmd_one () -> None :
193+ pass
194+
195+ @mod .command (name = "command-two" , aliases = ("c" ,))
196+ def cmd_two () -> None :
197+ pass
198+
199+ main_group .add_command (cmd_one )
200+ main_group .add_command (cmd_two )
201+
202+ ctx = base_click .Context (main_group )
203+ resolved = main_group .get_command (ctx , "c" )
204+
205+ # Last registered command with alias "c" wins
206+ assert resolved is not None
207+ assert resolved .name == "command-two"
208+
209+
210+ def test_non_aliased_command_lookup_still_works (monkeypatch : pytest .MonkeyPatch ) -> None :
211+ """Ensure patching doesn't break normal command lookup without aliases."""
212+ mod = _reload_utils_click (monkeypatch , "none" )
213+
214+ @mod .group (name = "main" )
215+ def main_group () -> None :
216+ pass
217+
218+ @mod .command (name = "simple" )
219+ def simple_cmd () -> None :
220+ pass
221+
222+ @mod .command (name = "another" , aliases = ("a" ,))
223+ def another_cmd () -> None :
224+ pass
225+
226+ main_group .add_command (simple_cmd )
227+ main_group .add_command (another_cmd )
228+
229+ ctx = base_click .Context (main_group )
230+
231+ # Non-aliased command should resolve by name
232+ simple_resolved = main_group .get_command (ctx , "simple" )
233+ assert simple_resolved is not None
234+ assert simple_resolved .name == "simple"
235+
236+ # Aliased command should resolve by both name and alias
237+ another_by_name = main_group .get_command (ctx , "another" )
238+ another_by_alias = main_group .get_command (ctx , "a" )
239+ assert another_by_name is not None
240+ assert another_by_alias is not None
241+ assert another_by_name .name == "another"
242+ assert another_by_alias .name == "another"
243+
244+
245+ def test_new_rich_click_child_with_plain_click_parent (monkeypatch : pytest .MonkeyPatch ) -> None :
246+ """New rich-click child group added to plain click parent group.
247+
248+ This validates the integration scenario where a child group using
249+ new rich-click (with native alias support) is added to a parent group
250+ that was created with plain click.
251+ """
252+ mod = _reload_utils_click (monkeypatch , "new" )
253+
254+ # Create parent with plain click (simulating framework CLI)
255+ @base_click .group (name = "main" )
256+ def parent_group () -> None :
257+ pass
258+
259+ # Create child with new rich-click (native aliases)
260+ @mod .group (name = "database" , aliases = ("db" ,))
261+ def child_group () -> None :
262+ pass
263+
264+ parent_group .add_command (child_group )
265+
266+ ctx = base_click .Context (parent_group )
267+
268+ # Should resolve both by name and alias
269+ by_name = parent_group .get_command (ctx , "database" )
270+ by_alias = parent_group .get_command (ctx , "db" )
271+
272+ assert by_name is not None
273+ assert by_name .name == "database"
274+ assert by_alias is not None
275+ assert by_alias .name == "database"
276+
277+
278+ def test_command_name_not_added_to_own_alias_mapping (monkeypatch : pytest .MonkeyPatch ) -> None :
279+ """A command's own name is not added to the alias mapping.
280+
281+ This ensures that looking up a command by its canonical name works
282+ through normal click resolution, not through alias mapping.
283+ """
284+ mod = _reload_utils_click (monkeypatch , "none" )
285+
286+ @mod .group (name = "main" )
287+ def main_group () -> None :
288+ pass
289+
290+ @mod .command (name = "status" , aliases = ("st" , "stat" ))
291+ def status_cmd () -> None :
292+ pass
293+
294+ main_group .add_command (status_cmd )
295+
296+ ctx = base_click .Context (main_group )
297+
298+ # Command resolves by canonical name (through normal click)
299+ by_name = main_group .get_command (ctx , "status" )
300+ # And by aliases (through alias mapping)
301+ by_alias_st = main_group .get_command (ctx , "st" )
302+ by_alias_stat = main_group .get_command (ctx , "stat" )
303+
304+ assert by_name is not None
305+ assert by_alias_st is not None
306+ assert by_alias_stat is not None
307+ assert by_name .name == "status"
308+ assert by_alias_st .name == "status"
309+ assert by_alias_stat .name == "status"
310+
311+ # The alias mapping should NOT contain the command's own name
312+ # This is why we do `self._alias_mapping.pop(command_name, None)`
313+ alias_mapping = getattr (main_group , "_alias_mapping" , {})
314+ assert "status" not in alias_mapping
315+ assert "st" in alias_mapping
316+ assert "stat" in alias_mapping
0 commit comments