diff --git a/features/steps/validation_add_output_format_steps.py b/features/steps/validation_add_output_format_steps.py new file mode 100644 index 000000000..7f4c13109 --- /dev/null +++ b/features/steps/validation_add_output_format_steps.py @@ -0,0 +1,134 @@ +"""Step definitions for validation add output format feature (issue #1422). + +Tests that ``agents validation add`` rich output includes: +- Config: field (path to config file) +- Created: field (timestamp) +- Capability panel (Read-Only, Checkpointable, Timeout) +- OK Validation registered footer message +""" + +from __future__ import annotations + +import os +import tempfile +from typing import Any +from unittest.mock import MagicMock, patch + +from behave import given, then, when +from behave.runner import Context +from typer.testing import CliRunner + +from cleveragents.cli.commands.validation import app as validation_app + +_VALIDATION_YAML = """\ +name: local/run-tests +description: Run unit tests with coverage +source: custom +mode: required +code: | + def run(inputs): + return {"passed": True} +""" + +_runner = CliRunner() + + +def _make_mock_validation( + name: str = "local/run-tests", +) -> dict[str, Any]: + """Create a mock validation dict as returned by the service layer.""" + return { + "name": name, + "description": "Run unit tests with coverage", + "source": "custom", + "tool_type": "validation", + "mode": "required", + "timeout": 300, + } + + +def _patch_val_svc(context: Context) -> Any: + return patch( + "cleveragents.cli.commands.validation._get_tool_registry_service", + return_value=context.val_output_mock_service, + ) + + +# --------------------------------------------------------------------------- +# Background +# --------------------------------------------------------------------------- + + +@given("a validation add output format test runner with mocks") +def step_val_output_format_runner(context: Context) -> None: + context.val_output_runner = _runner + context.val_output_result = None + context.val_output_yaml_path = None + context.val_output_mock_service = MagicMock() + context.val_output_mock_service.register_tool.return_value = _make_mock_validation() + context.val_output_mock_service.update_tool.return_value = _make_mock_validation() + context.val_output_mock_service.get_tool.return_value = None + + +# --------------------------------------------------------------------------- +# Given steps +# --------------------------------------------------------------------------- + + +@given("a validation add output format YAML config at a known path") +def step_val_output_yaml_config(context: Context) -> None: + fd, path = tempfile.mkstemp(suffix=".yaml") + with os.fdopen(fd, "w") as fh: + fh.write(_VALIDATION_YAML) + context.val_output_yaml_path = path + + +@given("the validation add output format validation already exists") +def step_val_output_already_exists(context: Context) -> None: + context.val_output_mock_service.get_tool.return_value = _make_mock_validation() + context.val_output_mock_service.update_tool.return_value = _make_mock_validation() + + +# --------------------------------------------------------------------------- +# When steps +# --------------------------------------------------------------------------- + + +@when("I invoke validation add output format with rich output") +def step_val_output_invoke_rich(context: Context) -> None: + with _patch_val_svc(context): + context.val_output_result = _runner.invoke( + validation_app, + ["add", "--config", context.val_output_yaml_path], + ) + + +@when("I invoke validation add output format with --update flag") +def step_val_output_invoke_update(context: Context) -> None: + with _patch_val_svc(context): + context.val_output_result = _runner.invoke( + validation_app, + ["add", "--config", context.val_output_yaml_path, "--update"], + ) + + +# --------------------------------------------------------------------------- +# Then assertions +# --------------------------------------------------------------------------- + + +@then("the validation add output format result should succeed") +def step_val_output_result_success(context: Context) -> None: + assert context.val_output_result is not None + assert context.val_output_result.exit_code == 0, ( + f"Expected exit 0, got {context.val_output_result.exit_code}. " + f"Output: {context.val_output_result.output}" + ) + + +@then('the validation add output format output should contain "{text}"') +def step_val_output_contains(context: Context, text: str) -> None: + assert context.val_output_result is not None + assert text in context.val_output_result.output, ( + f"Expected '{text}' in output. Got: {context.val_output_result.output}" + ) diff --git a/features/validation_add_output_format.feature b/features/validation_add_output_format.feature new file mode 100644 index 000000000..def3d5c05 --- /dev/null +++ b/features/validation_add_output_format.feature @@ -0,0 +1,66 @@ +@tdd_issue @tdd_issue_1422 +Feature: Bug #1422 — validation add output missing Capability panel, Config path, and Created timestamp + As a user of the CleverAgents CLI + I want the ``agents validation add`` command to display the Config path, + Created timestamp, Capability panel, and footer message in its rich output + So that the output matches the specification at docs/specification.md lines 9319-9360 + + Background: + Given a validation add output format test runner with mocks + + Scenario: validation add rich output includes Config path + Given a validation add output format YAML config at a known path + When I invoke validation add output format with rich output + Then the validation add output format result should succeed + And the validation add output format output should contain "Config:" + + Scenario: validation add rich output includes Created timestamp + Given a validation add output format YAML config at a known path + When I invoke validation add output format with rich output + Then the validation add output format result should succeed + And the validation add output format output should contain "Created:" + + Scenario: validation add rich output includes Capability panel + Given a validation add output format YAML config at a known path + When I invoke validation add output format with rich output + Then the validation add output format result should succeed + And the validation add output format output should contain "Capability" + + Scenario: validation add rich output includes Read-Only enforced field + Given a validation add output format YAML config at a known path + When I invoke validation add output format with rich output + Then the validation add output format result should succeed + And the validation add output format output should contain "Read-Only:" + + Scenario: validation add rich output includes Checkpointable enforced field + Given a validation add output format YAML config at a known path + When I invoke validation add output format with rich output + Then the validation add output format result should succeed + And the validation add output format output should contain "Checkpointable:" + + Scenario: validation add rich output includes Timeout field + Given a validation add output format YAML config at a known path + When I invoke validation add output format with rich output + Then the validation add output format result should succeed + And the validation add output format output should contain "Timeout:" + + Scenario: validation add rich output includes OK footer message + Given a validation add output format YAML config at a known path + When I invoke validation add output format with rich output + Then the validation add output format result should succeed + And the validation add output format output should contain "OK Validation registered" + + Scenario: validation add rich output includes Validation Registered panel title + Given a validation add output format YAML config at a known path + When I invoke validation add output format with rich output + Then the validation add output format result should succeed + And the validation add output format output should contain "Validation Registered" + + Scenario: validation add with --update flag also shows Config and Capability panel + Given a validation add output format YAML config at a known path + And the validation add output format validation already exists + When I invoke validation add output format with --update flag + Then the validation add output format result should succeed + And the validation add output format output should contain "Config:" + And the validation add output format output should contain "Capability" + And the validation add output format output should contain "OK Validation registered" diff --git a/src/cleveragents/cli/commands/validation.py b/src/cleveragents/cli/commands/validation.py index bc65ad080..51e8d39a7 100644 --- a/src/cleveragents/cli/commands/validation.py +++ b/src/cleveragents/cli/commands/validation.py @@ -48,6 +48,8 @@ Based on implementation_plan.md -- Task C1.tool.cli. from __future__ import annotations +import sys +from datetime import datetime from pathlib import Path from typing import Annotated, Any @@ -68,7 +70,9 @@ from cleveragents.domain.models.core.tool import Validation, ValidationMode # Create sub-app for validation commands app = typer.Typer(help="Manage validations (pass/fail tools) and resource attachments.") -console = Console() +# Use stdout so Typer/CliRunner captures output in result.output (BDD tests +# assert on result.output; Rich defaults to stderr which would break those). +console = Console(file=sys.stdout) # Reusable --format option description _FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)" @@ -136,8 +140,18 @@ def _print_validation( tool: Any, title: str = "Validation", fmt: str = OutputFormat.RICH.value, + config_path: str | None = None, + created_at: datetime | None = None, ) -> None: - """Print validation details in the requested format.""" + """Print validation details in the requested format. + + Args: + tool: The validation tool object to print + title: Panel title (e.g., "Validation Registered") + fmt: Output format (rich, json, yaml, etc.) + config_path: Path to the config file (optional) + created_at: Timestamp of creation (optional) + """ data = _validation_spec_dict(tool) if fmt != OutputFormat.RICH.value: console.print(format_output(data, fmt)) @@ -155,6 +169,15 @@ def _print_validation( f"[bold]Mode:[/bold] {mode}" ) + # Add Config field if provided + if config_path: + details += f"\n[bold]Config:[/bold] {config_path}" + + # Add Created field if provided + if created_at: + created_str = created_at.strftime("%Y-%m-%d %H:%M") + details += f"\n[bold]Created:[/bold] {created_str}" + wraps = data.get("wraps") if wraps: details += f"\n[bold]Wraps:[/bold] {wraps}" @@ -162,8 +185,21 @@ def _print_validation( if transform: details += f"\n[bold]Transform:[/bold] {transform}" + # Print main panel console.print(Panel(details, title=title, expand=False)) + # Add Capability panel + timeout = getattr(tool, "timeout", 300) + capability_details = ( + f"[bold]Read-Only:[/bold] true (enforced)\n" + f"[bold]Checkpointable:[/bold] false (enforced)\n" + f"[bold]Timeout:[/bold] {timeout}s" + ) + console.print(Panel(capability_details, title="Capability", expand=False)) + + # Add footer message + console.print("[green]✓ OK Validation registered[/green]") + @app.command("add") def add( @@ -239,11 +275,23 @@ def add( existing = service.get_tool(validation.name) if existing is not None: registered = service.update_tool(validation) - _print_validation(registered, title="Validation Updated", fmt=fmt) + _print_validation( + registered, + title="Validation Updated", + fmt=fmt, + config_path=str(config), + created_at=datetime.now(), + ) return registered = service.register_tool(validation) - _print_validation(registered, title="Validation Registered", fmt=fmt) + _print_validation( + registered, + title="Validation Registered", + fmt=fmt, + config_path=str(config), + created_at=datetime.now(), + ) except FileNotFoundError as exc: console.print(f"[red]Config file error:[/red] {exc}")