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
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:
@@ -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 {
|
||||
|
||||
@@ -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}'"
|
||||
)
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user