"""Step definitions for the output rendering framework feature tests.""" from __future__ import annotations import contextlib import json import re import threading import time from typing import Any, cast import yaml from behave import given, then, use_step_matcher, when from behave.runner import Context from cleveragents.cli.formatting import ( format_output, format_output_session, ) from cleveragents.cli.output.handles import ( ActionHint, CodeBlock, ColumnDef, DiffBlock, DiffLine, ElementClosedError, ElementHandle, ElementUpdated, Panel, PanelEntry, ProgressIndicator, StatusMessage, Table, TextBlock, Tree, ) from cleveragents.cli.output.materializers import ( ColorMaterializer, JsonMaterializer, PlainMaterializer, RichMaterializer, TableMaterializer, YamlMaterializer, strip_terminal_escapes, ) from cleveragents.cli.output.selection import ( TerminalCapabilities, detect_terminal_capabilities, select_materializer, ) from cleveragents.cli.output.session import OutputSession # ---- ANSI detection helper ---- ANSI_RE = re.compile(r"\033\[") # =========================================================================== # OutputSession lifecycle # =========================================================================== @given('an OutputSession with format "{fmt}"') def step_given_output_session(context: Context, fmt: str) -> None: strategy = PlainMaterializer() context.session = OutputSession(format=fmt, command="test", strategy=strategy) context.strategy = strategy @when("the session is closed") def step_session_close(context: Context) -> None: context.session_result = context.session.close() @then('the session state is "{state}"') def step_session_state(context: Context, state: str) -> None: assert context.session._state == state @when('I use an OutputSession context manager with format "{fmt}"') def step_session_context_manager(context: Context, fmt: str) -> None: strategy = PlainMaterializer() context.strategy = strategy with OutputSession(format=fmt, command="test-ctx", strategy=strategy) as sess: context.session = sess context.cm_session_open = sess._state == "open" @then("the session was closed automatically") def step_session_auto_closed(context: Context) -> None: assert context.cm_session_open is True assert context.session._state == "closed" @when("I use an OutputSession context manager that raises an exception") def step_session_ctx_exception(context: Context) -> None: strategy = PlainMaterializer() context.strategy = strategy try: with OutputSession( format="plain", command="test-exc", strategy=strategy ) as sess: context.session = sess raise RuntimeError("Intentional test error") except RuntimeError: pass # The session should have been closed with exit_code 1 by __exit__. @then("the session exit code is {code:d}") def step_session_exit_code(context: Context, code: int) -> None: assert context.session._state == "closed" # L8 fix (Luis review): use public snapshot() to read exit_code # instead of accessing private _exit_code attribute. snap = context.session.snapshot() assert snap.exit_code == code, f"Expected exit_code={code}, got {snap.exit_code}" # =========================================================================== # PanelHandle # =========================================================================== @when('I create a panel handle with title "{title}"') def step_create_panel(context: Context, title: str) -> None: context.panel_handle = context.session.panel(title) @when('I set entry "{key}" to "{value}"') def step_set_entry(context: Context, key: str, value: str) -> None: context.panel_handle.set_entry(key, value) @when("I close the panel handle") def step_close_panel(context: Context) -> None: context.panel_handle.close() @then("the panel has {count:d} entries") def step_panel_entry_count(context: Context, count: int) -> None: panel = cast(Panel, context.panel_handle.element) assert len(panel.entries) == count, f"Expected {count}, got {len(panel.entries)}" @then('the panel entry "{key}" has value "{value}"') def step_panel_entry_value(context: Context, key: str, value: str) -> None: panel = cast(Panel, context.panel_handle.element) for entry in panel.entries: if entry.key == key: assert entry.value == value, f"Expected {value!r}, got {entry.value!r}" return raise AssertionError(f"Entry {key!r} not found in panel") @when('I batch set entries with keys "{keys}" and values "{values}"') def step_batch_set_entries(context: Context, keys: str, values: str) -> None: key_list = keys.split(",") val_list = values.split(",") entries = dict(zip(key_list, val_list, strict=True)) context.panel_handle.set_entries(entries) @when('I remove entry "{key}"') def step_remove_entry(context: Context, key: str) -> None: context.panel_handle.remove_entry(key) @when('I add line "{key}" with value "{val}"') def step_add_line(context: Context, key: str, val: str) -> None: context.panel_handle.add_line(key, val) @then('setting entry "{key}" to "{value}" raises ElementClosedError') def step_set_entry_closed_raises(context: Context, key: str, value: str) -> None: try: context.panel_handle.set_entry(key, value) raise AssertionError("Expected ElementClosedError") except ElementClosedError: pass # =========================================================================== # TableHandle # =========================================================================== @when('I create a table handle with columns "{columns}"') def step_create_table(context: Context, columns: str) -> None: col_list = columns.split(",") context.table_handle = context.session.table(columns=col_list) @when('I add a dict row with Name "{name}" and Status "{status}"') def step_add_dict_row(context: Context, name: str, status: str) -> None: tbl = cast(Table, context.table_handle.element) col_names = [c.name for c in tbl.columns] row: dict[str, str] = {} if len(col_names) >= 1: row[col_names[0]] = name if len(col_names) >= 2: row[col_names[1]] = status context.table_handle.add_row(row) @when('I add a list row with values "{values}"') def step_add_list_row(context: Context, values: str) -> None: vals = values.split(",") context.table_handle.add_row(vals) @when("I add {count:d} batch rows") def step_add_batch_rows(context: Context, count: int) -> None: tbl = cast(Table, context.table_handle.element) col_names = [c.name for c in tbl.columns] rows: list[dict[str, str]] = [] for i in range(count): row: dict[str, str] = {} for col in col_names: row[col] = f"val-{i}" rows.append(row) context.table_handle.add_rows(rows) @then("the table has {count:d} rows") def step_table_row_count(context: Context, count: int) -> None: tbl = cast(Table, context.table_handle.element) assert len(tbl.rows) == count, f"Expected {count}, got {len(tbl.rows)}" @when('I set summary with total "{total}"') def step_set_summary(context: Context, total: str) -> None: context.table_handle.set_summary({"total": total}) @then("the table summary is set") def step_table_summary_set(context: Context) -> None: tbl = cast(Table, context.table_handle.element) assert tbl.summary is not None @when('I set sort key to "{column}"') def step_set_sort_key(context: Context, column: str) -> None: context.table_handle.set_sort_key(column) @then('the table sort key is "{column}"') def step_table_sort_key(context: Context, column: str) -> None: tbl = cast(Table, context.table_handle.element) assert tbl.sort_key == column @when("I close the table handle") def step_close_table(context: Context) -> None: context.table_handle.close() @then("adding a row raises ElementClosedError") def step_add_row_closed_raises(context: Context) -> None: try: context.table_handle.add_row({"Name": "x"}) raise AssertionError("Expected ElementClosedError") except ElementClosedError: pass # =========================================================================== # StatusHandle # =========================================================================== @when('I create a status handle with message "{message}"') def step_create_status(context: Context, message: str) -> None: context.status_handle = context.session.status(message) @when('I update the status message to "{message}"') def step_update_status_message(context: Context, message: str) -> None: context.status_handle.set_message(message) @when('I update the status level to "{level}"') def step_update_status_level(context: Context, level: str) -> None: context.status_handle.set_level(level) @when('I set the status detail to "{detail}"') def step_set_status_detail(context: Context, detail: str) -> None: context.status_handle.set_detail(detail) @when("I close the status handle") def step_close_status(context: Context) -> None: context.status_handle.close() @then('the status message is "{message}"') def step_status_message_check(context: Context, message: str) -> None: sm = cast(StatusMessage, context.status_handle.element) assert sm.message == message @then('the status level is "{level}"') def step_status_level_check(context: Context, level: str) -> None: sm = cast(StatusMessage, context.status_handle.element) assert sm.level == level @then('the status detail is "{detail}"') def step_status_detail_check(context: Context, detail: str) -> None: sm = cast(StatusMessage, context.status_handle.element) assert sm.detail == detail @then('setting status level to "{level}" raises ValueError') def step_invalid_status_level(context: Context, level: str) -> None: try: context.status_handle.set_level(level) raise AssertionError("Expected ValueError") except ValueError: pass # =========================================================================== # ProgressHandle # =========================================================================== use_step_matcher("re") @when( r'I create a progress handle labelled "(?P