Files
teknium1 81cec06c77 test(cli): banner hero line is not center-padded
Invariant test for #9879 (red on origin/main: Rich inserted centering spaces
before the braille-padded hero). Adapted from PR #9880's test to the current
banner internals.
2026-09-12 08:23:58 -07:00

138 lines
5.6 KiB
Python

"""Tests for banner toolset name normalization and skin color usage."""
from unittest.mock import patch
from rich.console import Console
import hermes_cli.banner as banner
import model_tools
import tools.mcp_tool_discovery
def test_cprint_falls_back_to_plain_print_when_prompt_toolkit_has_no_console(capsys):
with patch(
"prompt_toolkit.print_formatted_text",
side_effect=RuntimeError("no console screen buffer"),
):
banner.cprint("fallback text")
assert capsys.readouterr().out == "fallback text\n"
def test_build_welcome_banner_title_falls_back_when_no_tag():
"""Without a resolvable tag, the panel title renders as plain text (no hyperlink escape)."""
import io
from unittest.mock import patch as _patch
import hermes_cli.banner as _banner
import model_tools as _mt
import tools.mcp_tool as _mcp
from tools import mcp_tool_discovery as _mcp_discovery
_banner._latest_release_cache = None
buf = io.StringIO()
with (
_patch.object(_mt, "check_tool_availability", return_value=(["web"], [])),
_patch.object(_banner, "get_available_skills", return_value={}),
_patch.object(_banner, "get_update_result", return_value=None),
_patch.object(_mcp_discovery, "get_mcp_status", return_value=[]),
_patch.object(_banner, "get_latest_release_tag", return_value=None),
):
console = Console(file=buf, force_terminal=True, color_system="truecolor", width=160)
_banner.build_welcome_banner(
console=console, model="x", cwd="/tmp",
session_id="abc123",
tools=[{"function": {"name": "read_file"}}],
get_toolset_for_tool=lambda n: "file",
)
raw = buf.getvalue()
assert "Hermes Agent v" in raw, "Version label missing from title"
assert "\x1b]8;" not in raw, "OSC-8 hyperlink should not be emitted without a tag"
def test_build_welcome_banner_non_moa_unchanged(tmp_path, monkeypatch):
"""A normal provider still renders the bare model slug, no MoA prefix."""
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
(tmp_path / ".hermes").mkdir()
with (
patch.object(model_tools, "check_tool_availability", return_value=([], [])),
patch.object(banner, "get_available_skills", return_value={}),
patch.object(banner, "get_update_result", return_value=None),
patch.object(tools.mcp_tool_discovery, "get_mcp_status", return_value=[]),
):
console = Console(record=True, force_terminal=False, color_system=None, width=160)
banner.build_welcome_banner(
console=console,
model="anthropic/claude-opus-4.8",
cwd="/tmp/project",
tools=[],
enabled_toolsets=[],
provider="openrouter",
)
out = console.export_text()
assert "claude-opus-4.8" in out
assert "MoA:" not in out
def test_empty_model_shows_the_free_tier_route_when_it_carries_inference(tmp_path, monkeypatch):
"""The banner prints before credentials resolve, so ``model`` is empty on a fresh install. On the
free tier the route is known locally (identity on disk + tier on): the banner shows its model.
When nothing resolves the red "no model configured" line stays."""
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
(tmp_path / ".hermes").mkdir()
import hermes_cli.anon_auth as anon_auth
def render(carries: bool) -> str:
with (
patch.object(model_tools, "check_tool_availability", return_value=([], [])),
patch.object(banner, "get_available_skills", return_value={}),
patch.object(banner, "get_update_result", return_value=None),
patch.object(tools.mcp_tool_discovery, "get_mcp_status", return_value=[]),
patch.object(anon_auth, "guest_carries_inference", return_value=carries),
):
console = Console(record=True, force_terminal=False, color_system=None, width=160)
banner.build_welcome_banner(console=console, model="", cwd="/tmp/project", tools=[],
enabled_toolsets=[], provider="auto")
return console.export_text()
assert "welcome" in render(True) and "no model configured" not in render(True)
assert "no model configured" in render(False)
def test_build_welcome_banner_does_not_center_pad_hero_art():
"""A braille hero relies on its own U+2800 padding for symmetry; Rich centering inserts
ASCII spaces around the left column and distorts the silhouette (#9879). The hero line
must start flush at the column start."""
import io
from types import SimpleNamespace
from tools import mcp_tool_discovery as _mcp_discovery
skin = SimpleNamespace(banner_hero="[green]\u2800X[/]", banner_logo="")
buf = io.StringIO()
with (
patch.object(model_tools, "check_tool_availability", return_value=([], [])),
patch.object(banner, "get_available_skills", return_value={}),
patch.object(banner, "get_update_result", return_value=None),
patch.object(banner, "get_latest_release_tag", return_value=None),
patch.object(_mcp_discovery, "get_mcp_status", return_value=[]),
patch.object(banner, "_active_skin", return_value=skin),
):
console = Console(file=buf, force_terminal=False, color_system=None, width=80)
banner.build_welcome_banner(console=console, model="m", cwd="/tmp", tools=[],
get_toolset_for_tool=lambda _: None)
hero_line = next(line for line in buf.getvalue().splitlines() if "\u2800X" in line)
assert hero_line.startswith("\u2502 \u2800X"), repr(hero_line)