chore(cli): complete renderer migration for remaining command modules #1059

Merged
brent.edwards merged 1 commits from chore/m6-renderer-migration into master 2026-03-19 23:30:55 +00:00
11 changed files with 453 additions and 17 deletions
+98
View File
@@ -0,0 +1,98 @@
@mock_only
Feature: CLI renderers coverage
Verifies render_error, render_success, render_warning, and render_empty
helper functions across all output formats (rich, json, plain).
# --- render_error ---
Scenario: render_error with rich format shows red label
Given a captured console for renderers
When I call render_error with label "NOT_FOUND" message "Item missing" fmt "rich"
Then the renderer output should contain "NOT_FOUND"
And the renderer output should contain "Item missing"
Scenario: render_error with json format produces JSON envelope
Given a captured console for renderers
When I call render_error with label "NOT_FOUND" message "Item missing" fmt "json"
Then the renderer output should contain "NOT_FOUND"
Scenario: render_error with plain format shows ERROR prefix
Given a captured console for renderers
When I call render_error with label "NOT_FOUND" message "Item missing" fmt "plain"
Then the renderer output should contain "ERROR: NOT_FOUND: Item missing"
Scenario: render_error with recovery hint in rich format
Given a captured console for renderers
And a recovery hint "Try 'list' first"
When I call render_error with label "NOT_FOUND" message "Item missing" fmt "rich"
Then the renderer output should contain "Try 'list' first"
Scenario: render_error with recovery hint in plain format
Given a captured console for renderers
And a recovery hint "Try 'list' first"
When I call render_error with label "NOT_FOUND" message "Item missing" fmt "plain"
Then the renderer output should contain "Try 'list' first"
# --- render_success ---
Scenario: render_success with rich format shows green check
Given a captured console for renderers
When I call render_success with message "Created project" fmt "rich"
Then the renderer output should contain "Created project"
Scenario: render_success with json format produces JSON
Given a captured console for renderers
When I call render_success with message "Created project" fmt "json"
Then the renderer output should contain "ok"
Scenario: render_success with plain format shows OK prefix
Given a captured console for renderers
When I call render_success with message "Created project" fmt "plain"
Then the renderer output should contain "OK: Created project"
# --- render_warning ---
Scenario: render_warning with rich format shows yellow text
Given a captured console for renderers
When I call render_warning with message "Deprecated feature" fmt "rich"
Then the renderer output should contain "Deprecated feature"
Scenario: render_warning with json format produces JSON
Given a captured console for renderers
When I call render_warning with message "Deprecated feature" fmt "json"
Then the renderer output should contain "warning"
Scenario: render_warning with plain format shows WARNING prefix
Given a captured console for renderers
When I call render_warning with message "Deprecated feature" fmt "plain"
Then the renderer output should contain "WARNING: Deprecated feature"
# --- render_empty ---
Scenario: render_empty with rich format shows yellow message
Given a captured console for renderers
When I call render_empty with entity_type "actions" fmt "rich"
Then the renderer output should contain "No actions found"
Scenario: render_empty with json format produces empty list
Given a captured console for renderers
When I call render_empty with entity_type "actions" fmt "json"
Then the renderer output should contain "[]"
Scenario: render_empty with plain format shows text
Given a captured console for renderers
When I call render_empty with entity_type "actions" fmt "plain"
Then the renderer output should contain "No actions found."
Scenario: render_empty with recovery hint in rich format
Given a captured console for renderers
And a recovery hint "Run 'action create' first"
When I call render_empty with entity_type "actions" fmt "rich"
Then the renderer output should contain "Run 'action create' first"
Scenario: render_empty with table format and recovery
Given a captured console for renderers
And a recovery hint "Run 'project create' first"
When I call render_empty with entity_type "projects" fmt "table"
Then the renderer output should contain "No projects found."
And the renderer output should contain "Run 'project create' first"
@@ -0,0 +1,123 @@
"""Step definitions for cli_renderers_coverage.feature.
Tests the four shared renderer helpers (render_error, render_success,
render_warning, render_empty) across all output formats to ensure full
branch coverage of ``cleveragents.cli.renderers``.
"""
from __future__ import annotations
import contextlib
from io import StringIO
from behave import given, then, when
from behave.runner import Context
from rich.console import Console
from cleveragents.cli.renderers import (
render_empty,
render_error,
render_success,
render_warning,
)
@given("a captured console for renderers")
def step_captured_console(context: Context) -> None:
"""Create a Rich Console that writes to a StringIO buffer."""
context.renderer_buf = StringIO()
context.renderer_stdout = StringIO()
context.renderer_console = Console(
file=context.renderer_buf,
width=200,
no_color=True,
highlight=False,
force_terminal=False,
)
context.renderer_recovery = None
def _get_output(context: Context) -> str:
"""Combine Rich console buffer and any direct stdout writes."""
return context.renderer_buf.getvalue() + context.renderer_stdout.getvalue()
@given('a recovery hint "{recovery}"')
def step_set_recovery(context: Context, recovery: str) -> None:
"""Store a recovery hint for the next renderer call."""
context.renderer_recovery = recovery
# ---------------------------------------------------------------------------
# render_error
# ---------------------------------------------------------------------------
@when('I call render_error with label "{label}" message "{msg}" fmt "{fmt}"')
def step_call_render_error(context: Context, label: str, msg: str, fmt: str) -> None:
"""Invoke render_error with the given parameters."""
recovery = getattr(context, "renderer_recovery", None)
with contextlib.redirect_stdout(context.renderer_stdout):
render_error(
label,
msg,
fmt=fmt,
recovery=recovery,
console=context.renderer_console,
)
# ---------------------------------------------------------------------------
# render_success
# ---------------------------------------------------------------------------
@when('I call render_success with message "{msg}" fmt "{fmt}"')
def step_call_render_success(context: Context, msg: str, fmt: str) -> None:
"""Invoke render_success with the given parameters."""
with contextlib.redirect_stdout(context.renderer_stdout):
render_success(msg, fmt=fmt, console=context.renderer_console)
# ---------------------------------------------------------------------------
# render_warning
# ---------------------------------------------------------------------------
@when('I call render_warning with message "{msg}" fmt "{fmt}"')
def step_call_render_warning(context: Context, msg: str, fmt: str) -> None:
"""Invoke render_warning with the given parameters."""
with contextlib.redirect_stdout(context.renderer_stdout):
render_warning(msg, fmt=fmt, console=context.renderer_console)
# ---------------------------------------------------------------------------
# render_empty
# ---------------------------------------------------------------------------
@when('I call render_empty with entity_type "{etype}" fmt "{fmt}"')
def step_call_render_empty(context: Context, etype: str, fmt: str) -> None:
"""Invoke render_empty with the given parameters."""
recovery = getattr(context, "renderer_recovery", None)
with contextlib.redirect_stdout(context.renderer_stdout):
render_empty(
etype,
fmt=fmt,
recovery=recovery,
console=context.renderer_console,
)
# ---------------------------------------------------------------------------
# THEN assertions
# ---------------------------------------------------------------------------
@then('the renderer output should contain "{expected}"')
def step_renderer_output_contains(context: Context, expected: str) -> None:
"""Assert the captured output contains the expected text."""
output = _get_output(context)
assert expected in output, (
f"Expected {expected!r} in renderer output, got:\n{output}"
)
+2 -2
View File
@@ -8,17 +8,17 @@ from contextlib import suppress
from typing import Annotated
import typer
from rich.console import Console
from rich.live import Live
from rich.panel import Panel
from rich.text import Text
from cleveragents.cli.renderers import _get_console
from cleveragents.core.exceptions import CleverAgentsError, PlanError
from cleveragents.domain.models.core import Project
# Create sub-app for auto-debug commands
app = typer.Typer(help="Auto-debug commands")
console = Console()
console = _get_console()
def auto_debug_command(max_attempts: int = 3) -> tuple[bool, int]:
@@ -17,7 +17,6 @@ from typing import Annotated, Any
import typer
import yaml
from pydantic import ValidationError as PydanticValidationError
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
@@ -25,6 +24,7 @@ from cleveragents.application.services.automation_profile_service import (
AutomationProfileService,
)
from cleveragents.cli.formatting import OutputFormat, format_output
from cleveragents.cli.renderers import _get_console
from cleveragents.core.exceptions import (
CleverAgentsError,
NotFoundError,
@@ -40,7 +40,7 @@ from cleveragents.domain.models.core.automation_profile import (
app = typer.Typer(
help="Manage automation profiles that control plan execution autonomy."
)
console = Console()
console = _get_console()
# Reusable --format option description
_FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)"
+2 -2
View File
@@ -29,7 +29,6 @@ from pathlib import Path
from typing import Annotated, Any
import typer
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
@@ -39,9 +38,10 @@ from cleveragents.application.services.config_service import (
ConfigService,
)
from cleveragents.cli.formatting import OutputFormat, format_output
from cleveragents.cli.renderers import _get_console
app = typer.Typer(help="Manage configuration settings for CleverAgents.")
console = Console()
console = _get_console()
# ---------------------------------------------------------------------------
# Constants
+2 -2
View File
@@ -10,10 +10,10 @@ from pathlib import Path
from typing import TYPE_CHECKING, Annotated, Any
import typer
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from cleveragents.cli.renderers import _get_console
from cleveragents.core.exceptions import (
CleverAgentsError,
FileSystemError,
@@ -24,7 +24,7 @@ if TYPE_CHECKING:
# Create sub-app for context commands
app = typer.Typer(help="Context management commands")
console = Console()
console = _get_console()
def _normalize_context_entry(
+3 -3
View File
@@ -23,13 +23,13 @@ from pathlib import Path
from typing import Annotated, Any
import typer
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from cleveragents.application.services.context_service import DEFAULT_IGNORE_PATTERNS
from cleveragents.cli.commands.project_context import app as context_app
from cleveragents.cli.formatting import OutputFormat, format_output
from cleveragents.cli.renderers import _get_console, _get_err_console
from cleveragents.core.exceptions import (
CleverAgentsError,
ConfigurationError,
@@ -43,8 +43,8 @@ app = typer.Typer(help="Project management commands")
file_filter_app = typer.Typer(
help="Manage project include/exclude filters", name="file-filter"
)
console = Console()
err_console = Console(stderr=True)
console = _get_console()
err_console = _get_err_console()
# Reusable --format option description
_FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)"
+2 -2
View File
@@ -58,7 +58,6 @@ from pathlib import Path
from typing import Annotated, Any
import typer
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
@@ -67,6 +66,7 @@ from cleveragents.application.services.resource_registry_service import (
ResourceRegistryService,
)
from cleveragents.cli.formatting import OutputFormat, format_output
from cleveragents.cli.renderers import _get_console
from cleveragents.core.exceptions import (
CleverAgentsError,
NotFoundError,
@@ -95,7 +95,7 @@ type_app = typer.Typer(
)
app.add_typer(type_app, name="type")
console = Console()
console = _get_console()
_FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)"
+2 -2
View File
@@ -43,13 +43,13 @@ from typing import Annotated, Any
import typer
from pydantic import ValidationError as PydanticValidationError
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from cleveragents.application.container import get_container
from cleveragents.application.services.skill_service import SkillService
from cleveragents.cli.formatting import OutputFormat, format_output
from cleveragents.cli.renderers import _get_console
from cleveragents.domain.models.core.skill import (
ResolvedToolEntry,
Skill,
@@ -60,7 +60,7 @@ logger = logging.getLogger(__name__)
# Create sub-app for skill commands
app = typer.Typer(help="Manage skills (reusable, namespaced tool collections).")
console = Console()
console = _get_console()
# Reusable --format option description
_FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)"
+2 -2
View File
@@ -52,11 +52,11 @@ from typing import Annotated, Any
import typer
import yaml
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from cleveragents.cli.formatting import OutputFormat, format_output
from cleveragents.cli.renderers import _get_console
from cleveragents.core.exceptions import (
CleverAgentsError,
NotFoundError,
@@ -66,7 +66,7 @@ from cleveragents.domain.models.core.tool import Tool, ToolType
# Create sub-app for tool commands
app = typer.Typer(help="Manage tools (callable operations) in the tool registry.")
console = Console()
console = _get_console()
# Reusable --format option description
_FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)"
+215
View File
@@ -0,0 +1,215 @@
"""Unified CLI output renderers.
Shared output functions for every CLI command: detail views, list views,
errors, successes, warnings, and empty-result placeholders. All respect
the ``--format`` flag (rich/color/table/plain/json/yaml).
Provides ``_get_console()`` and ``_get_err_console()`` for lazy-initialised
shared Console instances, eliminating redundant module-level Console objects
across command modules.
"""
from __future__ import annotations
from typing import Any
from rich.console import Console
from rich.markup import escape as _esc
from cleveragents.cli.formatting import (
OutputFormat,
format_output,
)
__all__ = [
"_get_console",
"_get_err_console",
"render_empty",
"render_error",
"render_success",
"render_warning",
]
# Console helpers
_console: Console | None = None
_err_console: Console | None = None
def _get_console() -> Console:
"""Return the shared stdout Rich console (created on first call)."""
global _console
if _console is None:
_console = Console()
return _console
def _get_err_console() -> Console:
"""Return the shared stderr Rich console (created on first call)."""
global _err_console
if _err_console is None:
_err_console = Console(stderr=True)
return _err_console
# render_error
def render_error(
label: str,
message: str,
*,
recovery: str | None = None,
details: dict[str, Any] | None = None,
fmt: str = OutputFormat.RICH.value,
console: Console | None = None,
) -> None:
"""Render a uniform error message.
Parameters
----------
label:
Short error category (e.g. ``"Validation Error"``).
message:
Human-readable description of what went wrong.
recovery:
Optional recovery hint.
details:
Optional structured details dict for JSON/YAML.
fmt:
Output format string.
"""
console = console or _get_err_console()
if fmt in (OutputFormat.JSON.value, OutputFormat.YAML.value):
envelope: dict[str, Any] = {
"error": {
"code": label,
"message": message,
"details": details or {},
}
}
if recovery:
envelope["error"]["recovery"] = recovery
console.print(format_output(envelope, fmt))
return
if fmt in (OutputFormat.PLAIN.value, OutputFormat.TABLE.value):
console.print(f"ERROR: {label}: {message}")
if recovery:
console.print(recovery)
return
# Rich / Color
console.print(f"[red]{_esc(label)}:[/red] {_esc(message)}")
if recovery:
console.print(f"[dim]{_esc(recovery)}[/dim]")
# render_success
def render_success(
message: str,
*,
fmt: str = OutputFormat.RICH.value,
data: dict[str, Any] | None = None,
console: Console | None = None,
) -> None:
"""Render a green success confirmation.
Parameters
----------
message:
Confirmation text.
fmt:
Output format string.
data:
Optional dict to render as JSON/YAML instead of the plain message.
"""
console = console or _get_console()
if fmt in (OutputFormat.JSON.value, OutputFormat.YAML.value):
payload = data if data is not None else {"status": "ok", "message": message}
console.print(format_output(payload, fmt))
return
if fmt == OutputFormat.PLAIN.value:
console.print(f"OK: {message}")
return
console.print(f"[green]\u2713[/green] {_esc(message)}")
# render_warning
def render_warning(
message: str,
*,
fmt: str = OutputFormat.RICH.value,
console: Console | None = None,
) -> None:
"""Render a yellow advisory message.
Parameters
----------
message:
Warning text.
fmt:
Output format string.
"""
console = console or _get_console()
if fmt in (OutputFormat.JSON.value, OutputFormat.YAML.value):
payload = {"status": "warning", "message": message}
console.print(format_output(payload, fmt))
return
if fmt == OutputFormat.PLAIN.value:
console.print(f"WARNING: {message}")
return
console.print(f"[yellow]{_esc(message)}[/yellow]")
# render_empty
def render_empty(
entity_type: str,
*,
message: str | None = None,
recovery: str | None = None,
fmt: str = OutputFormat.RICH.value,
console: Console | None = None,
) -> None:
"""Render a "no items found" message with optional recovery hint.
Parameters
----------
entity_type:
Plural noun (e.g. ``"actions"``, ``"projects"``).
message:
Override the default ``"No {entity_type} found."`` text.
recovery:
Suggestion for what to do next.
fmt:
Output format string.
"""
console = console or _get_console()
text = message or f"No {entity_type} found."
if fmt in (OutputFormat.JSON.value, OutputFormat.YAML.value):
console.print(format_output([], fmt))
return
if fmt in (OutputFormat.PLAIN.value, OutputFormat.TABLE.value):
console.print(text)
if recovery:
console.print(recovery)
return
console.print(f"[yellow]{_esc(text)}[/yellow]")
if recovery:
console.print(_esc(recovery))