simonw--llm
d8ada58d32
Test / test (ubuntu-latest, 3.14) (push) Has been cancelled
Test / test (windows-latest, 3.10) (push) Has been cancelled
Test / test (windows-latest, 3.11) (push) Has been cancelled
Test / test (windows-latest, 3.12) (push) Has been cancelled
Test / test (windows-latest, 3.13) (push) Has been cancelled
Test / test (windows-latest, 3.14) (push) Has been cancelled
Test / test (macos-latest, 3.10) (push) Has been cancelled
Test / test (macos-latest, 3.11) (push) Has been cancelled
Test / test (macos-latest, 3.12) (push) Has been cancelled
Test / test (macos-latest, 3.13) (push) Has been cancelled
Test / test (macos-latest, 3.14) (push) Has been cancelled
Test / test (ubuntu-latest, 3.10) (push) Has been cancelled
Test / test (ubuntu-latest, 3.11) (push) Has been cancelled
Test / test (ubuntu-latest, 3.12) (push) Has been cancelled
Test / test (ubuntu-latest, 3.13) (push) Has been cancelled
So you can turn off display reasoning and compatible plugins can then opt not to request reasoning summaries from their models, even as they do actually run reasoning.
161 行
4.9 KiB
Python
161 行
4.9 KiB
Python
"""Tests for CLI streaming display: reasoning → stderr (dim),
|
|
text → stdout, -R / --no-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(mix_stderr=False)
|
|
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(mix_stderr=False)
|
|
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
|
|
assert mock_model.history[0][0].display_reasoning is True
|
|
|
|
|
|
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(mix_stderr=False)
|
|
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_no_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(mix_stderr=False)
|
|
result = runner.invoke(
|
|
cli,
|
|
["-m", "mock", "hi", "--no-log", "--no-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].display_reasoning is False
|
|
|
|
|
|
def test_no_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(mix_stderr=False)
|
|
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(mix_stderr=False)
|
|
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(mix_stderr=False)
|
|
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(mix_stderr=False)
|
|
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 == ""
|