From 63cf1a7872b337534c44e96fb24f5437c3ced2d8 Mon Sep 17 00:00:00 2001 From: CleverThis Date: Fri, 29 May 2026 23:52:28 -0400 Subject: [PATCH] test(cli): boost coverage for registry.py render dispatch and edge cases Add 16 BDD scenarios covering: - PlainElementRenderer.render_element dispatch for all 9 element types - _BaseElementRenderer.serialize() via PlainElementRenderer - ColorElementRenderer.render_element dispatch for all element types - TableElementRenderer can_render (True/False) and render_element dispatch - RichElementRenderer.render_element dispatch + can_render True path - JsonElementRenderer.render_element dispatch (all render_* return "") - YamlElementRenderer.render_element dispatch (all render_* return "") - RendererRegistry.resolve() unknown format fallback to plain - RendererRegistry.resolve() ValueError when no plain fallback registered Raises cli/output/registry.py coverage from 60.9% to pass the 96.5% overall threshold. ISSUES CLOSED: #917 --- features/output_rendering_registry.feature | 66 +++++++ .../steps/output_rendering_registry_steps.py | 177 ++++++++++++++++++ 2 files changed, 243 insertions(+) diff --git a/features/output_rendering_registry.feature b/features/output_rendering_registry.feature index 684b06e1b..81fe5e872 100644 --- a/features/output_rendering_registry.feature +++ b/features/output_rendering_registry.feature @@ -154,3 +154,69 @@ Feature: Output Rendering Framework — Registry, Renderer, and Capabilities Given a JsonMaterializer instance When I call bind on the json materializer Then no error is raised + + # ================================================================ + # Render dispatch — concrete renderer methods coverage + # ================================================================ + + Scenario: PlainElementRenderer dispatches all element types via render_element + Given a PlainElementRenderer + When I call render_element with each element type + Then no render dispatch error occurred + + Scenario: PlainElementRenderer serialize assembles rendered text + Given a PlainElementRenderer + When I call base serialize with a text element + Then the base serialized output is a string + + Scenario: ColorElementRenderer dispatches all element types via render_element + Given a ColorElementRenderer + When I call render_element with each element type + Then no render dispatch error occurred + + Scenario: ColorElementRenderer can_render returns True when ANSI is supported + Given a ColorElementRenderer + Then color renderer can_render is True with ANSI support + + Scenario: TableElementRenderer can_render checks is_tty flag + Given a TableElementRenderer + Then table renderer can_render is True on a TTY terminal + And table renderer can_render is False on a non-TTY terminal + + Scenario: TableElementRenderer dispatches all element types via render_element + Given a TableElementRenderer + When I call render_element with each element type + Then no render dispatch error occurred + + Scenario: RichElementRenderer dispatches all element types via render_element + Given a RichElementRenderer + When I call render_element with each element type + Then no render dispatch error occurred + + Scenario: RichElementRenderer can_render returns True with cursor movement + Given a RichElementRenderer + Then rich renderer can_render is True with cursor movement + + Scenario: JsonElementRenderer dispatches all element types returning empty strings + Given a JsonElementRenderer + When I call render_element with each element type + Then no render dispatch error occurred + + Scenario: YamlElementRenderer dispatches all element types returning empty strings + Given a YamlElementRenderer + When I call render_element with each element type + Then no render dispatch error occurred + + # ================================================================ + # RendererRegistry edge-case paths + # ================================================================ + + Scenario: RendererRegistry resolve unknown format falls back to plain + Given the default renderer registry + When I resolve format "unregistered_format" from the registry + Then the resolved renderer format name is "plain" + + Scenario: RendererRegistry resolve raises ValueError when no plain fallback exists + Given a registry with no plain and a non-renderable format + When I try to resolve the non-renderable format + Then a ValueError is raised from resolve diff --git a/features/steps/output_rendering_registry_steps.py b/features/steps/output_rendering_registry_steps.py index 3ebd9254e..a5f7359b4 100644 --- a/features/steps/output_rendering_registry_steps.py +++ b/features/steps/output_rendering_registry_steps.py @@ -414,3 +414,180 @@ def step_json_coldef_type_key(context: Context) -> None: def step_yaml_not_contains(context: Context, text: str) -> None: output = context.strategy.get_output() assert text not in output, f"Found {text!r} in YAML output: {output[:200]}" + + +# =========================================================================== +# TableElementRenderer given +# =========================================================================== + + +@given("a TableElementRenderer") +def step_table_renderer(context: Context) -> None: + from cleveragents.cli.output.registry import TableElementRenderer + + context.element_renderer = TableElementRenderer() + + +# =========================================================================== +# Generic render_element dispatch — covers all render_* methods +# =========================================================================== + + +@when("I call render_element with each element type") +def step_render_all_element_types(context: Context) -> None: + from cleveragents.cli.output.handles import ( + ActionHint, + CodeBlock, + DiffBlock, + ProgressIndicator, + Separator, + StatusMessage, + Table, + TextBlock, + Tree, + TreeNode, + ) + + elements = [ + TextBlock(content="hello world"), + StatusMessage(message="all ok"), + CodeBlock(content="x = 1"), + DiffBlock(), + Separator(), + ActionHint(commands=["ls", "pwd"]), + Table(title="data"), + Tree(root=TreeNode(label="root")), + ProgressIndicator(label="loading"), + ] + context.render_dispatch_error = None + try: + for elem in elements: + context.element_renderer.render_element(elem) + except Exception as exc: + context.render_dispatch_error = exc + + +@then("no render dispatch error occurred") +def step_no_dispatch_error(context: Context) -> None: + err = getattr(context, "render_dispatch_error", None) + assert err is None, f"Unexpected render error: {err}" + + +# =========================================================================== +# _BaseElementRenderer.serialize() via PlainElementRenderer +# =========================================================================== + + +@when("I call base serialize with a text element") +def step_base_serialize_text(context: Context) -> None: + from cleveragents.cli.output.handles import TextBlock + from cleveragents.cli.output.session import StructuredOutput + + output = StructuredOutput( + command="test-base", + session_id="ses-base-test", + elements=[TextBlock(content="hello from base serialize")], + exit_code=0, + ) + context.base_serialized = context.element_renderer.serialize(output) + + +@then("the base serialized output is a string") +def step_base_serialized_is_string(context: Context) -> None: + assert isinstance(context.base_serialized, str) + + +# =========================================================================== +# can_render True paths +# =========================================================================== + + +@then("color renderer can_render is True with ANSI support") +def step_color_can_render_true(context: Context) -> None: + caps = TerminalCapabilities( + is_tty=True, + supports_ansi=True, + supports_cursor_movement=True, + term_program="xterm-256color", + ) + assert context.element_renderer.can_render(caps) is True + + +@then("table renderer can_render is True on a TTY terminal") +def step_table_can_render_true(context: Context) -> None: + caps = TerminalCapabilities( + is_tty=True, + supports_ansi=True, + supports_cursor_movement=True, + term_program="xterm", + ) + assert context.element_renderer.can_render(caps) is True + + +@then("table renderer can_render is False on a non-TTY terminal") +def step_table_can_render_false(context: Context) -> None: + caps = TerminalCapabilities( + is_tty=False, + supports_ansi=False, + supports_cursor_movement=False, + term_program="", + ) + assert context.element_renderer.can_render(caps) is False + + +@then("rich renderer can_render is True with cursor movement") +def step_rich_can_render_true(context: Context) -> None: + caps = TerminalCapabilities( + is_tty=True, + supports_ansi=True, + supports_cursor_movement=True, + term_program="xterm", + ) + assert context.element_renderer.can_render(caps) is True + + +# =========================================================================== +# RendererRegistry error paths +# =========================================================================== + + +@given("a registry with no plain and a non-renderable format") +def step_registry_no_plain_non_renderable(context: Context) -> None: + from cleveragents.cli.output.materializers import PlainMaterializer + from cleveragents.cli.output.registry import RendererRegistry + + class _NeverRenders: + format_name = "norender" + + def can_render(self, terminal_caps: TerminalCapabilities) -> bool: + return False + + context.registry = RendererRegistry() + context.registry.register( + "norender", + strategy_factory=lambda caps: PlainMaterializer(), + renderer_factory=lambda caps: _NeverRenders(), + fallback=None, + ) + context.no_plain_format = "norender" + + +@when("I try to resolve the non-renderable format") +def step_resolve_non_renderable(context: Context) -> None: + caps = TerminalCapabilities( + is_tty=False, + supports_ansi=False, + supports_cursor_movement=False, + term_program="", + ) + context.resolve_error = None + try: + context.registry.resolve(context.no_plain_format, caps) + except ValueError as exc: + context.resolve_error = exc + + +@then("a ValueError is raised from resolve") +def step_valueerror_raised(context: Context) -> None: + assert context.resolve_error is not None, "Expected ValueError but none was raised" + assert isinstance(context.resolve_error, ValueError)