fix: CLI chat accepts multi-word messages without quotes
Changed message param from str to list[str] in chat() and route() commands. Words are joined with spaces, so 'timmy chat hello how are you' works without quoting. Single-word messages still work as before. - chat(): message: list[str], joined to full_message - route(): message: list[str], joined to full_message - 7 new tests in test_cli_multiword.py Closes #26
This commit is contained in:
@@ -143,7 +143,7 @@ def think(
|
||||
|
||||
@app.command()
|
||||
def chat(
|
||||
message: str = typer.Argument(..., help="Message to send"),
|
||||
message: list[str] = typer.Argument(..., help="Message to send to Timmy"),
|
||||
backend: str | None = _BACKEND_OPTION,
|
||||
model_size: str | None = _MODEL_SIZE_OPTION,
|
||||
new_session: bool = typer.Option(
|
||||
@@ -173,6 +173,8 @@ def chat(
|
||||
calls are checked against config/allowlist.yaml — allowlisted operations
|
||||
execute automatically, everything else is safely rejected.
|
||||
"""
|
||||
full_message = " ".join(message)
|
||||
|
||||
import uuid
|
||||
|
||||
if session_id is not None:
|
||||
@@ -184,7 +186,7 @@ def chat(
|
||||
timmy = create_timmy(backend=backend, model_size=model_size)
|
||||
|
||||
# Use agent.run() so we can intercept paused runs for tool confirmation.
|
||||
run_output = timmy.run(message, stream=False, session_id=session_id)
|
||||
run_output = timmy.run(full_message, stream=False, session_id=session_id)
|
||||
|
||||
# Handle paused runs — dangerous tools need user approval
|
||||
run_output = _handle_tool_confirmation(timmy, run_output, session_id, autonomous=autonomous)
|
||||
@@ -328,12 +330,13 @@ def voice(
|
||||
|
||||
@app.command()
|
||||
def route(
|
||||
message: str = typer.Argument(..., help="Message to route"),
|
||||
message: list[str] = typer.Argument(..., help="Message to route"),
|
||||
):
|
||||
"""Show which agent would handle a message (debug routing)."""
|
||||
full_message = " ".join(message)
|
||||
from timmy.agents.loader import route_request_with_match
|
||||
|
||||
agent_id, matched_pattern = route_request_with_match(message)
|
||||
agent_id, matched_pattern = route_request_with_match(full_message)
|
||||
if agent_id:
|
||||
typer.echo(f"→ {agent_id} (matched: {matched_pattern})")
|
||||
else:
|
||||
|
||||
130
tests/timmy/test_cli_multiword.py
Normal file
130
tests/timmy/test_cli_multiword.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""Tests for CLI multi-word message handling (issue #26).
|
||||
|
||||
This module tests that the chat and route commands properly handle
|
||||
multi-word messages without requiring quotes.
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from timmy.cli import _CLI_SESSION_ID, app
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# chat command — multi-word message handling
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_chat_joins_multiple_args():
|
||||
"""chat command must join multiple arguments into a single message."""
|
||||
mock_run_output = MagicMock()
|
||||
mock_run_output.content = "Hello! I am doing well."
|
||||
mock_run_output.status = "COMPLETED"
|
||||
mock_run_output.active_requirements = []
|
||||
|
||||
mock_timmy = MagicMock()
|
||||
mock_timmy.run.return_value = mock_run_output
|
||||
|
||||
with patch("timmy.cli.create_timmy", return_value=mock_timmy):
|
||||
result = runner.invoke(app, ["chat", "hello", "how", "are", "you"])
|
||||
|
||||
# Verify the message was joined with spaces
|
||||
mock_timmy.run.assert_called_once_with(
|
||||
"hello how are you", stream=False, session_id=_CLI_SESSION_ID
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "Hello! I am doing well." in result.output
|
||||
|
||||
|
||||
def test_chat_single_word_still_works():
|
||||
"""chat command must still work with a single word argument."""
|
||||
mock_run_output = MagicMock()
|
||||
mock_run_output.content = "Hello!"
|
||||
mock_run_output.status = "COMPLETED"
|
||||
mock_run_output.active_requirements = []
|
||||
|
||||
mock_timmy = MagicMock()
|
||||
mock_timmy.run.return_value = mock_run_output
|
||||
|
||||
with patch("timmy.cli.create_timmy", return_value=mock_timmy):
|
||||
result = runner.invoke(app, ["chat", "hello"])
|
||||
|
||||
mock_timmy.run.assert_called_once_with("hello", stream=False, session_id=_CLI_SESSION_ID)
|
||||
assert result.exit_code == 0
|
||||
assert "Hello!" in result.output
|
||||
|
||||
|
||||
def test_chat_joins_complex_message():
|
||||
"""chat command must handle complex multi-word messages with punctuation."""
|
||||
mock_run_output = MagicMock()
|
||||
mock_run_output.content = "The file is located at /path/to/file."
|
||||
mock_run_output.status = "COMPLETED"
|
||||
mock_run_output.active_requirements = []
|
||||
|
||||
mock_timmy = MagicMock()
|
||||
mock_timmy.run.return_value = mock_run_output
|
||||
|
||||
with patch("timmy.cli.create_timmy", return_value=mock_timmy):
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["chat", "what", "is", "the", "path", "to", "the", "config", "file?"],
|
||||
)
|
||||
|
||||
mock_timmy.run.assert_called_once_with(
|
||||
"what is the path to the config file?", stream=False, session_id=_CLI_SESSION_ID
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# route command — multi-word message handling
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_route_joins_multiple_args():
|
||||
"""route command must join multiple arguments into a single message."""
|
||||
with patch("timmy.agents.loader.route_request_with_match") as mock_route:
|
||||
mock_route.return_value = ("orchestrator", None)
|
||||
result = runner.invoke(app, ["route", "hello", "how", "are", "you"])
|
||||
|
||||
# Verify the message was joined with spaces
|
||||
mock_route.assert_called_once_with("hello how are you")
|
||||
assert result.exit_code == 0
|
||||
assert "orchestrator" in result.output
|
||||
|
||||
|
||||
def test_route_single_word_still_works():
|
||||
"""route command must still work with a single word argument."""
|
||||
with patch("timmy.agents.loader.route_request_with_match") as mock_route:
|
||||
mock_route.return_value = ("spark", "test pattern")
|
||||
result = runner.invoke(app, ["route", "test"])
|
||||
|
||||
mock_route.assert_called_once_with("test")
|
||||
assert result.exit_code == 0
|
||||
assert "spark" in result.output
|
||||
|
||||
|
||||
def test_route_shows_match_info():
|
||||
"""route command must display matched pattern when there is a match."""
|
||||
with patch("timmy.agents.loader.route_request_with_match") as mock_route:
|
||||
mock_route.return_value = ("agent_1", "pattern:hello")
|
||||
result = runner.invoke(app, ["route", "hello", "world"])
|
||||
|
||||
mock_route.assert_called_once_with("hello world")
|
||||
assert result.exit_code == 0
|
||||
assert "agent_1" in result.output
|
||||
assert "pattern:hello" in result.output
|
||||
|
||||
|
||||
def test_route_no_match():
|
||||
"""route command must show orchestrator when no pattern matches."""
|
||||
with patch("timmy.agents.loader.route_request_with_match") as mock_route:
|
||||
mock_route.return_value = (None, None)
|
||||
result = runner.invoke(app, ["route", "unknown", "command", "here"])
|
||||
|
||||
mock_route.assert_called_once_with("unknown command here")
|
||||
assert result.exit_code == 0
|
||||
assert "orchestrator (no pattern match)" in result.output
|
||||
Reference in New Issue
Block a user