honcho/honcho-cli/tests/test_output.py

140 lines
5.1 KiB
Python

"""Transcript rendering: timestamp normalization and content fidelity.
`session view` is a debugging surface, so the human-mode table must show what
was actually stored — no Markdown reflow, no truncated identifiers.
"""
from __future__ import annotations
from datetime import datetime, timezone
import pytest
from honcho_cli import output
from honcho_cli.output import _format_timestamp, print_transcript
@pytest.fixture
def render(monkeypatch, capsys):
"""Render a transcript in human mode at a fixed width and return stdout."""
monkeypatch.setattr(output, "is_tty", lambda: True)
monkeypatch.setattr(output, "_force_json", False)
def _render(messages, width: int = 120, **kwargs):
monkeypatch.setattr(output, "stdout_console", output.Console(width=width, no_color=True))
print_transcript(messages, **kwargs)
return capsys.readouterr().out
return _render
def _msg(content: str = "hi", **overrides) -> dict:
return {
"id": "V1StGXR8_Z5jdHi6B-myT",
"peer_id": "alice",
"content": content,
"created_at": "2026-01-01T00:00:00Z",
**overrides,
}
class TestFormatTimestamp:
@pytest.mark.parametrize(
("value", "expected"),
[
("2026-01-01T00:00:00Z", "2026-01-01T00:00:00.000Z"),
("2026-01-01T00:00:00+00:00", "2026-01-01T00:00:00.000Z"),
("2026-01-01 00:00:00+00:00", "2026-01-01T00:00:00.000Z"),
("2026-01-01 00:00:00", "2026-01-01T00:00:00.000Z"),
("2026-01-01T00:00:00.080000Z", "2026-01-01T00:00:00.080Z"),
],
)
def test_normalizes_utc_shapes(self, value, expected):
assert _format_timestamp(value) == expected
@pytest.mark.parametrize(
("value", "expected"),
[
("2026-01-01T14:30:00-05:00", "2026-01-01T19:30:00.000Z"),
("2026-01-01T14:30:00+02:00", "2026-01-01T12:30:00.000Z"),
],
)
def test_converts_offsets_instead_of_relabelling_them(self, value, expected):
"""An offset must be converted to UTC, not dropped and stamped `Z`."""
assert _format_timestamp(value) == expected
def test_keeps_sub_second_precision(self):
"""Messages inside the same second must stay distinguishable."""
a = _format_timestamp("2026-01-01T00:00:03.000Z")
b = _format_timestamp("2026-01-01T00:00:03.080Z")
assert a != b
assert (a, b) == ("2026-01-01T00:00:03.000Z", "2026-01-01T00:00:03.080Z")
def test_accepts_datetime_objects(self):
value = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
assert _format_timestamp(value) == "2026-01-01T12:00:00.000Z"
@pytest.mark.parametrize("value", [None, ""])
def test_empty_values_render_blank(self, value):
assert _format_timestamp(value) == ""
def test_unparseable_values_pass_through(self):
assert _format_timestamp("not-a-date") == "not-a-date"
def test_output_matches_the_declared_column_width(self):
assert len(_format_timestamp("2026-01-01T00:00:00Z")) == output.TIMESTAMP_WIDTH
class TestTranscriptFidelity:
def test_newlines_are_not_reflowed_into_a_paragraph(self, render):
out = render([_msg("line one\nline two\nline three")], session_id="s1")
assert "line one line two line three" not in out
for line in ("line one", "line two", "line three"):
assert line in out
def test_tagged_content_is_not_stripped(self, render):
"""Agent transcripts are full of `<thinking>`-style tags; they must survive."""
out = render([_msg("<thinking>reasoning</thinking> answer")], session_id="s1")
assert "<thinking>" in out
assert "</thinking>" in out
def test_console_markup_is_not_interpreted(self, render):
out = render([_msg("literal [bold]not markup[/bold] text")], session_id="s1")
assert "[bold]" in out
def test_ids_are_shown_in_full(self, render):
"""A displayed ID must be usable with `honcho message get`."""
out = render([_msg()], session_id="s1", show_ids=True)
assert "V1StGXR8_Z5jdHi6B-myT" in out
assert "" not in out
def test_long_peer_ids_stay_distinguishable(self, render):
out = render(
[
_msg(peer_id="user_1234567890abcdef"),
_msg(peer_id="user_1234567890abcXYZ"),
],
session_id="s1",
)
assert "abcdef" in out
assert "abcXYZ" in out
def test_empty_transcript_reports_the_session(self, render):
out = render([], session_id="s1")
assert "s1" in out
assert "(empty)" in out
class TestNextPageHint:
def test_given_hint_is_printed(self, render, monkeypatch):
printed: list[str] = []
monkeypatch.setattr(output, "status", printed.append)
render([_msg()], session_id="s1", page=1, pages=3, next_page_hint="honcho ... --page 2")
assert printed == ["more: honcho ... --page 2"]
def test_no_hint_when_none_given(self, render, monkeypatch):
printed: list[str] = []
monkeypatch.setattr(output, "status", printed.append)
render([_msg()], session_id="s1", page=3, pages=3)
assert printed == []