feat: REPL mode, stdin support, multi-word fix for CLI (#26)
This commit is contained in:
@@ -141,6 +141,180 @@ def test_chat_cleans_response():
|
||||
assert "The answer is 2." in result.output
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# chat command — multi-word messages
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_chat_multiword_without_quotes():
|
||||
"""chat should accept multiple arguments without requiring quotes."""
|
||||
mock_run_output = MagicMock()
|
||||
mock_run_output.content = "Response"
|
||||
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", "world", "how", "are", "you"])
|
||||
|
||||
mock_timmy.run.assert_called_once_with(
|
||||
"hello world how are you", stream=False, session_id=_CLI_SESSION_ID
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# chat command — stdin support
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_chat_stdin_with_dash():
|
||||
"""chat - should read message from stdin when "-" is passed."""
|
||||
mock_run_output = MagicMock()
|
||||
mock_run_output.content = "Stdin response"
|
||||
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", "-"], input="Hello from stdin")
|
||||
|
||||
mock_timmy.run.assert_called_once_with(
|
||||
"Hello from stdin", stream=False, session_id=_CLI_SESSION_ID
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "Stdin response" in result.output
|
||||
|
||||
|
||||
def test_chat_stdin_piped():
|
||||
"""chat should read from stdin when piped (non-tty)."""
|
||||
mock_run_output = MagicMock()
|
||||
mock_run_output.content = "Piped response"
|
||||
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),
|
||||
patch("timmy.cli._is_interactive", return_value=False),
|
||||
):
|
||||
result = runner.invoke(app, ["chat", "ignored"], input="Piped message")
|
||||
|
||||
mock_timmy.run.assert_called_once_with(
|
||||
"Piped message", stream=False, session_id=_CLI_SESSION_ID
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_chat_stdin_empty_exits_with_error():
|
||||
"""chat - should exit with error when stdin is empty."""
|
||||
with patch("timmy.cli._is_interactive", return_value=False):
|
||||
result = runner.invoke(app, ["chat", "-"], input="")
|
||||
|
||||
assert result.exit_code == 1
|
||||
assert "No input provided" in result.output or "No input provided" in str(result.output)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# repl command
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_repl_exits_on_ctrl_c():
|
||||
"""repl should exit gracefully on Ctrl+C (KeyboardInterrupt)."""
|
||||
with patch("builtins.input", side_effect=KeyboardInterrupt):
|
||||
result = runner.invoke(app, ["repl"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Goodbye" in result.output
|
||||
|
||||
|
||||
def test_repl_exits_on_ctrl_d():
|
||||
"""repl should exit gracefully on Ctrl+D (EOFError)."""
|
||||
with patch("builtins.input", side_effect=EOFError):
|
||||
result = runner.invoke(app, ["repl"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Goodbye" in result.output
|
||||
|
||||
|
||||
def test_repl_exits_on_quit_command():
|
||||
"""repl should exit when user types 'quit'."""
|
||||
with patch("builtins.input", return_value="quit"):
|
||||
result = runner.invoke(app, ["repl"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Goodbye" in result.output
|
||||
|
||||
|
||||
def test_repl_exits_on_exit_command():
|
||||
"""repl should exit when user types 'exit'."""
|
||||
with patch("builtins.input", return_value="exit"):
|
||||
result = runner.invoke(app, ["repl"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Goodbye" in result.output
|
||||
|
||||
|
||||
def test_repl_sends_message_to_chat():
|
||||
"""repl should send user input to session.chat()."""
|
||||
with (
|
||||
patch("builtins.input", side_effect=["Hello Timmy", "exit"]),
|
||||
patch("timmy.session.chat") as mock_chat,
|
||||
):
|
||||
mock_chat.return_value = "Hi there!"
|
||||
result = runner.invoke(app, ["repl"])
|
||||
|
||||
mock_chat.assert_called()
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
def test_repl_skips_empty_input():
|
||||
"""repl should skip empty input lines."""
|
||||
with (
|
||||
patch("builtins.input", side_effect=["", "", "hello", "exit"]),
|
||||
patch("timmy.session.chat") as mock_chat,
|
||||
):
|
||||
mock_chat.return_value = "Response"
|
||||
result = runner.invoke(app, ["repl"])
|
||||
|
||||
# chat should only be called once (for "hello"), empty lines are skipped, exit breaks
|
||||
assert mock_chat.call_count == 1
|
||||
|
||||
|
||||
def test_repl_uses_custom_session_id():
|
||||
"""repl --session-id should use the provided session ID."""
|
||||
with (
|
||||
patch("builtins.input", side_effect=["hello", "exit"]),
|
||||
patch("timmy.session.chat") as mock_chat,
|
||||
):
|
||||
mock_chat.return_value = "Response"
|
||||
runner.invoke(app, ["repl", "--session-id", "custom-session"])
|
||||
|
||||
# Check that chat was called with the custom session_id
|
||||
calls = mock_chat.call_args_list
|
||||
assert any(call.kwargs.get("session_id") == "custom-session" for call in calls)
|
||||
|
||||
|
||||
def test_repl_handles_chat_errors():
|
||||
"""repl should handle errors from chat() gracefully."""
|
||||
with (
|
||||
patch("builtins.input", side_effect=["hello", "exit"]),
|
||||
patch("timmy.session.chat") as mock_chat,
|
||||
):
|
||||
mock_chat.side_effect = Exception("Chat error")
|
||||
result = runner.invoke(app, ["repl"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Error" in result.output
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool confirmation gate
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user