6b2a97ecda
CI / load-versions (pull_request) Successful in 14s
CI / push-validation (pull_request) Successful in 24s
CI / lint (pull_request) Successful in 45s
CI / quality (pull_request) Successful in 58s
CI / security (pull_request) Successful in 1m4s
CI / typecheck (pull_request) Successful in 1m12s
CI / build (pull_request) Successful in 40s
CI / helm (pull_request) Successful in 38s
CI / unit_tests (pull_request) Successful in 4m51s
CI / docker (pull_request) Successful in 1m29s
CI / integration_tests (pull_request) Successful in 10m12s
CI / coverage (pull_request) Successful in 9m40s
CI / status-check (pull_request) Successful in 3s
Move _MOCK_TEXTUAL_KEYS, _build_mock_textual, _install_mock_textual, _restore_modules, _make_persona_state, _cleanup_tmpdir, and _FakeCommandRouter out of tui_app_coverage_steps.py into a shared _tui_mock_helpers.py module so both step files can import them without duplication and the coverage steps file stays within the 500-line budget. ISSUES CLOSED: #6361
477 lines
18 KiB
Python
477 lines
18 KiB
Python
"""Step definitions for tui_app_coverage.feature targeting cleveragents.tui.app."""
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from behave import given, then, when
|
|
|
|
from features.steps._tui_helpers import _submit_text
|
|
from features.steps._tui_mock_helpers import (
|
|
_FakeCommandRouter,
|
|
_cleanup_tmpdir,
|
|
_install_mock_textual,
|
|
_make_persona_state,
|
|
_restore_modules,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Background
|
|
# ---------------------------------------------------------------------------
|
|
@given("the TUI app module is imported with mocked Textual")
|
|
def step_import_with_mock_textual(context):
|
|
"""Install mock Textual, reload app module, register cleanup."""
|
|
_install_mock_textual(context)
|
|
context.add_cleanup(lambda: _restore_modules(context))
|
|
context.add_cleanup(lambda: _cleanup_tmpdir(context))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Module-level import gate (lines 31-38)
|
|
# ---------------------------------------------------------------------------
|
|
@then("the reloaded app module should have _TEXTUAL_AVAILABLE True")
|
|
def step_textual_available_true(context):
|
|
assert context._tui_app_mod._TEXTUAL_AVAILABLE is True
|
|
|
|
|
|
@then("the reloaded app module should expose _TextualCleverAgentsTuiApp")
|
|
def step_has_textual_class(context):
|
|
assert hasattr(context._tui_app_mod, "_TextualCleverAgentsTuiApp")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# textual_available function (lines 43-45)
|
|
# ---------------------------------------------------------------------------
|
|
@then("textual_available should return True on the reloaded module")
|
|
def step_textual_available_returns_true(context):
|
|
assert context._tui_app_mod.textual_available() is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SessionView (lines 48-54)
|
|
# ---------------------------------------------------------------------------
|
|
@when('I create a SessionView with session_id "{sid}" and transcript entries')
|
|
def step_create_session_view(context, sid):
|
|
context._tui_session_view = context._tui_app_mod.SessionView(
|
|
session_id=sid, transcript=["line1", "line2"]
|
|
)
|
|
|
|
|
|
@then('the SessionView session_id should be "{sid}"')
|
|
def step_session_view_id(context, sid):
|
|
assert context._tui_session_view.session_id == sid
|
|
|
|
|
|
@then("the SessionView transcript should contain {count:d} entries")
|
|
def step_session_view_transcript_count(context, count):
|
|
assert len(context._tui_session_view.transcript) == count
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _CommandRouter protocol (lines 56-61)
|
|
# ---------------------------------------------------------------------------
|
|
@when("I create a _CommandRouter implementation")
|
|
def step_create_command_router_impl(context):
|
|
class MyRouter:
|
|
def handle(self, raw, *, session_id):
|
|
return f"response:{raw}:{session_id}"
|
|
|
|
context._tui_router_impl = MyRouter()
|
|
|
|
|
|
@then("calling handle on the implementation should return a response")
|
|
def step_call_handle(context):
|
|
result = context._tui_router_impl.handle("test", session_id="s1")
|
|
assert result == "response:test:s1"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# App instantiation (lines 91-100)
|
|
# ---------------------------------------------------------------------------
|
|
class _FakeCommandRouter:
|
|
"""Test command router that returns predictable responses."""
|
|
|
|
def handle(self, raw, *, session_id):
|
|
return f"handled:{raw}"
|
|
|
|
|
|
@given("a mock command router and persona state")
|
|
def step_create_mock_deps(context):
|
|
context._tui_cmd_router = _FakeCommandRouter()
|
|
context._tui_persona_state = _make_persona_state(context)
|
|
|
|
|
|
@when("I instantiate the Textual TUI app")
|
|
def step_instantiate_app(context):
|
|
AppClass = context._tui_app_mod._ResolvedTuiApp
|
|
context._tui_app = AppClass(
|
|
command_router=context._tui_cmd_router,
|
|
persona_state=context._tui_persona_state,
|
|
)
|
|
|
|
|
|
@then('the app should have a _session with session_id "{sid}"')
|
|
def step_app_session_id(context, sid):
|
|
assert context._tui_app._session.session_id == sid
|
|
|
|
|
|
@then("the app should store the command router")
|
|
def step_app_has_router(context):
|
|
assert context._tui_app._command_router is context._tui_cmd_router
|
|
|
|
|
|
@then("the app should store the persona state")
|
|
def step_app_has_persona_state(context):
|
|
assert context._tui_app._persona_state is context._tui_persona_state
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Class variables (lines 84-89)
|
|
# ---------------------------------------------------------------------------
|
|
@then('the app class should have CSS_PATH set to "{path}"')
|
|
def step_css_path(context, path):
|
|
assert path == context._tui_app.CSS_PATH
|
|
|
|
|
|
@then("the app class should have {count:d} key bindings")
|
|
def step_bindings_count(context, count):
|
|
assert len(context._tui_app.BINDINGS) == count
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# compose (lines 102-112)
|
|
# ---------------------------------------------------------------------------
|
|
@when("I call compose on the app")
|
|
def step_call_compose(context):
|
|
context._tui_compose_items = list(context._tui_app.compose())
|
|
|
|
|
|
@then("compose should yield at least {count:d} widgets")
|
|
def step_compose_count(context, count):
|
|
assert len(context._tui_compose_items) >= count
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# on_mount (lines 114-121)
|
|
# ---------------------------------------------------------------------------
|
|
@when("I call on_mount on the app")
|
|
def step_call_on_mount(context):
|
|
context._tui_app.on_mount()
|
|
|
|
|
|
@then("the persona bar should have content set")
|
|
def step_persona_bar_has_content(context):
|
|
from cleveragents.tui.widgets.persona_bar import PersonaBar
|
|
|
|
bar = context._tui_app.query_one("#persona-bar", PersonaBar)
|
|
assert hasattr(bar, "_text")
|
|
assert bar._text # non-empty
|
|
|
|
|
|
@then("the help panel should be hidden on mount")
|
|
def step_help_panel_hidden_on_mount(context):
|
|
from cleveragents.tui.widgets.help_panel_overlay import HelpPanelOverlay
|
|
|
|
panel = context._tui_app.query_one("#help-panel", HelpPanelOverlay)
|
|
assert panel.visible is False
|
|
assert panel._text == ""
|
|
|
|
|
|
@then("the reference picker should have suggestions initialised")
|
|
def step_ref_picker_initialised(context):
|
|
from cleveragents.tui.widgets.reference_picker import ReferencePickerOverlay
|
|
|
|
picker = context._tui_app.query_one("#reference-picker", ReferencePickerOverlay)
|
|
assert hasattr(picker, "_text")
|
|
|
|
|
|
@then("the slash overlay should be hidden on mount")
|
|
def step_slash_overlay_hidden_on_mount(context):
|
|
from cleveragents.tui.widgets.slash_command_overlay import SlashCommandOverlay
|
|
|
|
overlay = context._tui_app.query_one("#slash-overlay", SlashCommandOverlay)
|
|
assert overlay.visible is False
|
|
assert overlay._text == ""
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# action_help (lines 123-125)
|
|
# ---------------------------------------------------------------------------
|
|
@when("I call action_help on the app")
|
|
def step_call_action_help(context):
|
|
context._tui_app.action_help()
|
|
|
|
|
|
@when('I set the prompt text to "{text}"')
|
|
def step_set_prompt_text(context, text):
|
|
from cleveragents.tui.widgets.prompt import PromptInput
|
|
|
|
prompt = context._tui_app.query_one("#prompt", PromptInput)
|
|
prompt.value = text
|
|
|
|
|
|
@then('the conversation widget should contain "{text}"')
|
|
def step_conversation_contains(context, text):
|
|
MockStatic = context._tui_mock_static
|
|
conv = context._tui_app.query_one("#conversation", MockStatic)
|
|
assert text in conv._text, f"Expected '{text}' in '{conv._text}'"
|
|
|
|
|
|
@then('the help panel text should contain "{text}"')
|
|
def step_help_panel_contains(context, text):
|
|
from cleveragents.tui.widgets.help_panel_overlay import HelpPanelOverlay
|
|
|
|
panel = context._tui_app.query_one("#help-panel", HelpPanelOverlay)
|
|
assert text in panel._text, f"Expected '{text}' in '{panel._text}'"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# action_cycle_preset (lines 127-129)
|
|
# ---------------------------------------------------------------------------
|
|
@when("I call action_cycle_preset on the app")
|
|
def step_call_action_cycle_preset(context):
|
|
context._tui_app.action_cycle_preset()
|
|
|
|
|
|
@then("the persona bar content should be refreshed")
|
|
def step_persona_bar_refreshed(context):
|
|
from cleveragents.tui.widgets.persona_bar import PersonaBar
|
|
|
|
bar = context._tui_app.query_one("#persona-bar", PersonaBar)
|
|
assert bar._text # non-empty after refresh
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _refresh_persona_bar (lines 131-142)
|
|
# ---------------------------------------------------------------------------
|
|
@when("I call _refresh_persona_bar on the app")
|
|
def step_call_refresh_persona_bar(context):
|
|
context._tui_app._refresh_persona_bar()
|
|
|
|
|
|
@then("the persona bar should display the persona name and scope")
|
|
def step_persona_bar_shows_name(context):
|
|
from cleveragents.tui.widgets.persona_bar import PersonaBar
|
|
|
|
bar = context._tui_app.query_one("#persona-bar", PersonaBar)
|
|
assert "default" in bar._text
|
|
assert "scope refs" in bar._text
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# on_input_submitted: empty text (lines 144-150)
|
|
# ---------------------------------------------------------------------------
|
|
@when("I submit empty text to the app")
|
|
def step_submit_empty(context):
|
|
MockStatic = context._tui_mock_static
|
|
conv = context._tui_app.query_one("#conversation", MockStatic)
|
|
context._tui_conv_before = conv._text
|
|
_submit_text(context, "")
|
|
|
|
|
|
@then("the conversation widget should not be updated by input")
|
|
def step_conv_not_updated(context):
|
|
MockStatic = context._tui_mock_static
|
|
conv = context._tui_app.query_one("#conversation", MockStatic)
|
|
assert conv._text == context._tui_conv_before
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# on_input_submitted: command mode (lines 152-167)
|
|
# ---------------------------------------------------------------------------
|
|
# ---------------------------------------------------------------------------
|
|
# on_input_submitted: normal text with @ (lines 179-185)
|
|
# ---------------------------------------------------------------------------
|
|
@then("the reference picker should have been updated")
|
|
def step_ref_picker_updated(context):
|
|
from cleveragents.tui.widgets.reference_picker import ReferencePickerOverlay
|
|
|
|
picker = context._tui_app.query_one("#reference-picker", ReferencePickerOverlay)
|
|
assert hasattr(picker, "_text")
|
|
assert picker._text # updated with suggestions
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# action_escape cascade (issue #6450)
|
|
# ---------------------------------------------------------------------------
|
|
@when("I show the actor selection overlay")
|
|
def step_show_actor_selection(context):
|
|
from cleveragents.tui.widgets.actor_selection_overlay import (
|
|
ActorSelectionOverlay,
|
|
)
|
|
|
|
overlay = context._tui_app.query_one("#actor-selection", ActorSelectionOverlay)
|
|
overlay.show()
|
|
|
|
|
|
@when('I show the help panel overlay for context "{context_name}"')
|
|
def step_show_help_panel_context(context, context_name):
|
|
from cleveragents.tui.widgets.help_panel_overlay import HelpPanelOverlay
|
|
|
|
panel = context._tui_app.query_one("#help-panel", HelpPanelOverlay)
|
|
panel.show_context(context_name)
|
|
|
|
|
|
@when('I show the slash overlay for query "{query}"')
|
|
def step_show_slash_overlay(context, query):
|
|
from cleveragents.tui.widgets.slash_command_overlay import SlashCommandOverlay
|
|
|
|
overlay = context._tui_app.query_one("#slash-overlay", SlashCommandOverlay)
|
|
commands = [SimpleNamespace(command=query, description=f"/{query} command")]
|
|
overlay.set_commands(query, commands)
|
|
|
|
|
|
@when('I show the reference picker overlay for query "{query}"')
|
|
def step_show_reference_picker(context, query):
|
|
from cleveragents.tui.widgets.reference_picker import ReferencePickerOverlay
|
|
|
|
picker = context._tui_app.query_one("#reference-picker", ReferencePickerOverlay)
|
|
picker.set_suggestions(query, [query])
|
|
|
|
|
|
@when("I call action_escape on the app")
|
|
def step_call_action_escape(context):
|
|
context._tui_app.action_escape()
|
|
|
|
|
|
@then("the actor selection overlay should be hidden")
|
|
def step_actor_overlay_hidden(context):
|
|
from cleveragents.tui.widgets.actor_selection_overlay import (
|
|
ActorSelectionOverlay,
|
|
)
|
|
|
|
overlay = context._tui_app.query_one("#actor-selection", ActorSelectionOverlay)
|
|
assert overlay.visible is False
|
|
|
|
|
|
@then("the help panel should remain visible")
|
|
def step_help_panel_visible(context):
|
|
from cleveragents.tui.widgets.help_panel_overlay import HelpPanelOverlay
|
|
|
|
panel = context._tui_app.query_one("#help-panel", HelpPanelOverlay)
|
|
assert panel.visible is True
|
|
assert panel._text
|
|
|
|
|
|
@then("the slash overlay should remain visible")
|
|
def step_slash_overlay_visible(context):
|
|
from cleveragents.tui.widgets.slash_command_overlay import SlashCommandOverlay
|
|
|
|
overlay = context._tui_app.query_one("#slash-overlay", SlashCommandOverlay)
|
|
assert overlay.visible is True
|
|
assert overlay._text
|
|
|
|
|
|
@then("the slash overlay should be hidden")
|
|
def step_slash_overlay_hidden(context):
|
|
from cleveragents.tui.widgets.slash_command_overlay import SlashCommandOverlay
|
|
|
|
overlay = context._tui_app.query_one("#slash-overlay", SlashCommandOverlay)
|
|
assert overlay.visible is False
|
|
assert overlay._text == ""
|
|
|
|
|
|
@then("the reference picker overlay should remain visible")
|
|
def step_reference_picker_visible(context):
|
|
from cleveragents.tui.widgets.reference_picker import ReferencePickerOverlay
|
|
|
|
picker = context._tui_app.query_one("#reference-picker", ReferencePickerOverlay)
|
|
assert picker.visible is True
|
|
assert picker._text
|
|
|
|
|
|
@then("the reference picker overlay should be hidden")
|
|
def step_reference_picker_hidden(context):
|
|
from cleveragents.tui.widgets.reference_picker import ReferencePickerOverlay
|
|
|
|
picker = context._tui_app.query_one("#reference-picker", ReferencePickerOverlay)
|
|
assert picker.visible is False
|
|
assert picker._text == ""
|
|
|
|
|
|
@then('the prompt text should be "{text}"')
|
|
def step_prompt_text_equals(context, text):
|
|
from cleveragents.tui.widgets.prompt import PromptInput
|
|
|
|
prompt = context._tui_app.query_one("#prompt", PromptInput)
|
|
assert prompt.value == text
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CleverAgentsTuiApp alias (line 189)
|
|
# ---------------------------------------------------------------------------
|
|
@then("CleverAgentsTuiApp should be _TextualCleverAgentsTuiApp on the reloaded module")
|
|
def step_alias_check(context):
|
|
assert (
|
|
context._tui_app_mod.CleverAgentsTuiApp is context._tui_app_mod._ResolvedTuiApp
|
|
)
|
|
assert (
|
|
context._tui_app_mod.CleverAgentsTuiApp.__name__ == "_TextualCleverAgentsTuiApp"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# THEME class variable (issue #4742)
|
|
# ---------------------------------------------------------------------------
|
|
@then('the app class should have THEME set to "{theme}"')
|
|
def step_theme_class_var(context, theme: str) -> None:
|
|
assert theme == context._tui_app.THEME, (
|
|
f"Expected THEME='{theme}', got '{context._tui_app.THEME}'"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# on_input_changed: live overlay update steps
|
|
# ---------------------------------------------------------------------------
|
|
def _trigger_input_changed(context, text: str) -> None:
|
|
"""Set prompt text and fire on_input_changed."""
|
|
from cleveragents.tui.widgets.prompt import PromptInput
|
|
|
|
prompt = context._tui_app.query_one("#prompt", PromptInput)
|
|
prompt.text = text
|
|
event = SimpleNamespace()
|
|
context._tui_app.on_input_changed(event)
|
|
|
|
|
|
@when('I trigger on_input_changed with text "{text}"')
|
|
def step_trigger_input_changed(context, text: str) -> None:
|
|
_trigger_input_changed(context, text)
|
|
|
|
|
|
@when("I trigger on_input_changed with empty text")
|
|
def step_trigger_input_changed_empty_text(context) -> None:
|
|
_trigger_input_changed(context, "")
|
|
|
|
|
|
@then('the slash overlay should show only commands matching "{query}"')
|
|
def step_slash_overlay_filtered(context, query: str) -> None:
|
|
from cleveragents.tui.widgets.slash_command_overlay import SlashCommandOverlay
|
|
|
|
overlay = context._tui_app.query_one("#slash-overlay", SlashCommandOverlay)
|
|
assert overlay._text, "Slash overlay text should not be empty"
|
|
# All displayed commands must start with the query
|
|
for cmd in overlay._commands:
|
|
assert cmd.command.startswith(query), (
|
|
f"Command '{cmd.command}' does not start with '{query}'"
|
|
)
|
|
|
|
|
|
@then("the reference picker should be reset to empty")
|
|
def step_ref_picker_reset(context) -> None:
|
|
from cleveragents.tui.widgets.reference_picker import ReferencePickerOverlay
|
|
|
|
picker = context._tui_app.query_one("#reference-picker", ReferencePickerOverlay)
|
|
assert picker._text == "", (
|
|
f"Expected reference picker to be empty after reset, got: '{picker._text}'"
|
|
)
|
|
|
|
|
|
@then("the slash overlay should be reset to all commands")
|
|
def step_slash_overlay_all_commands(context) -> None:
|
|
from cleveragents.tui.widgets.slash_command_overlay import SlashCommandOverlay
|
|
|
|
overlay = context._tui_app.query_one("#slash-overlay", SlashCommandOverlay)
|
|
# "reset to all commands" means the overlay returned to its default hidden state
|
|
# (set_commands with empty query calls hide(), clearing _text and _commands)
|
|
assert not overlay._visible, "Slash overlay should be hidden after reset"
|
|
assert overlay._text == "", "Slash overlay text should be empty after reset"
|