fix(tui): align sidebar implementation with master TextArea refactor
CI / lint (pull_request) Successful in 1m12s
CI / quality (pull_request) Successful in 58s
CI / typecheck (pull_request) Successful in 1m14s
CI / security (pull_request) Successful in 1m14s
CI / push-validation (pull_request) Successful in 34s
CI / helm (pull_request) Successful in 38s
CI / build (pull_request) Successful in 47s
CI / integration_tests (pull_request) Successful in 3m47s
CI / e2e_tests (pull_request) Successful in 3m36s
CI / unit_tests (pull_request) Failing after 4m13s
CI / docker (pull_request) Has been skipped
CI / coverage (pull_request) Successful in 10m39s
CI / status-check (pull_request) Failing after 3s

Update mock infrastructure and prompt widget to use TextArea (not Input)
following master commit 7523a50d. Add tui_prompt_textarea feature tests
from master. Restore pyproject.toml a2a-sdk upper bound pin.
This commit is contained in:
2026-04-23 16:44:47 +00:00
parent ec378a90c6
commit 330a24a228
7 changed files with 261 additions and 20 deletions
+7 -7
View File
@@ -108,13 +108,13 @@ def _build_mock_textual():
def update(self, text):
self._text = text
class MockInput:
"""Minimal Input stand-in for the Textual base class."""
class MockTextArea:
"""Minimal TextArea stand-in for the Textual base class."""
value = ""
text = ""
def __init__(self, *args, **kwargs):
self.value = ""
self.text = ""
class MockCollapsible:
def __init__(self, *args, **kwargs):
@@ -133,7 +133,7 @@ def _build_mock_textual():
mock_textual_widgets.Header = MockHeader
mock_textual_widgets.Footer = MockFooter
mock_textual_widgets.Static = MockStatic
mock_textual_widgets.Input = MockInput
mock_textual_widgets.TextArea = MockTextArea
mock_textual_widgets.Collapsible = MockCollapsible
return {
@@ -409,7 +409,7 @@ 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
prompt.text = text
@then('the conversation widget should contain "{text}"')
@@ -468,7 +468,7 @@ def _submit_text(context, text):
from cleveragents.tui.widgets.prompt import PromptInput
prompt = context._tui_app.query_one("#prompt", PromptInput)
prompt.value = text
prompt.text = text
event = SimpleNamespace()
context._tui_app.on_input_submitted(event)
@@ -105,11 +105,11 @@ def _build_mock_textual_for_sidebar() -> dict[str, ModuleType]:
def update(self, text: str) -> None:
self._text = text
class MockInput:
value = ""
class MockTextArea:
text = ""
def __init__(self, *args: object, **kwargs: object) -> None:
self.value = ""
self.text = ""
class MockCollapsible:
def __init__(self, *args: object, **kwargs: object) -> None:
@@ -128,7 +128,7 @@ def _build_mock_textual_for_sidebar() -> dict[str, ModuleType]:
mock_textual_widgets.Header = MockHeader
mock_textual_widgets.Footer = MockFooter
mock_textual_widgets.Static = MockStatic
mock_textual_widgets.Input = MockInput
mock_textual_widgets.TextArea = MockTextArea
mock_textual_widgets.Collapsible = MockCollapsible
return {
+202
View File
@@ -0,0 +1,202 @@
"""Step definitions for tui_prompt_textarea.feature.
Tests that PromptInput uses TextArea (multi-line) instead of Input (single-line).
"""
from __future__ import annotations
import importlib
import sys
from types import ModuleType
from behave import given, then, when
_MOCK_TEXTUAL_KEYS = [
"textual",
"textual.app",
"textual.containers",
"textual.widgets",
]
def _build_mock_textual_with_textarea():
"""Build mock textual modules that expose TextArea."""
mock_textual = ModuleType("textual")
mock_textual_app = ModuleType("textual.app")
mock_textual_containers = ModuleType("textual.containers")
mock_textual_widgets = ModuleType("textual.widgets")
class MockTextArea:
"""Minimal TextArea stand-in for the Textual base class."""
text: str = ""
def __init__(self, *args: object, **kwargs: object) -> None:
self.text = ""
mock_textual_app.App = object
mock_textual_containers.Vertical = object
mock_textual_widgets.Header = object
mock_textual_widgets.Footer = object
mock_textual_widgets.Static = object
mock_textual_widgets.TextArea = MockTextArea
return {
"textual": mock_textual,
"textual.app": mock_textual_app,
"textual.containers": mock_textual_containers,
"textual.widgets": mock_textual_widgets,
}, MockTextArea
def _install_mock_textual(context):
"""Inject mock textual into sys.modules and reload the prompt module."""
mocks, mock_textarea_cls = _build_mock_textual_with_textarea()
context._prompt_saved_modules = {}
for key in _MOCK_TEXTUAL_KEYS:
context._prompt_saved_modules[key] = sys.modules.pop(key, None)
for key, mod in mocks.items():
sys.modules[key] = mod
import cleveragents.tui.widgets.prompt as prompt_mod
importlib.reload(prompt_mod)
context._prompt_mod = prompt_mod
context._mock_textarea_cls = mock_textarea_cls
def _restore_modules(context):
"""Restore original sys.modules and reload the prompt module."""
for key, val in getattr(context, "_prompt_saved_modules", {}).items():
if val is None:
sys.modules.pop(key, None)
else:
sys.modules[key] = val
import cleveragents.tui.widgets.prompt as prompt_mod
importlib.reload(prompt_mod)
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("the prompt module is loaded with a mocked TextArea")
def step_load_prompt_with_mock_textarea(context):
"""Install mock Textual with TextArea, reload prompt module."""
_install_mock_textual(context)
context.add_cleanup(lambda: _restore_modules(context))
@given("the prompt module is loaded without textual")
def step_load_prompt_without_textual(context):
"""Remove textual from sys.modules so the fallback path is used."""
context._prompt_saved_modules_fallback = {}
for key in _MOCK_TEXTUAL_KEYS:
context._prompt_saved_modules_fallback[key] = sys.modules.pop(key, None)
import cleveragents.tui.widgets.prompt as prompt_mod
importlib.reload(prompt_mod)
context._prompt_mod_fallback = prompt_mod
def restore():
for key, val in context._prompt_saved_modules_fallback.items():
if val is None:
sys.modules.pop(key, None)
else:
sys.modules[key] = val
importlib.reload(prompt_mod)
context.add_cleanup(restore)
# ---------------------------------------------------------------------------
# Scenario: PromptInput base class is TextArea not Input
# ---------------------------------------------------------------------------
@then("the PromptInput base class should be the mocked TextArea")
def step_base_class_is_textarea(context):
PromptInput = context._prompt_mod.PromptInput
assert issubclass(PromptInput, context._mock_textarea_cls), (
f"Expected PromptInput to subclass MockTextArea, "
f"but got bases: {PromptInput.__bases__}"
)
# ---------------------------------------------------------------------------
# Scenario: PromptInput exposes a text property not value
# ---------------------------------------------------------------------------
@when("I create a PromptInput instance")
def step_create_prompt_input(context):
context._prompt_instance = context._prompt_mod.PromptInput()
@then("the PromptInput instance should have a text attribute")
def step_has_text_attribute(context):
assert hasattr(context._prompt_instance, "text"), (
"PromptInput instance should have a 'text' attribute"
)
# ---------------------------------------------------------------------------
# Scenario: consume_text returns the current text content
# ---------------------------------------------------------------------------
@when('I set the PromptInput text to "{text}"')
def step_set_prompt_input_text(context, text):
context._prompt_instance.text = text
@when("I call consume_text on the PromptInput")
def step_call_consume_text(context):
context._prompt_submitted = context._prompt_instance.consume_text()
@then('the PromptSubmitted text should be "{expected}"')
def step_prompt_submitted_text(context, expected):
assert context._prompt_submitted.text == expected, (
f"Expected '{expected}', got '{context._prompt_submitted.text}'"
)
# ---------------------------------------------------------------------------
# Scenario: consume_text clears the text after consuming
# ---------------------------------------------------------------------------
@then("the PromptInput text should be empty")
def step_prompt_input_text_empty(context):
assert context._prompt_instance.text == "", (
f"Expected empty text, got '{context._prompt_instance.text}'"
)
# ---------------------------------------------------------------------------
# Scenario: PromptInput fallback uses text attribute when TextArea unavailable
# ---------------------------------------------------------------------------
@when("I create a PromptInput instance from the fallback")
def step_create_fallback_prompt_input(context):
context._fallback_prompt_instance = context._prompt_mod_fallback.PromptInput()
@then("the fallback PromptInput instance should have a text attribute")
def step_fallback_has_text_attribute(context):
assert hasattr(context._fallback_prompt_instance, "text"), (
"Fallback PromptInput instance should have a 'text' attribute"
)
@then("the fallback PromptInput text should be empty string")
def step_fallback_text_empty(context):
assert context._fallback_prompt_instance.text == "", (
f"Expected empty string, got '{context._fallback_prompt_instance.text}'"
)
+37
View File
@@ -0,0 +1,37 @@
Feature: PromptInput uses multi-line TextArea widget
The PromptInput widget must use a multi-line TextArea widget (not a
single-line Input widget) to enable multi-line prompt composition.
Background:
Given the prompt module is loaded with a mocked TextArea
Scenario: PromptInput base class is TextArea not Input
Then the PromptInput base class should be the mocked TextArea
Scenario: PromptInput exposes a text property not value
When I create a PromptInput instance
Then the PromptInput instance should have a text attribute
Scenario: consume_text returns the current text content
When I create a PromptInput instance
And I set the PromptInput text to "hello world"
And I call consume_text on the PromptInput
Then the PromptSubmitted text should be "hello world"
Scenario: consume_text clears the text after consuming
When I create a PromptInput instance
And I set the PromptInput text to "some prompt"
And I call consume_text on the PromptInput
Then the PromptInput text should be empty
Scenario: consume_text supports multi-line text
When I create a PromptInput instance
And I set the PromptInput text to "line one\nline two\nline three"
And I call consume_text on the PromptInput
Then the PromptSubmitted text should be "line one\nline two\nline three"
Scenario: PromptInput fallback uses text attribute when TextArea unavailable
Given the prompt module is loaded without textual
When I create a PromptInput instance from the fallback
Then the fallback PromptInput instance should have a text attribute
And the fallback PromptInput text should be empty string
+4 -2
View File
@@ -48,7 +48,7 @@ dependencies = [
"tomlkit>=0.13.0", # TOML writing with comment preservation for config CLI
"tenacity>=8.2.0", # Retry framework for service layer resilience
"aiohttp>=3.13.4", # CVE-2026-34515 mitigation: open redirect vulnerability
"a2a-sdk>=0.3.0", # A2A Python SDK — required transport for local (stdio) and server (HTTP) modes (ADR-047)
"a2a-sdk>=0.3.0,<1.0.0", # A2A Python SDK — required transport for local (stdio) and server (HTTP) modes (ADR-047); pinned <1.0.0 (removed legacy A2AClient)
]
[project.optional-dependencies]
@@ -128,7 +128,9 @@ ignore = []
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
# Behave step files: F811 = redefined step_impl (Behave pattern), E501 = long step decorator strings
"features/steps/*.py" = ["F811", "E501"]
# B010 = setattr with constant attribute name is intentional in immutability tests (exercises frozen model enforcement)
# I001 = import sorting (Behave step files have specific import patterns)
"features/steps/*.py" = ["F811", "E501", "B010", "I001"]
"features/mocks/*.py" = ["E501"]
"features/environment.py" = ["E501"]
# retry_patterns.py re-exports symbols from retry_service_patterns at module bottom
+1 -1
View File
@@ -274,7 +274,7 @@ if _TEXTUAL_AVAILABLE:
def action_help(self) -> None:
prompt = self.query_one("#prompt", PromptInput)
help_panel = self.query_one("#help-panel", HelpPanelOverlay)
context_name = resolve_help_context(prompt.value)
context_name = resolve_help_context(prompt.text)
help_panel.toggle(context_name)
def action_cycle_preset(self) -> None:
+6 -6
View File
@@ -9,14 +9,14 @@ from typing import Any
def _load_input_base() -> type[Any]:
try:
return importlib.import_module("textual.widgets").Input
return importlib.import_module("textual.widgets").TextArea
except Exception: # pragma: no cover
class _FallbackInput:
value = ""
text = ""
def __init__(self, *args: object, **kwargs: object) -> None:
self.value = ""
self.text = ""
return _FallbackInput
@@ -32,9 +32,9 @@ class PromptSubmitted:
class PromptInput(_InputBase):
"""Input widget wrapper with helper methods."""
"""TextArea widget wrapper with helper methods."""
def consume_text(self) -> PromptSubmitted:
text = self.value
self.value = ""
text = self.text
self.text = ""
return PromptSubmitted(text=text)