chore(cli): complete renderer migration for remaining command modules #1059
@@ -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}"
|
||||
)
|
||||
@@ -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)"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user