Files
simonw--llm/tests/test_cli_streaming.py
2026-07-13 12:48:46 +08:00

160 lines
4.8 KiB
Python

"""Tests for CLI streaming display: reasoning → stderr (dim),
text → stdout, -R / --hide-reasoning flag.
"""
import click
from click.testing import CliRunner
import llm
from llm.cli import cli
def test_text_goes_to_stdout_not_stderr(mock_model):
mock_model.enqueue(["Hello world"])
runner = CliRunner()
result = runner.invoke(
cli,
["-m", "mock", "hi", "--no-log"],
catch_exceptions=False,
)
assert result.exit_code == 0
assert "Hello world" in result.stdout
# No reasoning was emitted — stderr should be empty.
assert result.stderr == ""
def test_reasoning_goes_to_stderr_not_stdout(mock_model):
mock_model.enqueue(
[
llm.parts.StreamEvent(
type="reasoning", chunk="thinking hard", part_index=0
),
llm.parts.StreamEvent(type="text", chunk="answer", part_index=1),
]
)
runner = CliRunner()
result = runner.invoke(
cli,
["-m", "mock", "hi", "--no-log"],
catch_exceptions=False,
)
assert result.exit_code == 0
assert "thinking hard" in result.stderr
assert "thinking hard" not in result.stdout
assert "answer" in result.stdout
def test_reasoning_rendered_in_dim_style(mock_model):
"""The click.style(..., dim=True) wrapper emits the ANSI dim code."""
mock_model.enqueue(
[
llm.parts.StreamEvent(type="reasoning", chunk="t", part_index=0),
llm.parts.StreamEvent(type="text", chunk="x", part_index=1),
]
)
runner = CliRunner()
result = runner.invoke(
cli,
["-m", "mock", "hi", "--no-log"],
catch_exceptions=False,
color=True,
)
assert result.exit_code == 0
# ANSI dim escape sequence is \x1b[2m
dim_start = click.style("x", dim=True).split("x", 1)[0]
assert dim_start in result.stderr
def test_hide_reasoning_flag_suppresses_reasoning(mock_model):
mock_model.enqueue(
[
llm.parts.StreamEvent(
type="reasoning", chunk="hidden thinking", part_index=0
),
llm.parts.StreamEvent(type="text", chunk="answer", part_index=1),
]
)
runner = CliRunner()
result = runner.invoke(
cli,
["-m", "mock", "hi", "--no-log", "--hide-reasoning"],
catch_exceptions=False,
)
assert result.exit_code == 0
assert "hidden thinking" not in result.stderr
assert "hidden thinking" not in result.stdout
assert "answer" in result.stdout
assert mock_model.history[0][0].hide_reasoning is True
def test_hide_reasoning_short_flag_R(mock_model):
mock_model.enqueue(
[
llm.parts.StreamEvent(type="reasoning", chunk="hidden", part_index=0),
llm.parts.StreamEvent(type="text", chunk="x", part_index=1),
]
)
runner = CliRunner()
result = runner.invoke(
cli,
["-m", "mock", "hi", "--no-log", "-R"],
catch_exceptions=False,
)
assert result.exit_code == 0
assert "hidden" not in result.stderr
def test_newline_between_reasoning_and_text(mock_model):
"""When reasoning ends and text begins, stderr gets a newline so the
text on stdout starts on a fresh visual line."""
mock_model.enqueue(
[
llm.parts.StreamEvent(type="reasoning", chunk="think", part_index=0),
llm.parts.StreamEvent(type="text", chunk="answer", part_index=1),
]
)
runner = CliRunner()
result = runner.invoke(
cli,
["-m", "mock", "hi", "--no-log"],
catch_exceptions=False,
)
assert result.exit_code == 0
# Reasoning ends, then a newline is emitted on stderr.
assert result.stderr.rstrip("\n").endswith("think") or "think\n" in result.stderr
def test_async_path_reasoning_to_stderr(async_mock_model):
async_mock_model.enqueue(
[
llm.parts.StreamEvent(
type="reasoning", chunk="async thinking", part_index=0
),
llm.parts.StreamEvent(type="text", chunk="async answer", part_index=1),
]
)
runner = CliRunner()
result = runner.invoke(
cli,
["-m", "mock", "hi", "--async", "--no-log"],
catch_exceptions=False,
)
assert result.exit_code == 0
assert "async thinking" in result.stderr
assert "async answer" in result.stdout
def test_plain_str_plugin_still_works(mock_model):
"""A plugin that yields plain strings (legacy) still displays
correctly — no reasoning branch, everything to stdout."""
mock_model.enqueue(["plain ", "text"])
runner = CliRunner()
result = runner.invoke(
cli,
["-m", "mock", "hi", "--no-log"],
catch_exceptions=False,
)
assert result.exit_code == 0
assert "plain text" in result.stdout
assert result.stderr == ""