hermes-agent/tests/hermes_cli/test_session_display_width.py
GatewayJ c026d66efc fix(cli): align session list table for CJK terminal width
Replace str.ljust-based columns with east_asian_width-aware padding so
`hermes sessions list` and inline /history session picks line up when
titles/previews mix Chinese and ASCII. Share one formatter via
print_sessions_table in hermes_cli/main.py.

test(cli): add coverage for display-width helpers in
tests/hermes_cli/test_session_display_width.py.
2026-04-15 10:20:04 +08:00

133 lines
4.7 KiB
Python

"""Tests for CJK-aware session list column padding (hermes_cli.main display helpers)."""
from unittest.mock import patch
from hermes_cli.main import (
_fit_display_width,
_pad_display_right,
_text_display_width,
print_sessions_table,
)
class TestTextDisplayWidth:
def test_empty(self) -> None:
assert _text_display_width("") == 0
def test_ascii(self) -> None:
assert _text_display_width("abc") == 3
def test_wide_cjk(self) -> None:
# Typical Han characters are wide (W) — two terminal cells each.
assert _text_display_width("中文") == 4
def test_mixed(self) -> None:
assert _text_display_width("a中") == 1 + 2
class TestFitDisplayWidth:
def test_no_truncation_when_fits(self) -> None:
assert _fit_display_width("hello", 10) == "hello"
def test_truncates_with_ellipsis(self) -> None:
out = _fit_display_width("abcdefghij", 5)
assert out.endswith("")
assert _text_display_width(out) <= 5
def test_cjk_truncation_respects_cells(self) -> None:
s = "一二三四五六"
out = _fit_display_width(s, 5)
assert out.endswith("")
assert _text_display_width(out) <= 5
def test_max_cells_zero_returns_empty(self) -> None:
assert _fit_display_width("hello", 0) == ""
class TestPadDisplayRight:
def test_pads_ascii_to_width(self) -> None:
assert _pad_display_right("hi", 8) == "hi "
assert _text_display_width(_pad_display_right("hi", 8)) == 8
def test_pads_mixed_to_display_width(self) -> None:
# "a" + two wide chars = 1 + 4 = 5 cells; pad to 8 → 3 spaces
got = _pad_display_right("a中文", 8)
assert _text_display_width(got) == 8
assert got.startswith("a中文")
def test_truncates_before_pad_when_too_long(self) -> None:
long = "x" * 80
got = _pad_display_right(long, 8)
assert _text_display_width(got) == 8
assert got.endswith("")
class TestPrintSessionsTable:
"""Smoke + fixed-clock integration so Last Active is deterministic."""
def test_with_titles_prints_header_and_row(self, capsys) -> None:
fixed_now = 1_700_000_000.0
with patch("hermes_cli.main._time.time", return_value=fixed_now):
print_sessions_table(
[
{
"title": "Rust 项目",
"preview": "分析当前",
"last_active": fixed_now - 400,
"id": "20260415_testsession",
}
],
has_titles=True,
)
out = capsys.readouterr().out
assert "Title" in out and "Preview" in out and "Last Active" in out
assert "Rust 项目" in out
assert "20260415_testsession" in out
assert "6m ago" in out
lines = [ln for ln in out.splitlines() if ln.strip()]
assert len(lines) >= 3
def test_without_titles_includes_source(self, capsys) -> None:
fixed_now = 1_700_000_000.0
with patch("hermes_cli.main._time.time", return_value=fixed_now):
print_sessions_table(
[
{
"preview": "hello",
"last_active": fixed_now - 90,
"source": "cli",
"id": "id_only",
}
],
has_titles=False,
)
out = capsys.readouterr().out
assert "Preview" in out and "Src" in out
assert "cli" in out and "id_only" in out
def test_indent_prefix(self, capsys) -> None:
fixed_now = 1_700_000_000.0
with patch("hermes_cli.main._time.time", return_value=fixed_now):
print_sessions_table(
[{"title": "t", "preview": "p", "last_active": fixed_now, "id": "i"}],
has_titles=True,
indent=" ",
)
out = capsys.readouterr().out
for line in out.splitlines():
if line.strip():
assert line.startswith(" ")
def test_row_columns_have_expected_display_widths(self) -> None:
"""Guardrail: titled row layout matches fixed column budgets + spaces."""
title = _pad_display_right("ColdStore Rust 项目", 32)
preview = _pad_display_right("分析当前项目", 40)
last = _pad_display_right("6m ago", 13)
sid = "20260415_081027_6b979d"
line = f"{title} {preview} {last} {sid}"
assert _text_display_width(title) == 32
assert _text_display_width(preview) == 40
assert _text_display_width(last) == 13
# Single space between padded columns, then unbounded id.
assert line == f"{title} {preview} {last} {sid}"