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 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
516 lines
20 KiB
Python
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)}"
|