"""Tests for R2-WS-8: User input processing."""
from __future__ import annotations
import os
import pytest
from src.command_system.input_processing import (
InputHistory,
ParsedInput,
is_multiline_complete,
is_multiline_trigger,
parse_user_input,
suggest_commands,
validate_input,
)
from src.command_system.registry import CommandRegistry
from src.command_system.types import LocalCommand, LocalCommandResult
class TestParseUserInput:
def test_empty_input(self):
result = parse_user_input("")
assert result.input_type == "empty"
def test_whitespace_only(self):
result = parse_user_input(" \n ")
assert result.input_type == "empty"
def test_command_no_args(self):
result = parse_user_input("/help")
assert result.input_type == "command"
assert result.command_name == "help"
assert result.command_args == ""
def test_command_with_args(self):
result = parse_user_input("/context verbose")
assert result.input_type == "command"
assert result.command_name == "context"
assert result.command_args == "verbose"
def test_command_with_multi_word_args(self):
result = parse_user_input("/search foo bar baz")
assert result.input_type == "command"
assert result.command_name == "search"
assert result.command_args == "foo bar baz"
def test_regular_text(self):
result = parse_user_input("Hello, please fix the bug")
assert result.input_type == "text"
assert result.text == "Hello, please fix the bug"
def test_escaped_command(self):
result = parse_user_input("\\/help")
assert result.input_type == "text"
assert result.text == "/help"
assert result.is_escaped_command is True
def test_file_mention(self):
result = parse_user_input("Look at @./src/main.py")
assert result.input_type == "text"
assert len(result.file_mentions) == 1
assert result.file_mentions[0].endswith("src/main.py")
def test_file_mention_home(self):
result = parse_user_input("Check @~/config.json")
assert result.input_type == "text"
assert len(result.file_mentions) == 1
home = os.path.expanduser("~")
assert result.file_mentions[0].startswith(home)
def test_url_mention(self):
result = parse_user_input("See https://example.com/docs")
assert result.input_type == "text"
assert len(result.url_mentions) == 1
assert result.url_mentions[0] == "https://example.com/docs"
def test_multiple_urls(self):
result = parse_user_input("Check https://a.com and https://b.com")
assert len(result.url_mentions) == 2
def test_image_path(self):
result = parse_user_input("See @./screenshot.png")
assert len(result.image_paths) == 1
def test_no_image_for_non_image(self):
result = parse_user_input("See @./src/main.py")
assert len(result.image_paths) == 0
def test_command_with_leading_whitespace(self):
result = parse_user_input(" /help ")
assert result.input_type == "command"
assert result.command_name == "help"
def test_command_name_with_dash(self):
result = parse_user_input("/my-command arg1")
assert result.input_type == "command"
assert result.command_name == "my-command"
assert result.command_args == "arg1"
class TestValidateInput:
def test_empty_valid(self):
valid, _ = validate_input("")
assert valid is True
def test_normal_valid(self):
valid, _ = validate_input("hello world")
assert valid is True
def test_too_long(self):
valid, msg = validate_input("x" * 2_000_000)
assert valid is False
assert "too long" in msg.lower()
def test_custom_max_length(self):
valid, _ = validate_input("hello", max_length=3)
assert valid is False
class TestInputHistory:
def test_add_and_entries(self):
history = InputHistory()
history.add("first")
history.add("second")
assert history.size == 2
assert history.entries == ["first", "second"]
def test_empty_not_added(self):
history = InputHistory()
history.add("")
history.add(" ")
assert history.size == 0
def test_no_duplicate_last(self):
history = InputHistory()
history.add("same")
history.add("same")
assert history.size == 1
def test_previous(self):
history = InputHistory()
history.add("first")
history.add("second")
assert history.previous() == "second"
assert history.previous() == "first"
def test_next(self):
history = InputHistory()
history.add("first")
history.add("second")
history.previous()
history.previous()
assert history.next() == "second"
def test_next_past_end(self):
history = InputHistory()
history.add("first")
result = history.next()
assert result == ""
def test_previous_empty(self):
history = InputHistory()
assert history.previous() is None
def test_search(self):
history = InputHistory()
history.add("/help")
history.add("hello world")
history.add("/history clear")
results = history.search("/h")
assert len(results) == 2
assert results[0] == "/history clear"
def test_max_entries(self):
history = InputHistory(max_entries=3)
for i in range(5):
history.add(f"entry-{i}")
assert history.size == 3
assert history.entries[0] == "entry-2"
def test_clear(self):
history = InputHistory()
history.add("data")
history.clear()
assert history.size == 0
class TestMultiline:
def test_triple_backtick_trigger(self):
assert is_multiline_trigger("```python") is True
assert is_multiline_trigger("```") is True
def test_heredoc_trigger(self):
assert is_multiline_trigger("<<EOF") is True
def test_backslash_trigger(self):
assert is_multiline_trigger("hello \\") is True
def test_no_trigger(self):
assert is_multiline_trigger("hello world") is False
assert is_multiline_trigger("/help") is False
def test_triple_backtick_complete(self):
assert is_multiline_complete("```python\nprint('hello')\n```") is True
def test_triple_backtick_incomplete(self):
assert is_multiline_complete("```python\nprint('hello')") is False
def test_heredoc_complete(self):
assert is_multiline_complete("<<EOF\nhello world\nEOF") is True
def test_heredoc_incomplete(self):
assert is_multiline_complete("<<EOF\nhello world") is False
def test_backslash_complete(self):
assert is_multiline_complete("hello \\\nworld") is True
def test_backslash_incomplete(self):
assert is_multiline_complete("hello \\\nworld \\") is False
class TestCommandSuggestion:
def _make_registry(self) -> CommandRegistry:
registry = CommandRegistry()
for name in ("help", "history", "compact", "context", "cost", "clear", "exit"):
cmd = LocalCommand(name=name, description=f"{name} command")
registry.register(cmd)
return registry
def test_suggest_partial(self):
registry = self._make_registry()
suggestions = suggest_commands("/he", registry)
assert "/help" in suggestions
def test_suggest_all(self):
registry = self._make_registry()
suggestions = suggest_commands("/", registry)
assert len(suggestions) == 7
def test_suggest_no_match(self):
registry = self._make_registry()
suggestions = suggest_commands("/zzz", registry)
assert len(suggestions) == 0
def test_suggest_no_slash(self):
registry = self._make_registry()
suggestions = suggest_commands("help", registry)
assert len(suggestions) == 0
def test_suggest_limit(self):
registry = self._make_registry()
suggestions = suggest_commands("/", registry, limit=3)
assert len(suggestions) <= 3
def test_suggest_co_prefix(self):
registry = self._make_registry()
suggestions = suggest_commands("/co", registry)
names = set(suggestions)
assert "/compact" in names or "/context" in names or "/cost" in names