File size: 7,113 Bytes
9aa5185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
"""Tests for slash command prefix matching in HermesCLI.process_command."""
from unittest.mock import MagicMock, patch
from cli import HermesCLI


def _make_cli():
    cli_obj = HermesCLI.__new__(HermesCLI)
    cli_obj.config = {}
    cli_obj.console = MagicMock()
    cli_obj.agent = None
    cli_obj.conversation_history = []
    cli_obj.session_id = None
    cli_obj._pending_input = MagicMock()
    return cli_obj


class TestSlashCommandPrefixMatching:
    def test_unique_prefix_dispatches_command(self):
        """/con should dispatch to /config when it uniquely matches."""
        cli_obj = _make_cli()
        with patch.object(cli_obj, 'show_config') as mock_config:
            cli_obj.process_command("/con")
        mock_config.assert_called_once()

    def test_unique_prefix_with_args_does_not_recurse(self):
        """/con set key value should expand to /config set key value without infinite recursion."""
        cli_obj = _make_cli()
        dispatched = []

        original = cli_obj.process_command.__func__

        def counting_process_command(self_inner, cmd):
            dispatched.append(cmd)
            if len(dispatched) > 5:
                raise RecursionError("process_command called too many times")
            return original(self_inner, cmd)

        # Mock show_config since the test is about recursion, not config display
        with patch.object(type(cli_obj), 'process_command', counting_process_command), \
             patch.object(cli_obj, 'show_config'):
            try:
                cli_obj.process_command("/con set key value")
            except RecursionError:
                assert False, "process_command recursed infinitely"

        # Should have been called at most twice: once for /con set..., once for /config set...
        assert len(dispatched) <= 2

    def test_exact_command_with_args_does_not_recurse(self):
        """/config set key value hits exact branch and does not loop back to prefix."""
        cli_obj = _make_cli()
        call_count = [0]

        original_pc = HermesCLI.process_command

        def guarded(self_inner, cmd):
            call_count[0] += 1
            if call_count[0] > 10:
                raise RecursionError("Infinite recursion detected")
            return original_pc(self_inner, cmd)

        # Mock show_config since the test is about recursion, not config display
        with patch.object(HermesCLI, 'process_command', guarded), \
             patch.object(cli_obj, 'show_config'):
            try:
                cli_obj.process_command("/config set key value")
            except RecursionError:
                assert False, "Recursed infinitely on /config set key value"

        assert call_count[0] <= 3

    def test_ambiguous_prefix_shows_suggestions(self):
        """/re matches multiple commands — should show ambiguous message."""
        cli_obj = _make_cli()
        with patch("cli._cprint") as mock_cprint:
            cli_obj.process_command("/re")
            printed = " ".join(str(c) for c in mock_cprint.call_args_list)
        assert "Ambiguous" in printed or "Did you mean" in printed

    def test_unknown_command_shows_error(self):
        """/xyz should show unknown command error."""
        cli_obj = _make_cli()
        with patch("cli._cprint") as mock_cprint:
            cli_obj.process_command("/xyz")
            printed = " ".join(str(c) for c in mock_cprint.call_args_list)
        assert "Unknown command" in printed

    def test_exact_command_still_works(self):
        """/help should still work as exact match."""
        cli_obj = _make_cli()
        with patch.object(cli_obj, 'show_help') as mock_help:
            cli_obj.process_command("/help")
        mock_help.assert_called_once()

    def test_skill_command_prefix_matches(self):
        """A prefix that uniquely matches a skill command should dispatch it."""
        cli_obj = _make_cli()
        fake_skill = {"/test-skill-xyz": {"name": "Test Skill", "description": "test"}}
        printed = []
        cli_obj.console.print = lambda *a, **kw: printed.append(str(a))

        import cli as cli_mod
        with patch.object(cli_mod, '_skill_commands', fake_skill):
            cli_obj.process_command("/test-skill-xy")

        # Should NOT show "Unknown command" — should have dispatched or attempted skill
        unknown = any("Unknown command" in p for p in printed)
        assert not unknown, f"Expected skill prefix to match, got: {printed}"

    def test_ambiguous_between_builtin_and_skill(self):
        """Ambiguous prefix spanning builtin + skill commands shows suggestions."""
        cli_obj = _make_cli()
        # /help-extra is a fake skill that shares /hel prefix with /help
        fake_skill = {"/help-extra": {"name": "Help Extra", "description": "test"}}

        import cli as cli_mod
        with patch.object(cli_mod, '_skill_commands', fake_skill),              patch.object(cli_obj, 'show_help') as mock_help:
            cli_obj.process_command("/help")

        # /help is an exact match so should work normally, not show ambiguous
        mock_help.assert_called_once()
        printed = " ".join(str(c) for c in cli_obj.console.print.call_args_list)
        assert "Ambiguous" not in printed

    def test_shortest_match_preferred_over_longer_skill(self):
        """/qui should dispatch to /quit (5 chars) not report ambiguous with /quint-pipeline (15 chars)."""
        cli_obj = _make_cli()
        fake_skill = {"/quint-pipeline": {"name": "Quint Pipeline", "description": "test"}}

        import cli as cli_mod
        with patch.object(cli_mod, '_skill_commands', fake_skill):
            # /quit is caught by the exact "/quit" branch → process_command returns False
            result = cli_obj.process_command("/qui")

        # Returns False because /quit was dispatched (exits chat loop)
        assert result is False
        printed = " ".join(str(c) for c in cli_obj.console.print.call_args_list)
        assert "Ambiguous" not in printed

    def test_tied_shortest_matches_still_ambiguous(self):
        """/re matches /reset and /retry (both 6 chars) — no unique shortest, stays ambiguous."""
        cli_obj = _make_cli()
        printed = []
        import cli as cli_mod
        with patch.object(cli_mod, '_cprint', side_effect=lambda t: printed.append(t)):
            cli_obj.process_command("/re")
        combined = " ".join(printed)
        assert "Ambiguous" in combined or "Did you mean" in combined

    def test_exact_typed_name_dispatches_over_longer_match(self):
        """/help typed with /help-extra skill installed → exact match wins."""
        cli_obj = _make_cli()
        fake_skill = {"/help-extra": {"name": "Help Extra", "description": ""}}
        import cli as cli_mod
        with patch.object(cli_mod, '_skill_commands', fake_skill), \
             patch.object(cli_obj, 'show_help') as mock_help:
            cli_obj.process_command("/help")
        mock_help.assert_called_once()
        printed = " ".join(str(c) for c in cli_obj.console.print.call_args_list)
        assert "Ambiguous" not in printed