4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
360 lines
11 KiB
Python
360 lines
11 KiB
Python
"""Tests for the grouped /help picker renderer."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import re
|
|
import sys
|
|
|
|
from platform.terminal import theme as ui_theme
|
|
from surfaces.interactive_shell.command_registry.types import SlashCommand
|
|
from surfaces.interactive_shell.ui.help import help_menu
|
|
|
|
_ANSI_RE = re.compile(r"\x1b\[[0-9;:]*[A-Za-z]")
|
|
|
|
|
|
def _cmd(name: str, description: str | None = None) -> SlashCommand:
|
|
return SlashCommand(name, description or f"{name} description", lambda *_args: True)
|
|
|
|
|
|
def test_flatten_help_rows_preserves_category_headers() -> None:
|
|
rows = help_menu._flatten_help_rows(
|
|
[
|
|
("Session", [_cmd("/status")]),
|
|
("Tasks", [_cmd("/tasks"), _cmd("/cancel")]),
|
|
]
|
|
)
|
|
|
|
assert [row.section for row in rows if row.section is not None] == ["Session", "Tasks"]
|
|
assert sum(1 for row in rows if row.separator) == 1
|
|
assert [row.command.name for row in rows if row.command is not None] == [
|
|
"/status",
|
|
"/tasks",
|
|
"/cancel",
|
|
]
|
|
|
|
|
|
def test_navigation_skips_category_headers() -> None:
|
|
rows = help_menu._flatten_help_rows(
|
|
[
|
|
("Session", [_cmd("/status")]),
|
|
("Tasks", [_cmd("/tasks")]),
|
|
]
|
|
)
|
|
|
|
assert help_menu._first_selectable_index(rows) == 1
|
|
assert help_menu._next_selectable_index(rows, 1, 1) == 4
|
|
assert help_menu._next_selectable_index(rows, 4, -1) == 1
|
|
|
|
|
|
def test_draw_help_menu_renders_horizontal_category_dividers(monkeypatch) -> None:
|
|
rows = help_menu._flatten_help_rows(
|
|
[
|
|
("Session", [_cmd("/status")]),
|
|
("Tasks", [_cmd("/tasks")]),
|
|
]
|
|
)
|
|
out = io.StringIO()
|
|
monkeypatch.setattr(sys, "stdout", out)
|
|
monkeypatch.setattr(help_menu, "menu_columns", lambda: 40)
|
|
|
|
help_menu._draw_help_menu(
|
|
rows,
|
|
selected=1,
|
|
expanded=None,
|
|
erase_lines=0,
|
|
viewport_height=7,
|
|
)
|
|
|
|
plain = _ANSI_RE.sub("", out.getvalue())
|
|
assert "Session" in plain
|
|
assert "Tasks" in plain
|
|
assert plain.count(help_menu._separator_rule(40)) >= 2
|
|
assert "┼" in plain
|
|
assert help_menu._render_grid_row("Session", "", 40) in plain
|
|
assert help_menu._render_grid_row("Tasks", "", 40) in plain
|
|
|
|
|
|
def test_section_rows_keep_divider_dim() -> None:
|
|
rendered = help_menu._render_help_row(
|
|
help_menu.HelpRow(section="Investigation"),
|
|
selected=False,
|
|
expanded=False,
|
|
width=40,
|
|
)
|
|
|
|
assert f"{ui_theme.BOLD_BRAND_ANSI}Investigation" in rendered
|
|
assert f"{ui_theme.DIM_COUNTER_ANSI}│" in rendered
|
|
|
|
|
|
def test_detail_rows_use_text_labels_dim_values_and_dim_divider() -> None:
|
|
label = help_menu._render_display_row(
|
|
help_menu.HelpDisplayRow(detail=help_menu.HelpDetailLine("usage:", "label")),
|
|
selected=False,
|
|
expanded=False,
|
|
width=40,
|
|
)
|
|
value = help_menu._render_display_row(
|
|
help_menu.HelpDisplayRow(detail=help_menu.HelpDetailLine(" /help", "value")),
|
|
selected=False,
|
|
expanded=False,
|
|
width=40,
|
|
)
|
|
|
|
assert f"{ui_theme.DIM_COUNTER_ANSI}{' ' * help_menu._left_column_width(40)}│ " in label
|
|
assert f"{ui_theme.TEXT_ANSI}usage:" in label
|
|
assert f"{ui_theme.DIM_COUNTER_ANSI}{' ' * help_menu._left_column_width(40)}│ " in value
|
|
assert f"{ui_theme.DIM_COUNTER_ANSI} /help" in value
|
|
|
|
|
|
def test_draw_help_menu_centers_title(monkeypatch) -> None:
|
|
rows = help_menu._flatten_help_rows([("Session", [_cmd("/status")])])
|
|
out = io.StringIO()
|
|
monkeypatch.setattr(sys, "stdout", out)
|
|
monkeypatch.setattr(help_menu, "menu_columns", lambda: 40)
|
|
|
|
help_menu._draw_help_menu(
|
|
rows,
|
|
selected=1,
|
|
expanded=None,
|
|
erase_lines=0,
|
|
viewport_height=7,
|
|
)
|
|
|
|
plain_lines = _ANSI_RE.sub("", out.getvalue()).splitlines()
|
|
assert " Slash commands " in plain_lines
|
|
|
|
|
|
def test_draw_help_menu_uses_bounded_viewport_and_category_context(monkeypatch) -> None:
|
|
sections = [
|
|
("Session", [_cmd(f"/session-{index}") for index in range(10)]),
|
|
("Tasks", [_cmd(f"/task-{index}") for index in range(10)]),
|
|
]
|
|
rows = help_menu._flatten_help_rows(sections)
|
|
selected = next(
|
|
index for index, row in enumerate(rows) if row.command and row.command.name == "/task-2"
|
|
)
|
|
out = io.StringIO()
|
|
monkeypatch.setattr(sys, "stdout", out)
|
|
monkeypatch.setattr(help_menu, "menu_columns", lambda: 90)
|
|
|
|
height = help_menu._draw_help_menu(
|
|
rows,
|
|
selected=selected,
|
|
expanded=None,
|
|
erase_lines=0,
|
|
viewport_height=7,
|
|
)
|
|
|
|
rendered = out.getvalue()
|
|
plain = _ANSI_RE.sub("", rendered)
|
|
assert height == 13
|
|
assert "Tasks" in plain
|
|
assert "/task-2" in plain
|
|
assert "/session-0" not in plain
|
|
assert "/task-9" not in plain
|
|
|
|
|
|
def test_draw_help_menu_expands_selected_command_inline(monkeypatch) -> None:
|
|
command = SlashCommand(
|
|
"/new",
|
|
"Start a new session keeping conversation context.",
|
|
lambda *_args: True,
|
|
usage=("/new",),
|
|
notes=("Unlike /clear, /new preserves the LLM conversation context.",),
|
|
)
|
|
rows = help_menu._flatten_help_rows([("Session", [command])])
|
|
out = io.StringIO()
|
|
monkeypatch.setattr(sys, "stdout", out)
|
|
monkeypatch.setattr(help_menu, "menu_columns", lambda: 90)
|
|
|
|
help_menu._draw_help_menu(
|
|
rows,
|
|
selected=1,
|
|
expanded=1,
|
|
erase_lines=0,
|
|
viewport_height=7,
|
|
)
|
|
|
|
plain = _ANSI_RE.sub("", out.getvalue())
|
|
assert "/new" in plain
|
|
assert "> ▾ /new" in plain
|
|
assert "│ Start a new session" in plain
|
|
assert help_menu._render_grid_row("", "usage:", 90) in plain
|
|
assert "usage:" in plain
|
|
assert "LLM conversation context" in plain
|
|
|
|
|
|
def test_draw_help_menu_marks_expandable_and_plain_commands(monkeypatch) -> None:
|
|
expandable = SlashCommand(
|
|
"/trust",
|
|
"Manage trust mode.",
|
|
lambda *_args: True,
|
|
usage=("/trust on", "/trust off"),
|
|
)
|
|
plain_command = _cmd("/status", "Show session status.")
|
|
rows = help_menu._flatten_help_rows([("Session", [expandable, plain_command])])
|
|
out = io.StringIO()
|
|
monkeypatch.setattr(sys, "stdout", out)
|
|
monkeypatch.setattr(help_menu, "menu_columns", lambda: 90)
|
|
|
|
help_menu._draw_help_menu(
|
|
rows,
|
|
selected=1,
|
|
expanded=None,
|
|
erase_lines=0,
|
|
viewport_height=7,
|
|
)
|
|
|
|
plain = _ANSI_RE.sub("", out.getvalue())
|
|
assert "> ▸ /trust" in plain
|
|
assert "│ Manage trust mode." in plain
|
|
assert " /status" in plain
|
|
assert "▸ /status" not in plain
|
|
|
|
|
|
def test_command_rows_highlight_only_unselected_command_name() -> None:
|
|
rendered = help_menu._render_command_row(
|
|
SlashCommand(
|
|
"/trust",
|
|
"Manage trust mode.",
|
|
lambda *_args: True,
|
|
usage=("/trust on", "/trust off"),
|
|
),
|
|
selected=False,
|
|
expanded=False,
|
|
width=60,
|
|
)
|
|
|
|
assert (
|
|
f"{ui_theme.DIM_COUNTER_ANSI} ▸ {ui_theme.ANSI_RESET}{ui_theme.HIGHLIGHT_ANSI}/trust"
|
|
) in rendered
|
|
assert f"{ui_theme.HIGHLIGHT_ANSI}▸" not in rendered
|
|
|
|
|
|
def test_help_menu_command_rows_track_active_theme() -> None:
|
|
from platform.terminal.theme import set_active_theme
|
|
|
|
set_active_theme("green")
|
|
green_highlight = ui_theme.HIGHLIGHT_ANSI
|
|
set_active_theme("purple")
|
|
rendered = help_menu._render_command_row(
|
|
SlashCommand("/trust", "Manage trust mode.", lambda *_args: True),
|
|
selected=False,
|
|
expanded=False,
|
|
width=60,
|
|
)
|
|
|
|
assert ui_theme.HIGHLIGHT_ANSI in rendered
|
|
assert green_highlight not in rendered
|
|
|
|
|
|
def test_expanded_detail_lines_include_all_usage_examples_and_notes() -> None:
|
|
command = SlashCommand(
|
|
"/model",
|
|
"Show model settings.",
|
|
lambda *_args: True,
|
|
usage=tuple(f"/model action {index}" for index in range(10)),
|
|
examples=("/model set openai",),
|
|
notes=("Provider defaults can be restored.",),
|
|
)
|
|
|
|
lines = help_menu._expanded_detail_lines(command)
|
|
text_lines = [line.text for line in lines]
|
|
|
|
assert lines[0].role == "label"
|
|
assert lines[1].role == "value"
|
|
assert "/model action 0" in lines[1].text
|
|
assert " /model action 9" in text_lines
|
|
assert "examples:" in text_lines
|
|
assert " /model set openai" in text_lines
|
|
assert "notes:" in text_lines
|
|
assert " Provider defaults can be restored." in text_lines
|
|
assert "…" not in text_lines
|
|
|
|
|
|
def test_draw_help_menu_expands_viewport_to_show_all_details(monkeypatch) -> None:
|
|
command = SlashCommand(
|
|
"/model",
|
|
"Show model settings.",
|
|
lambda *_args: True,
|
|
usage=tuple(f"/model action {index}" for index in range(10)),
|
|
)
|
|
rows = help_menu._flatten_help_rows([("Models", [command, _cmd("/status")])])
|
|
out = io.StringIO()
|
|
monkeypatch.setattr(sys, "stdout", out)
|
|
monkeypatch.setattr(help_menu, "menu_columns", lambda: 90)
|
|
|
|
height = help_menu._draw_help_menu(
|
|
rows,
|
|
selected=1,
|
|
expanded=1,
|
|
erase_lines=0,
|
|
viewport_height=7,
|
|
)
|
|
|
|
plain = _ANSI_RE.sub("", out.getvalue())
|
|
assert height > 13
|
|
assert "/model action 0" in plain
|
|
assert "/model action 9" in plain
|
|
assert "…" not in plain
|
|
|
|
|
|
def test_choose_help_command_space_toggles_inline_details_and_exits(monkeypatch) -> None:
|
|
sections = [
|
|
(
|
|
"Session",
|
|
[
|
|
_cmd("/status"),
|
|
SlashCommand(
|
|
"/trust",
|
|
"Manage trust mode.",
|
|
lambda *_args: True,
|
|
usage=("/trust on", "/trust off"),
|
|
),
|
|
],
|
|
)
|
|
]
|
|
out = io.StringIO()
|
|
actions = iter(["down", "space", "cancel"])
|
|
monkeypatch.setattr(sys, "stdout", out)
|
|
monkeypatch.setattr(help_menu, "menu_columns", lambda: 80)
|
|
monkeypatch.setattr(help_menu, "read_menu_action", lambda: next(actions))
|
|
|
|
selected = help_menu.choose_help_command(sections)
|
|
|
|
assert selected is None
|
|
rendered = out.getvalue()
|
|
assert "\x1b[" in rendered
|
|
assert "A\r\x1b[J" in rendered
|
|
plain = _ANSI_RE.sub("", rendered)
|
|
assert "/trust on" in plain
|
|
|
|
|
|
def test_choose_help_command_enter_selects_command_without_details(monkeypatch) -> None:
|
|
sections = [("Session", [_cmd("/status")])]
|
|
out = io.StringIO()
|
|
actions = iter(["enter"])
|
|
monkeypatch.setattr(sys, "stdout", out)
|
|
monkeypatch.setattr(help_menu, "menu_columns", lambda: 80)
|
|
monkeypatch.setattr(help_menu, "read_menu_action", lambda: next(actions))
|
|
|
|
assert help_menu.choose_help_command(sections) == "/status"
|
|
|
|
plain = _ANSI_RE.sub("", out.getvalue())
|
|
assert "No additional usage." not in plain
|
|
|
|
|
|
def test_choose_help_command_ignores_unknown_actions(monkeypatch) -> None:
|
|
sections = [("Session", [_cmd("/status")])]
|
|
out = io.StringIO()
|
|
actions = iter(["ignore", "cancel"])
|
|
monkeypatch.setattr(sys, "stdout", out)
|
|
monkeypatch.setattr(help_menu, "menu_columns", lambda: 80)
|
|
monkeypatch.setattr(help_menu, "read_menu_action", lambda: next(actions))
|
|
|
|
assert help_menu.choose_help_command(sections) is None
|
|
|
|
rendered = out.getvalue()
|
|
assert rendered.count("Slash commands") == 2
|