fix: address prefix matching recursion and skill command coverage

Per teknium1 review on PR #968:

1. Guard against infinite recursion: if expanded name equals the typed
   token (already exact), fall through to Unknown command instead of
   redispatching the same string forever.

2. Include skill slash commands in prefix resolution so execution-time
   matching agrees with tab-completion (set(COMMANDS) | set(_skill_commands)).

3. Add missing test cases:
   - unambiguous prefix with extra args does not recurse
   - exact command with args does not loop
   - skill command prefix matches correctly
   - exact builtin takes priority over skill prefix ambiguity

8 tests passing.
This commit is contained in:
teyrebaz33 2026-03-14 14:11:34 +03:00 committed by teknium1
parent a50550fdb4
commit fbdce27b9a
2 changed files with 90 additions and 23 deletions

24
cli.py
View file

@ -3094,15 +3094,27 @@ class HermesCLI:
else:
self.console.print(f"[bold red]Failed to load skill for {base_cmd}[/]")
else:
# Prefix matching: if input uniquely identifies one command, execute it
# Prefix matching: if input uniquely identifies one command, execute it.
# Matches against both built-in COMMANDS and installed skill commands so
# that execution-time resolution agrees with tab-completion.
from hermes_cli.commands import COMMANDS
typed_base = cmd_lower.split()[0]
matches = [c for c in COMMANDS if c.startswith(typed_base)]
all_known = set(COMMANDS) | set(_skill_commands)
matches = [c for c in all_known if c.startswith(typed_base)]
if len(matches) == 1:
# Re-dispatch with the full command name, preserving any arguments
remainder = cmd_original.strip()[len(typed_base):]
full_cmd = matches[0] + remainder
return self.process_command(full_cmd)
# Expand the prefix to the full command name, preserving arguments.
# Guard against redispatching the same token to avoid infinite
# recursion when the expanded name still doesn't hit an exact branch
# (e.g. /config with extra args that are not yet handled above).
full_name = matches[0]
if full_name == typed_base:
# Already an exact token — no expansion possible; fall through
self.console.print(f"[bold red]Unknown command: {cmd_lower}[/]")
self.console.print("[dim #B8860B]Type /help for available commands[/]")
else:
remainder = cmd_original.strip()[len(typed_base):]
full_cmd = full_name + remainder
return self.process_command(full_cmd)
elif len(matches) > 1:
self.console.print(f"[bold yellow]Ambiguous command: {cmd_lower}[/]")
self.console.print(f"[dim]Did you mean: {', '.join(sorted(matches))}?[/]")