chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,359 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user