76972449c7
* feat(cli): multi-stage slash command completions with subcommand awareness Phase 1 of #82 — subcommand and argument awareness in completions. - commands/base.py: add SubCommand dataclass and subcommands/category ClassVars to the Command ABC. Each SubCommand has name, description, and optional arguments. - commands/manager.py: add get_subcommands() and list_subcommands() methods to expose subcommand metadata for completion rendering. - commands/implementation/mcp.py: declare 6 subcommands (list, config, add, edit, remove, install). - commands/implementation/model_fallback.py: declare 6 subcommands (list, add, remove, clear, save, help). - commands/implementation/channel.py: declare 2 subcommands (status, stop). - cli/tui_interactive.py: rewrite on_text_area_changed slash-completion branch. When the user types a command name + trailing space and the command has subcommands, show subcommand completions instead of hiding the popup. Filter subcommands by typed prefix in multi-token input. - commands/implementation/general.py: /help now lists subcommands below each command that declares them. Tests: 8 new tests covering SubCommand creation, CommandManager subcommand lookup, and cross-command verification. 2293 passed baseline, no regressions. * fix: subcommand completion preserves prefix + prompt_toolkit + tests - _apply_selected_completion: preserve '/mcp ' prefix when completing subcommands via _comp_is_subcommand flag - SlashCommandCompleter (Rich CLI): add subcommand completion support - Fix trailing-space bug: rstrip prefix before top-level matching - test_tui_widgets.py: update stub on_input_changed to match multi-stage logic; add 5 new subcommand tests - test_command_manager.py: 8 tests for SubCommand + CommandManager 28 passed, 0 failed. * style: ruff format tui_interactive.py + test_tui_widgets.py * style: fix RUF012 ClassVar annotation on subcommands lists * fix: sync test stub, add len>=3 guard, remove exact-match hide - Sync test stub on_input_changed with real TUI code (remove exact-match hide for subcommands, add len(parts)>=3 guard) - Update test_input_changed_exact_subcommand_hides -> shows_confirmation - Add test_input_changed_three_parts_hides - Remove unused category ClassVar (din0s: what is this for) * refactor(commands): extract shared completion engine Per din0s feedback: one shared completion engine (commands/_completion_engine.py) that parses text + cursor once, returns structured CompletionCandidate objects with replace_start/replace_end ranges. - SlashCommandCompleter (Rich CLI): thin adapter, delegates to engine - on_text_area_changed (TUI): thin adapter, delegates to engine - _apply_selected_completion: uses candidate.replace_start/replace_end instead of _comp_is_subcommand flag - Tests: engine tested directly (10 new tests), stub methods updated 29 passed, 0 failed. * style: ruff format * fix: preserve text after cursor when applying completion CodeRabbit: replace_start only cuts from start to cursor, dropping any suffix after the cursor. Use replace_start + replace_end to correctly splice the replacement while preserving trailing text. * fix(cli): repair slash-command completion (TUI crash, subcommand bugs, sort) Apology + context: the previous push shipped a TUI-breaking change (the new shared engine assumed ``event.text_area.cursor_position`` existed, but ``ChatTextArea`` / ``Changed`` don't expose it). User caught the crash on ``/``; fixing that surfaced two more bugs in the engine that din0s had already flagged. This commit addresses all of them and drops a piece of dead stub code. ## Bug fixes 1. **TUI crash on ``/``** (``tui_interactive.py:2335``) ``event.cursor_position`` doesn't exist on the ``Changed`` event, and ``ChatTextArea`` (Textual ``TextArea`` subclass) doesn't expose ``cursor_position`` either. Pass ``len(event.text_area.text)`` instead — the user types at the end of the input in practice. 2. **Subcommand trailing-space duplication** (``_completion_engine.py``) Typing ``/mcp a `` + Tab produced ``/mcp aadd``. The engine included the trailing space in ``replace_end``; the TUI apply unconditionally appended ``" "``, producing double-space output. Fix: ``replace_end`` excludes the trailing space; the TUI apply checks ``current[replace_end:].startswith(" ")`` and skips the separator when the suffix already has one. 3. **Subcommand exact-match confirmation noise** (``_completion_engine.py``) Typing ``/mcp list`` + Tab re-inserted ``list`` and the popup kept showing the same subcommand. Add a guard mirroring the top-level exact-match rule: when the only subcommand match is the prefix itself (no trailing space), return ``empty``. 4. **Alphabetical sort dropped in CLI** (``cli/interactive.py``) The new completer iterated ``result.candidates`` in registration order. Re-add ``sorted(result.candidates, key=lambda c: c.text)``. Same sort added to the TUI for consistency. ## Cleanup - Drop the dead ``on_input_changed`` method from the ``_StubApp`` test stub (0 call sites) plus the unused ``_slash_commands`` / ``_subcommands`` locals that fed it. This addresses din0s's comment about the stub duplicating real TUI logic — the inlined copy is no longer needed since the real completer now routes through the shared engine. ## Tests - ``test_engine_exact_subcommand_shows_confirmation`` → renamed to ``test_engine_exact_subcommand_hides`` to match new behavior. - New: ``test_engine_subcommand_trailing_space_excludes_space_from_range`` and ``test_engine_subcommand_trailing_space_apply_does_not_double_space``. - All 97 tests in ``test_tui_widgets.py`` pass. - ``ruff check`` / ``ruff format`` clean. - Local TUI smoke: ``/`` (no crash, top-level popup), ``/mcp `` (subcommand popup), ``/mcp a `` + Tab → ``/mcp add ``. Refs the din0s review comments on PR #273. CLI path tests and the ``category`` ClassVar follow-up are deferred to a separate PR (the former is a test-suite addition; the latter is already absent from ``base.py`` on the current branch). * fix: address remaining review items (help duplication, CLI tests, stub sync, docstrings) - mcp.py: auto-generate help text from subcommands ClassVar (#1) - tests/test_cli_completion.py: add 9 CLI completer tests (#2c) - test_tui_widgets.py: sync _apply_selected_completion stub with real code (#4) - mcp.py + interactive.py: add docstrings to key functions (#8) * fix: hide completions on exact subcommand match regardless of trailing space Remove the ot has_trailing_space guard from the exact-subcommand check. Previously /mcp list (with trailing space) would still return candidates, causing Tab to oscillate between adding and removing the trailing whitespace. Now the engine hides whenever the subcommand is an exact match, same as the top-level rule. Added test_engine_exact_subcommand_with_trailing_space_hides to cover the scenario din0s flagged. * refactor: use StrEnum for CompletionResult.kind Replace plain str with CompletionKind(StrEnum) for type safety. Backward-compatible with existing string comparisons. * fix: normalize @file completion tuples to CompletionCandidate complete_file_mention() returns list[tuple[str, str]] but the TUI rendering/apply code expects objects with .text/.description. Wrap tuples in CompletionCandidate to prevent AttributeError crash. --------- Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com>
73 lines
2.7 KiB
Python
73 lines
2.7 KiB
Python
"""Tests for CLI SlashCommandCompleter (prompt_toolkit adapter)."""
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
from EvoScientist.cli.interactive import SlashCommandCompleter
|
|
|
|
|
|
def _doc(text: str):
|
|
"""Create a minimal prompt_toolkit Document stub."""
|
|
doc = MagicMock()
|
|
doc.text_before_cursor = text
|
|
return doc
|
|
|
|
|
|
class TestSlashCommandCompleter:
|
|
"""Verify that ``SlashCommandCompleter.get_completions`` correctly
|
|
delegates to the shared ``compute_completions`` engine and translates
|
|
candidates into prompt_toolkit ``Completion`` objects.
|
|
"""
|
|
|
|
def test_top_level_slash_shows_commands(self):
|
|
completer = SlashCommandCompleter()
|
|
completions = list(completer.get_completions(_doc("/he"), None))
|
|
texts = {c.text for c in completions}
|
|
assert "/help" in texts
|
|
|
|
def test_exact_command_no_space_hides(self):
|
|
completer = SlashCommandCompleter()
|
|
completions = list(completer.get_completions(_doc("/help"), None))
|
|
assert completions == []
|
|
|
|
def test_non_slash_returns_empty(self):
|
|
completer = SlashCommandCompleter()
|
|
completions = list(completer.get_completions(_doc("hello"), None))
|
|
assert completions == []
|
|
|
|
def test_trailing_space_shows_subcommands(self):
|
|
completer = SlashCommandCompleter()
|
|
completions = list(completer.get_completions(_doc("/mcp "), None))
|
|
texts = {c.text for c in completions}
|
|
assert "list" in texts
|
|
assert "add" in texts
|
|
|
|
def test_subcommand_prefix_filters(self):
|
|
completer = SlashCommandCompleter()
|
|
completions = list(completer.get_completions(_doc("/mcp lis"), None))
|
|
texts = {c.text for c in completions}
|
|
assert texts == {"list"}
|
|
|
|
def test_exact_subcommand_hides(self):
|
|
completer = SlashCommandCompleter()
|
|
completions = list(completer.get_completions(_doc("/mcp list"), None))
|
|
assert completions == []
|
|
|
|
def test_results_sorted_alphabetically(self):
|
|
completer = SlashCommandCompleter()
|
|
completions = list(completer.get_completions(_doc("/"), None))
|
|
texts = [c.text for c in completions]
|
|
assert texts == sorted(texts)
|
|
|
|
def test_display_meta_is_description(self):
|
|
completer = SlashCommandCompleter()
|
|
completions = list(completer.get_completions(_doc("/he"), None))
|
|
for c in completions:
|
|
if c.text == "/help":
|
|
assert c.display_meta is not None
|
|
|
|
def test_subcommand_completions_sorted(self):
|
|
completer = SlashCommandCompleter()
|
|
completions = list(completer.get_completions(_doc("/mcp "), None))
|
|
texts = [c.text for c in completions]
|
|
assert texts == sorted(texts)
|