Files
temp/features/steps/tui_slash_command_overlay_coverage_steps.py
freemo 0f9ca00ce7 fix(tui): show command descriptions in SlashCommandOverlay alongside command names
Update SlashCommandOverlay.set_commands() to accept list[SlashCommandSpec]
instead of list[str], and render each entry as aligned columns:
  /command-name       Description text

Changes:
- slash_command_overlay.py: update set_commands() signature to accept
  list[SlashCommandSpec]; render name + description with aligned padding
  using _COMMAND_COL_WIDTH = 28 characters for the name column
- slash_catalog.py: add slash_command_specs() helper returning all specs
- app.py: switch on_mount() to call slash_command_specs() instead of
  slash_command_names() so full spec objects are passed to the overlay
- features/tui_slash_command_overlay_coverage.feature: add scenario
  verifying description rendering; update existing scenarios to use
  SlashCommandSpec objects via _make_specs() helper
- features/steps/tui_slash_command_overlay_coverage_steps.py: update
  step implementations to build SlashCommandSpec objects from CSV names
- features/tui_slash_overlay_descriptions.feature: new feature file with
  BDD scenarios covering description rendering and @tdd_expected_fail
  capture of the pre-fix name-only behaviour
- features/steps/tui_slash_overlay_descriptions_steps.py: step defs for
  the new feature file
- robot/tui_smoke.robot: add Slash Command Overlay Renders Descriptions
  integration test verifying descriptions appear in rendered overlay

ISSUES CLOSED: #3437
2026-04-05 17:50:51 +00:00

184 lines
7.3 KiB
Python

"""Step definitions for tui_slash_command_overlay_coverage.feature.
These steps target uncovered lines in slash_command_overlay.py:
- Line 16: _FallbackStatic.__init__ sets self._text = ""
- Line 19: _FallbackStatic.update sets self._text = text
- Lines 31-32: set_commands filtering logic (query vs empty query)
- Line 34: lines = [f"/{query}"]
- Lines 35-36: loop appending filtered commands with descriptions
- Lines 37-38: "(no commands)" fallback when nothing matches
- Line 39: self.update("\n".join(lines))
"""
import importlib
from unittest.mock import patch
from behave import given, then, when
import cleveragents.tui.widgets.slash_command_overlay as _sco_mod
from cleveragents.tui.slash_catalog import SlashCommandSpec
from cleveragents.tui.widgets.slash_command_overlay import _load_static_base
def _make_specs(names_csv: str) -> list[SlashCommandSpec]:
"""Build a list of SlashCommandSpec from a CSV of command names."""
return [
SlashCommandSpec(
command=c.strip(), group="Test", description=f"Desc for {c.strip()}"
)
for c in names_csv.split(",")
if c.strip()
]
# ---------------------------------------------------------------------------
# Background — force FallbackStatic so tests never need a live Textual app
# ---------------------------------------------------------------------------
@given("the slash command overlay module is imported")
def step_module_imported(context):
"""Reload the module with textual import patched out to force fallback."""
with patch("importlib.import_module", side_effect=ImportError("no textual")):
importlib.reload(_sco_mod)
# Re-bind to module-level names after reload
context._sco_overlay_cls = _sco_mod.SlashCommandOverlay
assert context._sco_overlay_cls is not None
assert _load_static_base is not None
def _restore():
importlib.reload(_sco_mod)
context.add_cleanup(_restore)
# ---------------------------------------------------------------------------
# Instantiation (line 16)
# ---------------------------------------------------------------------------
@when("I create a SlashCommandOverlay instance")
def step_create_overlay(context):
"""Instantiate SlashCommandOverlay, hitting _FallbackStatic.__init__."""
context.overlay = context._sco_overlay_cls()
@then("the overlay internal text should be empty")
def step_verify_empty_text(context):
"""Verify _text was initialised to empty string (line 16)."""
assert context.overlay._text == ""
# ---------------------------------------------------------------------------
# update method (line 19)
# ---------------------------------------------------------------------------
@given("I have a SlashCommandOverlay instance")
def step_have_overlay(context):
"""Create and store an overlay instance for subsequent steps."""
context.overlay = context._sco_overlay_cls()
@when('I call update with "{text}"')
def step_call_update(context, text):
"""Call update on the overlay, hitting _FallbackStatic.update (line 19)."""
context.overlay.update(text)
@then('the overlay internal text should be "{expected}"')
def step_verify_text(context, expected):
"""Verify _text matches the expected value."""
assert context.overlay._text == expected, (
f"Expected '{expected}', got '{context.overlay._text}'"
)
# ---------------------------------------------------------------------------
# set_commands with query (lines 31-36, 39)
# ---------------------------------------------------------------------------
@when('I call set_commands with query "{query}" and commands "{commands_csv}"')
def step_call_set_commands(context, query, commands_csv):
"""Call set_commands with a query and a CSV list of commands (as SlashCommandSpec)."""
specs = _make_specs(commands_csv)
context.overlay.set_commands(query, specs)
@when('I call set_commands with empty query and commands "{commands_csv}"')
def step_call_set_commands_empty_query(context, commands_csv):
"""Call set_commands with an empty query and a CSV list of commands (as SlashCommandSpec)."""
specs = _make_specs(commands_csv)
context.overlay.set_commands("", specs)
@then('the overlay text should contain "{substring}"')
def step_text_contains(context, substring):
"""Verify the overlay text contains the expected substring."""
assert substring in context.overlay._text, (
f"Expected '{substring}' in:\n{context.overlay._text}"
)
@then('the overlay text should not contain "{substring}"')
def step_text_not_contains(context, substring):
"""Verify the overlay text does NOT contain the substring."""
assert substring not in context.overlay._text, (
f"Did not expect '{substring}' in:\n{context.overlay._text}"
)
# ---------------------------------------------------------------------------
# set_commands with empty query (line 32 else branch)
# ---------------------------------------------------------------------------
# Reuses the same step definitions above with query ""
# ---------------------------------------------------------------------------
# set_commands no matches (lines 37-38)
# ---------------------------------------------------------------------------
# Reuses step_call_set_commands; the "zzz" query triggers the no-commands path
# ---------------------------------------------------------------------------
# set_commands truncation to 12 entries (line 35 [:12])
# ---------------------------------------------------------------------------
@when(
'I call set_commands with query "" and {count:d} commands prefixed with "{prefix}"'
)
def step_call_set_commands_many(context, count, prefix):
"""Call set_commands with a large number of commands (as SlashCommandSpec)."""
specs = [
SlashCommandSpec(command=f"{prefix}{i}", group="Test", description=f"Desc {i}")
for i in range(count)
]
context.overlay.set_commands("", specs)
@then("the overlay text should have exactly {expected:d} lines")
def step_verify_line_count(context, expected):
"""Verify the number of lines in overlay text."""
lines = context.overlay._text.split("\n")
assert len(lines) == expected, (
f"Expected {expected} lines, got {len(lines)}: {lines}"
)
# ---------------------------------------------------------------------------
# Force fallback static base (lines 16, 19 via except path)
# ---------------------------------------------------------------------------
@when("I force the fallback static base to load")
def step_force_fallback(context):
"""Patch importlib.import_module to raise, forcing the fallback path."""
with patch("importlib.import_module", side_effect=ImportError("no textual")):
fallback_cls = _load_static_base()
context.fallback_cls = fallback_cls
context.fallback_instance = fallback_cls()
@then("the fallback class should be usable as a standalone object")
def step_verify_fallback_class(context):
"""Verify the fallback instance has the expected _text attribute."""
assert hasattr(context.fallback_instance, "_text")
assert context.fallback_instance._text == ""
@then("calling update on the fallback instance should store the text")
def step_verify_fallback_update(context):
"""Verify update stores text on the fallback instance (line 19)."""
context.fallback_instance.update("test content")
assert context.fallback_instance._text == "test content"