fix: CLI chat accepts multi-word messages without quotes
Some checks failed
Tests / lint (pull_request) Successful in 3s
Tests / test (pull_request) Failing after 56s

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:
2026-03-14 19:43:52 -04:00
parent a728665159
commit 9171d93ef9
2 changed files with 137 additions and 4 deletions

View File

@@ -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:

View 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