Files
cleveragents-core/features/steps/tui_materializer_steps.py
T
HAL9000 5e71221f61
CI / load-versions (push) Successful in 16s
CI / push-validation (push) Successful in 28s
CI / build (push) Successful in 33s
CI / lint (push) Successful in 50s
CI / typecheck (push) Successful in 1m0s
CI / quality (push) Successful in 1m8s
CI / security (push) Successful in 1m17s
CI / helm (push) Successful in 39s
CI / unit_tests (push) Successful in 6m53s
CI / docker (push) Successful in 1m55s
CI / integration_tests (push) Successful in 10m43s
CI / coverage (push) Successful in 13m43s
CI / status-check (push) Successful in 3s
CI / load-versions (pull_request) Successful in 13s
CI / push-validation (pull_request) Successful in 34s
CI / lint (pull_request) Successful in 48s
CI / quality (pull_request) Successful in 54s
CI / typecheck (pull_request) Successful in 59s
CI / security (pull_request) Successful in 1m12s
CI / build (pull_request) Successful in 51s
CI / helm (pull_request) Successful in 58s
CI / unit_tests (pull_request) Successful in 6m37s
CI / integration_tests (pull_request) Successful in 10m35s
CI / benchmark-publish (push) Has been cancelled
CI / benchmark-regression (push) Has been cancelled
CI / docker (pull_request) Failing after 13m18s
CI / coverage (pull_request) Failing after 28m20s
CI / status-check (pull_request) Has been cancelled
fix(tui): resolve lint and AmbiguousStep errors in TuiMaterializer
- Fix ruff format violations: collapse list comprehension in
  materializer.py, collapse function signature, collapse decorators
  and assertion in tui_materializer_steps.py, remove trailing
  whitespace in vulture_whitelist.py
- Remove duplicate Behave step definitions from tui_materializer_steps.py
  that conflicted with output_rendering_steps.py and
  project_commands_coverage_steps.py: create status handle,
  create code handle, close panel handle, close status handle,
  no error should be raised
- Rename render_element_for_tui step texts to avoid conflict with
  tui_first_run_steps.py: rendered text should contain/be empty ->
  render output should contain/be empty
- Update tui_materializer.feature to use renamed step texts and
  replace no error should be raised with materializer still running

ISSUES CLOSED: #11164
2026-06-15 11:14:31 +00:00

516 lines
20 KiB
Python

"""Step definitions for tui_materializer.feature."""
from __future__ import annotations
import threading
from typing import Any
from behave import given, then, when # type: ignore[import-untyped]
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("the tui materializer module is imported")
def step_import_tui_materializer(context: Any) -> None:
from cleveragents.tui.materializer import (
TuiMaterializer,
TuiWidgetEvent,
TuiWidgetEventType,
render_element_for_tui,
)
context.TuiMaterializer = TuiMaterializer
context.TuiWidgetEvent = TuiWidgetEvent
context.TuiWidgetEventType = TuiWidgetEventType
context.render_element_for_tui = render_element_for_tui
context.materializer = None
context.session = None
context.panel_handle = None
context.status_handle = None
context.rendered_text = ""
context.callback_events: list[TuiWidgetEvent] = []
context.last_event: TuiWidgetEvent | None = None
# ---------------------------------------------------------------------------
# Module exports
# ---------------------------------------------------------------------------
@then("TuiMaterializer should be importable from tui.materializer")
def step_check_tui_materializer_importable(context: Any) -> None:
from cleveragents.tui.materializer import TuiMaterializer
assert TuiMaterializer is not None
@then("TuiWidgetEvent should be importable from tui.materializer")
def step_check_tui_widget_event_importable(context: Any) -> None:
from cleveragents.tui.materializer import TuiWidgetEvent
assert TuiWidgetEvent is not None
@then("TuiWidgetEventType should be importable from tui.materializer")
def step_check_tui_widget_event_type_importable(context: Any) -> None:
from cleveragents.tui.materializer import TuiWidgetEventType
assert TuiWidgetEventType is not None
@then("render_element_for_tui should be importable from tui.materializer")
def step_check_render_element_for_tui_importable(context: Any) -> None:
from cleveragents.tui.materializer import render_element_for_tui
assert render_element_for_tui is not None
# ---------------------------------------------------------------------------
# TuiMaterializer instantiation
# ---------------------------------------------------------------------------
@when("I create a TuiMaterializer without a callback")
def step_create_materializer_no_callback(context: Any) -> None:
from cleveragents.tui.materializer import TuiMaterializer
context.materializer = TuiMaterializer()
@when("I create a TuiMaterializer with a callback")
def step_create_materializer_with_callback(context: Any) -> None:
from cleveragents.tui.materializer import TuiMaterializer, TuiWidgetEvent
context.callback_events = []
def _callback(event: TuiWidgetEvent) -> None:
context.callback_events.append(event)
context.materializer = TuiMaterializer(on_event=_callback)
@then('the materializer strategy_name should be "tui"')
def step_check_strategy_name(context: Any) -> None:
assert context.materializer.strategy_name == "tui"
@then("the materializer supports_incremental_updates should be True")
def step_check_supports_incremental(context: Any) -> None:
assert context.materializer.supports_incremental_updates is True
@then("the materializer events list should be empty")
def step_check_events_empty(context: Any) -> None:
assert context.materializer.events == []
# ---------------------------------------------------------------------------
# the materializer should still be running (alias for strategy checks)
# ---------------------------------------------------------------------------
@then("the materializer should still be running")
def step_materializer_still_running(context: Any) -> None:
"""The materializer is considered 'running' when it reports a valid
strategy name and supports incremental updates."""
assert context.materializer.strategy_name == "tui"
assert context.materializer.supports_incremental_updates is True
# ---------------------------------------------------------------------------
# on_session_begin
# ---------------------------------------------------------------------------
@when("I call on_session_begin with a mock session")
def step_call_on_session_begin(context: Any) -> None:
context.materializer.on_session_begin(None) # type: ignore[arg-type]
# ---------------------------------------------------------------------------
# OutputSession integration
# ---------------------------------------------------------------------------
@when("I use the materializer with an OutputSession")
def step_use_with_output_session(context: Any) -> None:
from cleveragents.cli.output.session import OutputSession
context.session = OutputSession(strategy=context.materializer)
@when('I create a panel handle titled "{title}"')
def step_create_panel_handle(context: Any, title: str) -> None:
context.panel_handle = context.session.panel(title)
@when('I create a table handle titled "{title}"')
def step_create_table_handle(context: Any, title: str) -> None:
context.table_handle = context.session.table(title)
@when('I create a progress handle with label "{label}"')
def step_create_progress_handle(context: Any, label: str) -> None:
context.progress_handle = context.session.progress(label, indeterminate=True)
@when("I create a separator handle")
def step_create_separator_handle(context: Any) -> None:
context.separator_handle = context.session.separator()
@when('I create an action hint handle with command "{command}"')
def step_create_action_hint_handle(context: Any, command: str) -> None:
context.action_hint_handle = context.session.action_hint([command])
@when("I close the session")
def step_close_session(context: Any) -> None:
context.session.close()
@when('I create and close a status handle with message "{message}"')
def step_create_and_close_status_handle(context: Any, message: str) -> None:
handle = context.session.status(message)
handle.close()
# ---------------------------------------------------------------------------
# Event assertions
# ---------------------------------------------------------------------------
@then("an element_created event should be emitted")
def step_check_element_created_event(context: Any) -> None:
events = context.materializer.events
created_events = [e for e in events if e.event_type == "element_created"]
assert len(created_events) > 0, f"No element_created events found. Events: {events}"
context.last_event = created_events[-1]
@then('the event element_kind should be "{kind}"')
def step_check_event_element_kind(context: Any, kind: str) -> None:
assert context.last_event is not None
assert context.last_event.element_kind == kind, (
f"Expected element_kind={kind!r}, got {context.last_event.element_kind!r}"
)
@then('the event rendered_text should contain "{text}"')
def step_check_event_rendered_text_contains(context: Any, text: str) -> None:
assert context.last_event is not None
assert text in context.last_event.rendered_text, (
f"Expected {text!r} in rendered_text={context.last_event.rendered_text!r}"
)
@then("an element_closed event should be emitted")
def step_check_element_closed_event(context: Any) -> None:
events = context.materializer.events
closed_events = [e for e in events if e.event_type == "element_closed"]
assert len(closed_events) > 0, f"No element_closed events found. Events: {events}"
context.last_closed_event = closed_events[-1]
@then('the closed event rendered_text should contain "{text}"')
def step_check_closed_event_rendered_text(context: Any, text: str) -> None:
assert context.last_closed_event is not None
assert text in context.last_closed_event.rendered_text, (
f"Expected {text!r} in rendered_text={context.last_closed_event.rendered_text!r}"
)
@then("a session_end event should be emitted")
def step_check_session_end_event(context: Any) -> None:
events = context.materializer.events
end_events = [e for e in events if e.event_type == "session_end"]
assert len(end_events) > 0, f"No session_end events found. Events: {events}"
# ---------------------------------------------------------------------------
# Callback assertions
# ---------------------------------------------------------------------------
@then("the callback should have been called")
def step_check_callback_called(context: Any) -> None:
assert len(context.callback_events) > 0, "Callback was not called"
@then('the callback event type should be "element_created"')
def step_check_callback_event_type_created(context: Any) -> None:
created = [e for e in context.callback_events if e.event_type == "element_created"]
assert len(created) > 0, (
f"No element_created callback events. Got: {[e.event_type for e in context.callback_events]}"
)
@then('the callback should have been called with event_type "{event_type}"')
def step_check_callback_event_type(context: Any, event_type: str) -> None:
matching = [e for e in context.callback_events if e.event_type == event_type]
assert len(matching) > 0, (
f"No {event_type!r} callback events. Got: {[e.event_type for e in context.callback_events]}"
)
# ---------------------------------------------------------------------------
# rendered_output property
# ---------------------------------------------------------------------------
@then("the materializer rendered_output should be empty")
def step_check_rendered_output_empty(context: Any) -> None:
assert context.materializer.rendered_output == "", (
f"Expected empty rendered_output, got: {context.materializer.rendered_output!r}"
)
@then('the materializer rendered_output should contain "{text}"')
def step_check_rendered_output_contains(context: Any, text: str) -> None:
output = context.materializer.rendered_output
assert text in output, f"Expected {text!r} in rendered_output={output!r}"
# ---------------------------------------------------------------------------
# render_element_for_tui
# ---------------------------------------------------------------------------
@when('I render a Panel element with title "{title}" and entry "{key}" "{value}"')
def step_render_panel_element(context: Any, title: str, key: str, value: str) -> None:
from cleveragents.cli.output.handles._models import Panel, PanelEntry
from cleveragents.tui.materializer import render_element_for_tui
element = Panel(title=title, entries=[PanelEntry(key=key, value=value)])
context.rendered_text = render_element_for_tui(element)
@when('I render a Table element with title "{title}" and column "{column}"')
def step_render_table_element(context: Any, title: str, column: str) -> None:
from cleveragents.cli.output.handles._models import ColumnDef, Table
from cleveragents.tui.materializer import render_element_for_tui
element = Table(title=title, columns=[ColumnDef(name=column)])
context.rendered_text = render_element_for_tui(element)
@when('I render a StatusMessage element with level "{level}" and message "{message}"')
def step_render_status_element(context: Any, level: str, message: str) -> None:
from cleveragents.cli.output.handles._models import StatusMessage
from cleveragents.tui.materializer import render_element_for_tui
element = StatusMessage(message=message, level=level)
context.rendered_text = render_element_for_tui(element)
@when('I render an indeterminate ProgressIndicator with label "{label}"')
def step_render_indeterminate_progress(context: Any, label: str) -> None:
from cleveragents.cli.output.handles._models import ProgressIndicator
from cleveragents.tui.materializer import render_element_for_tui
element = ProgressIndicator(label=label, indeterminate=True)
context.rendered_text = render_element_for_tui(element)
@when(
'I render a determinate ProgressIndicator with label "{label}" current {current:d} total {total:d}'
)
def step_render_determinate_progress(
context: Any, label: str, current: int, total: int
) -> None:
from cleveragents.cli.output.handles._models import ProgressIndicator
from cleveragents.tui.materializer import render_element_for_tui
element = ProgressIndicator(label=label, current=current, total=total)
context.rendered_text = render_element_for_tui(element)
@when('I render a Tree element with root "{root}" and child "{child}"')
def step_render_tree_element(context: Any, root: str, child: str) -> None:
from cleveragents.cli.output.handles._models import Tree, TreeNode
from cleveragents.tui.materializer import render_element_for_tui
root_node = TreeNode(label=root, children=[TreeNode(label=child)])
element = Tree(root=root_node)
context.rendered_text = render_element_for_tui(element)
@when('I render a CodeBlock element with language "{language}" and content "{content}"')
def step_render_code_element(context: Any, language: str, content: str) -> None:
from cleveragents.cli.output.handles._models import CodeBlock
from cleveragents.tui.materializer import render_element_for_tui
element = CodeBlock(content=content, language=language)
context.rendered_text = render_element_for_tui(element)
@when('I render a DiffBlock element with file_a "{file_a}" and file_b "{file_b}"')
def step_render_diff_element(context: Any, file_a: str, file_b: str) -> None:
from cleveragents.cli.output.handles._models import DiffBlock
from cleveragents.tui.materializer import render_element_for_tui
element = DiffBlock(file_a=file_a, file_b=file_b)
context.rendered_text = render_element_for_tui(element)
@when('I render a Separator element with style "{style}"')
def step_render_separator_element(context: Any, style: str) -> None:
from cleveragents.cli.output.handles._models import Separator
from cleveragents.tui.materializer import render_element_for_tui
element = Separator(style=style)
context.rendered_text = render_element_for_tui(element)
@when('I render an ActionHint element with command "{command}"')
def step_render_action_hint_element(context: Any, command: str) -> None:
from cleveragents.cli.output.handles._models import ActionHint
from cleveragents.tui.materializer import render_element_for_tui
element = ActionHint(commands=[command])
context.rendered_text = render_element_for_tui(element)
@when('I render a TextBlock element with content "{content}"')
def step_render_text_element(context: Any, content: str) -> None:
from cleveragents.cli.output.handles._models import TextBlock
from cleveragents.tui.materializer import render_element_for_tui
element = TextBlock(content=content)
context.rendered_text = render_element_for_tui(element)
@when('I render a TextBlock element with content "{content}" and indent {indent:d}')
def step_render_text_element_with_indent(
context: Any, content: str, indent: int
) -> None:
from cleveragents.cli.output.handles._models import TextBlock
from cleveragents.tui.materializer import render_element_for_tui
element = TextBlock(content=content, indent=indent)
context.rendered_text = render_element_for_tui(element)
@then('the render output should contain "{text}"')
def step_check_rendered_text_contains(context: Any, text: str) -> None:
assert text in context.rendered_text, (
f"Expected {text!r} in rendered_text={context.rendered_text!r}"
)
@then("the render output should be empty")
def step_check_rendered_text_empty(context: Any) -> None:
assert context.rendered_text == "", (
f"Expected empty rendered_text, got: {context.rendered_text!r}"
)
# ---------------------------------------------------------------------------
# A2A event routing
# ---------------------------------------------------------------------------
@when('I route a permission request for "{file_path}" with type "{request_type}"')
def step_route_permission_request(
context: Any, file_path: str, request_type: str
) -> None:
from cleveragents.domain.models.core.inline_permission_question import (
InlinePermissionQuestion,
PermissionRequestType,
)
question = InlinePermissionQuestion(
file_path=file_path,
request_type=PermissionRequestType(request_type),
)
context.permission_question = question
context.materializer.route_permission_request(question)
@then("a permission_request event should be emitted")
def step_check_permission_request_event(context: Any) -> None:
events = context.materializer.events
perm_events = [e for e in events if e.event_type == "permission_request"]
assert len(perm_events) > 0, f"No permission_request events found. Events: {events}"
context.last_perm_event = perm_events[-1]
@then("the permission event extra should be the InlinePermissionQuestion")
def step_check_permission_event_extra(context: Any) -> None:
assert context.last_perm_event.extra is context.permission_question
@then('the permission event rendered_text should contain "{text}"')
def step_check_permission_event_rendered_text(context: Any, text: str) -> None:
assert text in context.last_perm_event.rendered_text, (
f"Expected {text!r} in rendered_text={context.last_perm_event.rendered_text!r}"
)
@when('I route a thought block with content "{content}"')
def step_route_thought_block(context: Any, content: str) -> None:
from cleveragents.domain.models.thought.thought_block import ThoughtBlock
thought = ThoughtBlock(content=content)
context.thought_block = thought
context.materializer.route_thought_block(thought)
@then("a thought_block event should be emitted")
def step_check_thought_block_event(context: Any) -> None:
events = context.materializer.events
thought_events = [e for e in events if e.event_type == "thought_block"]
assert len(thought_events) > 0, f"No thought_block events found. Events: {events}"
context.last_thought_event = thought_events[-1]
@then("the thought event extra should be the ThoughtBlock")
def step_check_thought_event_extra(context: Any) -> None:
assert context.last_thought_event.extra is context.thought_block
@then('the thought event rendered_text should contain "{text}"')
def step_check_thought_event_rendered_text(context: Any, text: str) -> None:
assert text in context.last_thought_event.rendered_text, (
f"Expected {text!r} in rendered_text={context.last_thought_event.rendered_text!r}"
)
# ---------------------------------------------------------------------------
# Thread safety
# ---------------------------------------------------------------------------
@when("I create multiple status handles concurrently")
def step_create_multiple_status_handles_concurrently(context: Any) -> None:
errors: list[Exception] = []
def _create_handle(i: int) -> None:
try:
handle = context.session.status(f"Message {i}")
handle.close()
except Exception as exc:
errors.append(exc)
threads = [threading.Thread(target=_create_handle, args=(i,)) for i in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
context.thread_errors = errors
@then("all events should be recorded without data corruption")
def step_check_thread_safety(context: Any) -> None:
assert context.thread_errors == [], f"Thread errors: {context.thread_errors}"
events = context.materializer.events
# We should have at least 10 element_created + 10 element_closed events
created = [e for e in events if e.event_type == "element_created"]
closed = [e for e in events if e.event_type == "element_closed"]
assert len(created) >= 10, f"Expected >= 10 created events, got {len(created)}"
assert len(closed) >= 10, f"Expected >= 10 closed events, got {len(closed)}"