"""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)}"