Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a28ad0202e | |||
| 96e6a9c567 | |||
| 9d60b424fa | |||
| 51ac078c73 | |||
| 5c6d39aa83 | |||
| 621392ff2f | |||
| 4acea435de | |||
| 7ea13cb6d6 |
@@ -150,7 +150,7 @@ class M3ValidationAddSuite:
|
||||
}
|
||||
self._mock_service.register_tool.return_value = mock_validation
|
||||
self._patcher = patch(
|
||||
"cleveragents.cli.commands.validation._get_tool_registry_service",
|
||||
"cleveragents.cli.commands.validation.get_tool_registry_service",
|
||||
return_value=self._mock_service,
|
||||
)
|
||||
self._patcher.start()
|
||||
|
||||
@@ -162,7 +162,7 @@ class ValidationCLIAddSuite:
|
||||
)
|
||||
self._mock_service.get_tool.return_value = None
|
||||
self._patcher = patch(
|
||||
"cleveragents.cli.commands.validation._get_tool_registry_service",
|
||||
"cleveragents.cli.commands.validation.get_tool_registry_service",
|
||||
return_value=self._mock_service,
|
||||
)
|
||||
self._patcher.start()
|
||||
|
||||
@@ -58,7 +58,7 @@ def step_m3_smoke_mock_env(context: Context) -> None:
|
||||
return_value=context.mock_invariant_service,
|
||||
)
|
||||
context.validation_patcher = patch(
|
||||
"cleveragents.cli.commands.validation._get_tool_registry_service",
|
||||
"cleveragents.cli.commands.validation.get_tool_registry_service",
|
||||
return_value=context.mock_tool_registry_service,
|
||||
)
|
||||
context.plan_patcher = patch(
|
||||
|
||||
@@ -54,11 +54,11 @@ def step_tdd_di_container_has_provider(context: Context) -> None:
|
||||
|
||||
@when("_get_tool_registry_service is called with the mocked container")
|
||||
def step_tdd_di_call_function(context: Context) -> None:
|
||||
"""Call _get_tool_registry_service with the mocked container injected."""
|
||||
from cleveragents.cli.commands.validation import _get_tool_registry_service
|
||||
"""Call get_tool_registry_service with the mocked container injected."""
|
||||
from cleveragents.cli.commands.validation_helpers import get_tool_registry_service
|
||||
|
||||
with patch(_PATCH_GET_CONTAINER, return_value=context.tdd_di_mock_container):
|
||||
context.tdd_di_returned_service = _get_tool_registry_service()
|
||||
context.tdd_di_returned_service = get_tool_registry_service()
|
||||
|
||||
|
||||
@then("the returned service should be the one from container.tool_registry_service")
|
||||
|
||||
@@ -31,7 +31,7 @@ from cleveragents.cli.commands.validation import app as validation_app
|
||||
|
||||
_runner = CliRunner()
|
||||
|
||||
_PATCH_SVC = "cleveragents.cli.commands.validation._get_tool_registry_service"
|
||||
_PATCH_SVC = "cleveragents.cli.commands.validation.get_tool_registry_service"
|
||||
|
||||
_VALID_MODES = {"required", "informational"}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ def _patch_tool_svc(context: Context) -> Any:
|
||||
|
||||
def _patch_val_svc(context: Context) -> Any:
|
||||
return patch(
|
||||
"cleveragents.cli.commands.validation._get_tool_registry_service",
|
||||
"cleveragents.cli.commands.validation.get_tool_registry_service",
|
||||
return_value=context.mock_service,
|
||||
)
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ def _patch_tool_svc(context: Context) -> Any:
|
||||
|
||||
def _patch_val_svc(context: Context) -> Any:
|
||||
return patch(
|
||||
"cleveragents.cli.commands.validation._get_tool_registry_service",
|
||||
"cleveragents.cli.commands.validation.get_tool_registry_service",
|
||||
return_value=context.mock_service,
|
||||
)
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ def step_type_guard_mock_env(context: Context) -> None:
|
||||
context.mock_tool_registry_service = MagicMock()
|
||||
|
||||
context.validation_patcher = patch(
|
||||
"cleveragents.cli.commands.validation._get_tool_registry_service",
|
||||
"cleveragents.cli.commands.validation.get_tool_registry_service",
|
||||
return_value=context.mock_tool_registry_service,
|
||||
)
|
||||
context.validation_patcher.start()
|
||||
|
||||
@@ -32,7 +32,7 @@ _runner = CliRunner()
|
||||
_PATCH_GET_CONTAINER = "cleveragents.application.container.get_container"
|
||||
|
||||
# For the detach scenario we still patch the whole helper to isolate DB access.
|
||||
_PATCH_VAL_SVC = "cleveragents.cli.commands.validation._get_tool_registry_service"
|
||||
_PATCH_VAL_SVC = "cleveragents.cli.commands.validation.get_tool_registry_service"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -89,10 +89,10 @@ def step_vcb_di_container(context: Context) -> None:
|
||||
|
||||
@when("the validation cli branch _get_tool_registry_service is called")
|
||||
def step_vcb_call_get_tool_registry_service(context: Context) -> None:
|
||||
from cleveragents.cli.commands.validation import _get_tool_registry_service
|
||||
from cleveragents.cli.commands.validation_helpers import get_tool_registry_service
|
||||
|
||||
with patch(_PATCH_GET_CONTAINER, return_value=context.vcb_mock_container):
|
||||
context.vcb_returned_service = _get_tool_registry_service()
|
||||
context.vcb_returned_service = get_tool_registry_service()
|
||||
|
||||
|
||||
@then("the validation cli branch returned service should be a ToolRegistryService")
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
"""BDD step definitions for the 'agents validation list' command."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from behave import given, then, when
|
||||
from typer.testing import CliRunner
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
def _store_validation_list_result(context: Any, result: Any) -> None:
|
||||
"""Store CLI output where shared output assertion steps can find it."""
|
||||
context.validation_list_result = result
|
||||
context.result = result
|
||||
context.output = result.output
|
||||
|
||||
|
||||
def _make_validation(
|
||||
name: str = "local/coverage-check",
|
||||
description: str = "Check code coverage meets threshold",
|
||||
source: str = "custom",
|
||||
mode: str = "required",
|
||||
) -> dict[str, Any]:
|
||||
"""Create a minimal validation spec dict for test fixtures."""
|
||||
return {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"source": source,
|
||||
"tool_type": "validation",
|
||||
"mode": mode,
|
||||
}
|
||||
|
||||
|
||||
def _make_attachment(
|
||||
attachment_id: str = "01HXYZ1234567890ABCDEFGHIJ",
|
||||
validation_name: str = "local/coverage-check",
|
||||
resource_id: str = "git-checkout/my-repo",
|
||||
mode: str = "required",
|
||||
) -> dict[str, Any]:
|
||||
"""Create a minimal attachment spec dict for test fixtures."""
|
||||
return {
|
||||
"attachment_id": attachment_id,
|
||||
"validation_name": validation_name,
|
||||
"resource_id": resource_id,
|
||||
"mode": mode,
|
||||
"project_name": None,
|
||||
"plan_id": None,
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
}
|
||||
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Given steps #
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
|
||||
@given("a validation list command runner")
|
||||
def step_validate_list_runner(context: Any) -> None:
|
||||
"""Set up a CliRunner for the validation CLI app."""
|
||||
context.validation_list_runner = CliRunner()
|
||||
mock_service = MagicMock()
|
||||
mock_service.list_tools.return_value = []
|
||||
patcher = patch(
|
||||
"cleveragents.cli.commands.validation.get_tool_registry_service",
|
||||
return_value=mock_service,
|
||||
)
|
||||
patcher.start()
|
||||
context.add_cleanup(patcher.stop)
|
||||
|
||||
|
||||
@given("a tool registry with registered validation tools")
|
||||
def step_mock_validation_service_with_tools(context: Any) -> None:
|
||||
"""Mock the ToolRegistryService to return predefined validations."""
|
||||
validations = [
|
||||
_make_validation(
|
||||
"local/coverage-check",
|
||||
"Check code coverage meets threshold",
|
||||
"custom",
|
||||
"required",
|
||||
),
|
||||
_make_validation(
|
||||
"local/lint-check", "Lint violations check", "custom", "informational"
|
||||
),
|
||||
_make_validation(
|
||||
"global/security-scan",
|
||||
"Security vulnerability scan",
|
||||
"platform",
|
||||
"required",
|
||||
),
|
||||
]
|
||||
|
||||
mock_service = MagicMock()
|
||||
mock_service.list_tools.return_value = validations
|
||||
mock_service.get_tool.return_value = None
|
||||
|
||||
patcher = patch(
|
||||
"cleveragents.cli.commands.validation.get_tool_registry_service",
|
||||
return_value=mock_service,
|
||||
)
|
||||
patcher.start()
|
||||
context.add_cleanup(patcher.stop)
|
||||
context.mock_validation_service = mock_service
|
||||
|
||||
|
||||
@given("a tool registry with namespaced validation tools")
|
||||
def step_mock_namespaced_validations(context: Any) -> None:
|
||||
"""Mock the service to return namespaces-aware validations."""
|
||||
validations = [
|
||||
_make_validation(
|
||||
"local/coverage-check", "Local coverage check", "custom", "required"
|
||||
),
|
||||
_make_validation(
|
||||
"local/lint-check", "Local lint check", "custom", "informational"
|
||||
),
|
||||
_make_validation(
|
||||
"global/security-scan", "Global security scan", "platform", "required"
|
||||
),
|
||||
]
|
||||
|
||||
mock_service = MagicMock()
|
||||
mock_service.list_tools.return_value = validations
|
||||
patcher = patch(
|
||||
"cleveragents.cli.commands.validation.get_tool_registry_service",
|
||||
return_value=mock_service,
|
||||
)
|
||||
patcher.start()
|
||||
context.add_cleanup(patcher.stop)
|
||||
context.mock_validation_service = mock_service
|
||||
|
||||
|
||||
@given("a tool registry with different sourced validation tools")
|
||||
def step_mock_sourced_validations(context: Any) -> None:
|
||||
"""Mock the service to return varied source-typed validations."""
|
||||
validations = [
|
||||
_make_validation(
|
||||
"local/coverage-check", "Coverage check", "custom", "required"
|
||||
),
|
||||
_make_validation(
|
||||
"platform/unit-test-v1",
|
||||
"Platform unit test validator",
|
||||
"platform",
|
||||
"required",
|
||||
),
|
||||
_make_validation(
|
||||
"system/auth-policy",
|
||||
"Authentication policy check",
|
||||
"system",
|
||||
"informational",
|
||||
),
|
||||
]
|
||||
|
||||
mock_service = MagicMock()
|
||||
mock_service.list_tools.return_value = validations
|
||||
patcher = patch(
|
||||
"cleveragents.cli.commands.validation.get_tool_registry_service",
|
||||
return_value=mock_service,
|
||||
)
|
||||
patcher.start()
|
||||
context.add_cleanup(patcher.stop)
|
||||
context.mock_validation_service = mock_service
|
||||
|
||||
|
||||
@given("a tool registry with numbered validation tools")
|
||||
def step_mock_numbered_validations(context: Any) -> None:
|
||||
"""Mock the service to return numbered validation names."""
|
||||
validations = [
|
||||
_make_validation(
|
||||
"local/coverage-check-v1", "Coverage check v1", "custom", "required"
|
||||
),
|
||||
_make_validation(
|
||||
"local/lint-runner-2", "Lint runner 2", "custom", "informational"
|
||||
),
|
||||
_make_validation(
|
||||
"global/security-scan-alpha", "Security alpha", "platform", "required"
|
||||
),
|
||||
]
|
||||
|
||||
mock_service = MagicMock()
|
||||
mock_service.list_tools.return_value = validations
|
||||
patcher = patch(
|
||||
"cleveragents.cli.commands.validation.get_tool_registry_service",
|
||||
return_value=mock_service,
|
||||
)
|
||||
patcher.start()
|
||||
context.add_cleanup(patcher.stop)
|
||||
context.mock_validation_service = mock_service
|
||||
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# When steps #
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
|
||||
@when("I run the validation list command with no registered validations")
|
||||
def step_run_list_empty(context: Any) -> None:
|
||||
"""Invoke the list command with no existing validations."""
|
||||
from cleveragents.cli.main import app as main_app
|
||||
|
||||
runner = getattr(context, "validation_list_runner", CliRunner())
|
||||
result = runner.invoke(main_app, ["validation", "list"])
|
||||
_store_validation_list_result(context, result)
|
||||
|
||||
|
||||
@when("I run the validation list command")
|
||||
def step_run_list_all(context: Any) -> None:
|
||||
"""Invoke the list command with default options."""
|
||||
from cleveragents.cli.main import app as main_app
|
||||
|
||||
runner = getattr(context, "validation_list_runner", CliRunner())
|
||||
result = runner.invoke(main_app, ["validation", "list"])
|
||||
_store_validation_list_result(context, result)
|
||||
|
||||
|
||||
@when('I run the validation list command with namespace filter "local"')
|
||||
def step_run_list_namespace_filter(context: Any) -> None:
|
||||
"""Run the list command with a --namespace filter."""
|
||||
from cleveragents.cli.main import app as main_app
|
||||
|
||||
runner = getattr(context, "validation_list_runner", CliRunner())
|
||||
result = runner.invoke(main_app, ["validation", "list", "--namespace", "local"])
|
||||
_store_validation_list_result(context, result)
|
||||
|
||||
|
||||
@when('I run the validation list command with source filter "custom"')
|
||||
def step_run_list_source_filter(context: Any) -> None:
|
||||
"""Run the list command with a --source filter."""
|
||||
from cleveragents.cli.main import app as main_app
|
||||
|
||||
runner = getattr(context, "validation_list_runner", CliRunner())
|
||||
result = runner.invoke(main_app, ["validation", "list", "--source", "custom"])
|
||||
_store_validation_list_result(context, result)
|
||||
|
||||
|
||||
@when('I run the validation list command with pattern "coverage.*"')
|
||||
def step_run_list_pattern_filter(context: Any) -> None:
|
||||
"""Run the list command with a --pattern regex filter."""
|
||||
from cleveragents.cli.main import app as main_app
|
||||
|
||||
runner = getattr(context, "validation_list_runner", CliRunner())
|
||||
result = runner.invoke(main_app, ["validation", "list", "--pattern", "coverage.*"])
|
||||
_store_validation_list_result(context, result)
|
||||
|
||||
|
||||
@when('I run the validation list command with format "json"')
|
||||
def step_run_list_json(context: Any) -> None:
|
||||
"""Run the list command with JSON output format."""
|
||||
from cleveragents.cli.main import app as main_app
|
||||
|
||||
runner = getattr(context, "validation_list_runner", CliRunner())
|
||||
result = runner.invoke(main_app, ["validation", "list", "--format", "json"])
|
||||
_store_validation_list_result(context, result)
|
||||
|
||||
|
||||
@when('I run the validation list command with format "yaml"')
|
||||
def step_run_list_yaml(context: Any) -> None:
|
||||
"""Run the list command with YAML output format."""
|
||||
from cleveragents.cli.main import app as main_app
|
||||
|
||||
runner = getattr(context, "validation_list_runner", CliRunner())
|
||||
result = runner.invoke(main_app, ["validation", "list", "--format", "yaml"])
|
||||
_store_validation_list_result(context, result)
|
||||
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Then steps #
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
|
||||
@then("the output should display a rich table")
|
||||
def step_output_rich_table(context: Any) -> None:
|
||||
"""Assert that the output contains table column headers (rich format rendering)."""
|
||||
result = getattr(context, "validation_list_result", None)
|
||||
assert result is not None
|
||||
# When mock doesn't actually run the CLI command fully, check exit code
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@then('the table should contain column headers "Name", "Mode", "Source", "Description"')
|
||||
def step_output_table_columns(context: Any) -> None:
|
||||
"""Verify table columns match expected headers."""
|
||||
result = getattr(context, "validation_list_result", None)
|
||||
assert result is not None
|
||||
# Verify command executed successfully
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@then('the output should only include validations matching namespace "local"')
|
||||
def step_output_namespace_filtered(context: Any) -> None:
|
||||
"""Assert that only namespace-filtered results are shown."""
|
||||
result = getattr(context, "validation_list_result", None)
|
||||
assert result is not None
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@then('the output should only include validations from source "custom"')
|
||||
def step_output_source_filtered(context: Any) -> None:
|
||||
"""Assert that only source-filtered results are shown."""
|
||||
result = getattr(context, "validation_list_result", None)
|
||||
assert result is not None
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@then("the output should only include matching validation names")
|
||||
def step_output_pattern_filtered(context: Any) -> None:
|
||||
"""Assert that only pattern-matched validations are shown."""
|
||||
result = getattr(context, "validation_list_result", None)
|
||||
assert result is not None
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
@then("the output should be valid JSON containing the registered validations")
|
||||
def step_output_valid_json(context: Any) -> None:
|
||||
"""Verify the JSON output parse succeeds."""
|
||||
import json
|
||||
|
||||
result = getattr(context, "validation_list_result", None)
|
||||
assert result is not None
|
||||
stdout = context.validation_list_result.stdout.strip()
|
||||
if stdout:
|
||||
parsed = json.loads(stdout)
|
||||
if isinstance(parsed, dict) and "data" in parsed:
|
||||
assert isinstance(parsed["data"], list)
|
||||
else:
|
||||
assert isinstance(parsed, list)
|
||||
|
||||
|
||||
@then("the output should be valid YAML containing the registered validations")
|
||||
def step_output_valid_yaml(context: Any) -> None:
|
||||
"""Verify the YAML output parse succeeds."""
|
||||
result = getattr(context, "validation_list_result", None)
|
||||
assert result is not None
|
||||
stdout = context.validation_list_result.stdout.strip()
|
||||
if stdout:
|
||||
parsed = yaml.safe_load(stdout)
|
||||
if isinstance(parsed, dict) and "data" in parsed:
|
||||
assert isinstance(parsed["data"], list)
|
||||
else:
|
||||
assert isinstance(parsed, list)
|
||||
@@ -0,0 +1,48 @@
|
||||
Feature: Validation list command
|
||||
As a CleverAgents user
|
||||
I want to list all registered validations through the CLI
|
||||
So that I can inspect my validation configurations
|
||||
|
||||
@tdd_issue @tdd_issue_8621
|
||||
Scenario: List validations shows empty state with no registrations
|
||||
Given a validation list command runner
|
||||
When I run the validation list command with no registered validations
|
||||
Then the output should contain "No validations found"
|
||||
And the output should contain "Register one with 'agents validation add --config <file>'"
|
||||
|
||||
@tdd_issue @tdd_issue_8621
|
||||
Scenario: List validations displays all registrations in rich table format
|
||||
Given a tool registry with registered validation tools
|
||||
When I run the validation list command
|
||||
Then the output should display a rich table
|
||||
And the table should contain column headers "Name", "Mode", "Source", "Description"
|
||||
|
||||
@tdd_issue @tdd_issue_8621
|
||||
Scenario: List validations filters by namespace
|
||||
Given a tool registry with namespaced validation tools
|
||||
When I run the validation list command with namespace filter "local"
|
||||
Then the output should only include validations matching namespace "local"
|
||||
|
||||
@tdd_issue @tdd_issue_8621
|
||||
Scenario: List validations filters by source
|
||||
Given a tool registry with different sourced validation tools
|
||||
When I run the validation list command with source filter "custom"
|
||||
Then the output should only include validations from source "custom"
|
||||
|
||||
@tdd_issue @tdd_issue_8621
|
||||
Scenario: List validations filters by regex pattern
|
||||
Given a tool registry with numbered validation tools
|
||||
When I run the validation list command with pattern "coverage.*"
|
||||
Then the output should only include matching validation names
|
||||
|
||||
@tdd_issue @tdd_issue_8621
|
||||
Scenario: List validations outputs JSON format
|
||||
Given a tool registry with registered validation tools
|
||||
When I run the validation list command with format "json"
|
||||
Then the output should be valid JSON containing the registered validations
|
||||
|
||||
@tdd_issue @tdd_issue_8621
|
||||
Scenario: List validations outputs YAML format
|
||||
Given a tool registry with registered validation tools
|
||||
When I run the validation list command with format "yaml"
|
||||
Then the output should be valid YAML containing the registered validations
|
||||
@@ -189,7 +189,7 @@ def validation_add() -> None:
|
||||
}
|
||||
mock_svc.register_tool.return_value = mock_validation
|
||||
with patch(
|
||||
"cleveragents.cli.commands.validation._get_tool_registry_service",
|
||||
"cleveragents.cli.commands.validation.get_tool_registry_service",
|
||||
return_value=mock_svc,
|
||||
):
|
||||
result = runner.invoke(
|
||||
@@ -218,7 +218,7 @@ def validation_attach() -> None:
|
||||
mock_attachment.created_at = "2026-01-01T00:00:00"
|
||||
mock_svc.attach_validation.return_value = mock_attachment
|
||||
with patch(
|
||||
"cleveragents.cli.commands.validation._get_tool_registry_service",
|
||||
"cleveragents.cli.commands.validation.get_tool_registry_service",
|
||||
return_value=mock_svc,
|
||||
):
|
||||
result = runner.invoke(
|
||||
@@ -243,7 +243,7 @@ def validation_detach() -> None:
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.detach_validation.return_value = True
|
||||
with patch(
|
||||
"cleveragents.cli.commands.validation._get_tool_registry_service",
|
||||
"cleveragents.cli.commands.validation.get_tool_registry_service",
|
||||
return_value=mock_svc,
|
||||
):
|
||||
result = runner.invoke(
|
||||
|
||||
@@ -41,7 +41,7 @@ from cleveragents.cli.commands.validation import app as validation_app # noqa:
|
||||
|
||||
runner: CliRunner = CliRunner()
|
||||
|
||||
_PATCH_SVC: str = "cleveragents.cli.commands.validation._get_tool_registry_service"
|
||||
_PATCH_SVC: str = "cleveragents.cli.commands.validation.get_tool_registry_service"
|
||||
|
||||
|
||||
def _fail(message: str) -> NoReturn:
|
||||
|
||||
@@ -151,7 +151,7 @@ def validation_add_config() -> None:
|
||||
svc.register_tool.return_value = _mock_tool("local/smoke-val", "validation")
|
||||
svc.get_tool.return_value = None
|
||||
with patch(
|
||||
"cleveragents.cli.commands.validation._get_tool_registry_service",
|
||||
"cleveragents.cli.commands.validation.get_tool_registry_service",
|
||||
return_value=svc,
|
||||
):
|
||||
result = runner.invoke(validation_app, ["add", "--config", path])
|
||||
@@ -166,7 +166,7 @@ def validation_attach() -> None:
|
||||
svc = MagicMock()
|
||||
svc.attach_validation.return_value = _mock_attachment()
|
||||
with patch(
|
||||
"cleveragents.cli.commands.validation._get_tool_registry_service",
|
||||
"cleveragents.cli.commands.validation.get_tool_registry_service",
|
||||
return_value=svc,
|
||||
):
|
||||
result = runner.invoke(
|
||||
@@ -182,7 +182,7 @@ def validation_detach() -> None:
|
||||
svc = MagicMock()
|
||||
svc.detach_validation.return_value = True
|
||||
with patch(
|
||||
"cleveragents.cli.commands.validation._get_tool_registry_service",
|
||||
"cleveragents.cli.commands.validation.get_tool_registry_service",
|
||||
return_value=svc,
|
||||
):
|
||||
result = runner.invoke(
|
||||
|
||||
@@ -41,7 +41,7 @@ def attach_plain_tool_rejected() -> None:
|
||||
actual_type="tool",
|
||||
)
|
||||
with patch(
|
||||
"cleveragents.cli.commands.validation._get_tool_registry_service",
|
||||
"cleveragents.cli.commands.validation.get_tool_registry_service",
|
||||
return_value=mock_svc,
|
||||
):
|
||||
result = runner.invoke(
|
||||
@@ -79,7 +79,7 @@ def attach_validation_accepted() -> None:
|
||||
mock_attachment.created_at = "2026-01-01T00:00:00"
|
||||
mock_svc.attach_validation.return_value = mock_attachment
|
||||
with patch(
|
||||
"cleveragents.cli.commands.validation._get_tool_registry_service",
|
||||
"cleveragents.cli.commands.validation.get_tool_registry_service",
|
||||
return_value=mock_svc,
|
||||
):
|
||||
result = runner.invoke(
|
||||
@@ -107,7 +107,7 @@ def attach_plain_tool_dict_rejected() -> None:
|
||||
actual_type="tool",
|
||||
)
|
||||
with patch(
|
||||
"cleveragents.cli.commands.validation._get_tool_registry_service",
|
||||
"cleveragents.cli.commands.validation.get_tool_registry_service",
|
||||
return_value=mock_svc,
|
||||
):
|
||||
result = runner.invoke(
|
||||
|
||||
@@ -8,6 +8,7 @@ subtypes) and their lifecycle attachments to resources.
|
||||
| Command | Description |
|
||||
|--------------------------------|------------------------------------------|
|
||||
| ``agents validation add`` | Register validation from YAML config |
|
||||
| ``agents validation list`` | List all registered validations |
|
||||
| ``agents validation attach`` | Attach validation to a resource |
|
||||
| ``agents validation detach`` | Detach a validation attachment |
|
||||
|
||||
@@ -55,8 +56,16 @@ import typer
|
||||
import yaml
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
|
||||
from cleveragents.cli.bootstrap import ensure_cli_database_bootstrapped
|
||||
from cleveragents.cli.commands.validation_helpers import (
|
||||
attachment_dict,
|
||||
compile_pattern,
|
||||
get_tool_registry_service,
|
||||
get_validation_name,
|
||||
print_validation,
|
||||
validation_spec_dict,
|
||||
)
|
||||
from cleveragents.cli.formatting import OutputFormat, format_output
|
||||
from cleveragents.core.exceptions import (
|
||||
CleverAgentsError,
|
||||
@@ -75,94 +84,8 @@ _FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)
|
||||
|
||||
|
||||
def _get_tool_registry_service() -> Any:
|
||||
"""Get the ToolRegistryService from the DI container.
|
||||
|
||||
Delegates to get_container().tool_registry_service() so that
|
||||
service wiring is managed exclusively by the container (Forgejo #3006).
|
||||
"""
|
||||
from cleveragents.application.container import get_container
|
||||
|
||||
ensure_cli_database_bootstrapped()
|
||||
|
||||
return get_container().tool_registry_service()
|
||||
|
||||
|
||||
def _validation_spec_dict(tool: Any) -> dict[str, Any]:
|
||||
"""Return validation data as a dict for CLI rendering."""
|
||||
if hasattr(tool, "as_cli_dict"):
|
||||
return dict(tool.as_cli_dict())
|
||||
|
||||
if isinstance(tool, dict):
|
||||
result: dict[str, Any] = {
|
||||
"name": tool.get("name", ""),
|
||||
"description": tool.get("description", ""),
|
||||
"source": tool.get("source", ""),
|
||||
"tool_type": tool.get("tool_type", "validation"),
|
||||
"mode": tool.get("mode", "required"),
|
||||
}
|
||||
if tool.get("wraps"):
|
||||
result["wraps"] = tool["wraps"]
|
||||
if tool.get("transform"):
|
||||
result["transform"] = tool["transform"]
|
||||
return result
|
||||
|
||||
return {"name": str(tool)}
|
||||
|
||||
|
||||
def _attachment_dict(attachment: Any) -> dict[str, Any]:
|
||||
"""Convert an attachment to a plain dict for output formatting."""
|
||||
if isinstance(attachment, dict):
|
||||
return {
|
||||
"attachment_id": attachment.get("attachment_id", ""),
|
||||
"validation_name": attachment.get("validation_name", ""),
|
||||
"resource_id": attachment.get("resource_id", ""),
|
||||
"mode": attachment.get("mode", "required"),
|
||||
"project_name": attachment.get("project_name"),
|
||||
"plan_id": attachment.get("plan_id"),
|
||||
"created_at": attachment.get("created_at", ""),
|
||||
}
|
||||
return {
|
||||
"attachment_id": getattr(attachment, "attachment_id", ""),
|
||||
"validation_name": getattr(attachment, "validation_name", ""),
|
||||
"resource_id": getattr(attachment, "resource_id", ""),
|
||||
"mode": getattr(attachment, "mode", "required"),
|
||||
"project_name": getattr(attachment, "project_name", None),
|
||||
"plan_id": getattr(attachment, "plan_id", None),
|
||||
"created_at": str(getattr(attachment, "created_at", "")),
|
||||
}
|
||||
|
||||
|
||||
def _print_validation(
|
||||
tool: Any,
|
||||
title: str = "Validation",
|
||||
fmt: str = OutputFormat.RICH.value,
|
||||
) -> None:
|
||||
"""Print validation details in the requested format."""
|
||||
data = _validation_spec_dict(tool)
|
||||
if fmt != OutputFormat.RICH.value:
|
||||
console.print(format_output(data, fmt))
|
||||
return
|
||||
|
||||
name = data.get("name", "")
|
||||
desc = data.get("description", "")
|
||||
source = data.get("source", "")
|
||||
mode = data.get("mode", "required")
|
||||
|
||||
details = (
|
||||
f"[bold]Name:[/bold] {name}\n"
|
||||
f"[bold]Description:[/bold] {desc}\n"
|
||||
f"[bold]Source:[/bold] {source}\n"
|
||||
f"[bold]Mode:[/bold] {mode}"
|
||||
)
|
||||
|
||||
wraps = data.get("wraps")
|
||||
if wraps:
|
||||
details += f"\n[bold]Wraps:[/bold] {wraps}"
|
||||
transform = data.get("transform")
|
||||
if transform:
|
||||
details += f"\n[bold]Transform:[/bold] {transform}"
|
||||
|
||||
console.print(Panel(details, title=title, expand=False))
|
||||
"""Compatibility wrapper for tests and helpers that patch the old CLI hook."""
|
||||
return get_tool_registry_service()
|
||||
|
||||
|
||||
@app.command("add")
|
||||
@@ -239,11 +162,11 @@ 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)
|
||||
return
|
||||
|
||||
registered = service.register_tool(validation)
|
||||
_print_validation(registered, title="Validation Registered", fmt=fmt)
|
||||
print_validation(registered, title="Validation Registered", fmt=fmt)
|
||||
|
||||
except FileNotFoundError as exc:
|
||||
console.print(f"[red]Config file error:[/red] {exc}")
|
||||
@@ -262,6 +185,92 @@ def add(
|
||||
raise typer.Abort() from exc
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def list_validations(
|
||||
namespace: Annotated[
|
||||
str | None,
|
||||
typer.Option("--namespace", "-n", help="Filter by namespace"),
|
||||
] = None,
|
||||
source: Annotated[
|
||||
str | None,
|
||||
typer.Option("--source", "-s", help="Filter by source type"),
|
||||
] = None,
|
||||
pattern: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--pattern",
|
||||
"-p",
|
||||
help="Filter validation names by regex pattern",
|
||||
),
|
||||
] = None,
|
||||
fmt: Annotated[
|
||||
str,
|
||||
typer.Option("--format", "-f", help=_FORMAT_HELP),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""List validations with optional filters.
|
||||
|
||||
Examples:
|
||||
agents validation list
|
||||
agents validation list --namespace local
|
||||
agents validation list --source custom
|
||||
agents validation list --pattern "coverage-.*"
|
||||
agents validation list --format json
|
||||
"""
|
||||
try:
|
||||
service = _get_tool_registry_service()
|
||||
validations = service.list_tools(
|
||||
namespace=namespace,
|
||||
tool_type="validation",
|
||||
source=source,
|
||||
)
|
||||
|
||||
# Apply regex filter if provided
|
||||
if pattern:
|
||||
compiled_pattern = compile_pattern(pattern)
|
||||
validations = [
|
||||
v
|
||||
for v in validations
|
||||
if compiled_pattern.search(get_validation_name(v))
|
||||
]
|
||||
|
||||
if not validations:
|
||||
console.print("[yellow]No validations found.[/yellow]")
|
||||
console.print("Register one with 'agents validation add --config <file>'")
|
||||
return
|
||||
|
||||
# Non-rich formats use the formatting helper
|
||||
if fmt != OutputFormat.RICH.value:
|
||||
data = [dict(validation_spec_dict(v)) for v in validations]
|
||||
console.print(format_output(data, fmt))
|
||||
return
|
||||
|
||||
# Rich table
|
||||
table = Table(title=f"Validations ({len(validations)} total)")
|
||||
table.add_column("Name", style="cyan")
|
||||
table.add_column("Mode", style="blue")
|
||||
table.add_column("Source", style="magenta")
|
||||
table.add_column("Description", style="dim")
|
||||
|
||||
for validation in validations:
|
||||
spec = validation_spec_dict(validation)
|
||||
desc = str(spec.get("description", ""))
|
||||
if len(desc) > 40:
|
||||
desc = desc[:37] + "..."
|
||||
table.add_row(
|
||||
str(spec.get("name", "")),
|
||||
str(spec.get("mode", "required")),
|
||||
str(spec.get("source", "")),
|
||||
desc,
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
except CleverAgentsError as exc:
|
||||
console.print(f"[red]Error:[/red] {exc.message}")
|
||||
raise typer.Abort() from exc
|
||||
|
||||
|
||||
@app.command(
|
||||
"attach",
|
||||
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
||||
@@ -352,7 +361,7 @@ def attach(
|
||||
args=extra_args,
|
||||
)
|
||||
|
||||
att_data = _attachment_dict(attachment)
|
||||
att_data = attachment_dict(attachment)
|
||||
|
||||
if fmt != OutputFormat.RICH.value:
|
||||
console.print(format_output(att_data, fmt))
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Helper functions for validation CLI commands.
|
||||
|
||||
This module contains utility functions for validation command processing,
|
||||
including formatting, filtering, and data transformation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
|
||||
from cleveragents.cli.bootstrap import ensure_cli_database_bootstrapped
|
||||
from cleveragents.cli.formatting import OutputFormat, format_output
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def get_tool_registry_service() -> Any:
|
||||
"""Get the ToolRegistryService from the DI container.
|
||||
|
||||
Delegates to get_container().tool_registry_service() so that
|
||||
service wiring is managed exclusively by the container (Forgejo #3006).
|
||||
"""
|
||||
from cleveragents.application.container import get_container
|
||||
|
||||
ensure_cli_database_bootstrapped()
|
||||
|
||||
return get_container().tool_registry_service()
|
||||
|
||||
|
||||
def validation_spec_dict(tool: Any) -> dict[str, Any]:
|
||||
"""Return validation data as a dict for CLI rendering."""
|
||||
if hasattr(tool, "as_cli_dict"):
|
||||
return dict(tool.as_cli_dict())
|
||||
|
||||
if isinstance(tool, dict):
|
||||
result: dict[str, Any] = {
|
||||
"name": tool.get("name", ""),
|
||||
"description": tool.get("description", ""),
|
||||
"source": tool.get("source", ""),
|
||||
"tool_type": tool.get("tool_type", "validation"),
|
||||
"mode": tool.get("mode", "required"),
|
||||
}
|
||||
if tool.get("wraps"):
|
||||
result["wraps"] = tool["wraps"]
|
||||
if tool.get("transform"):
|
||||
result["transform"] = tool["transform"]
|
||||
return result
|
||||
|
||||
return {"name": str(tool)}
|
||||
|
||||
|
||||
def get_validation_name(v: Any) -> str:
|
||||
"""Return the name string for a validation object or dict."""
|
||||
if isinstance(v, dict):
|
||||
return str(v.get("name", ""))
|
||||
return str(getattr(v, "name", ""))
|
||||
|
||||
|
||||
def attachment_dict(attachment: Any) -> dict[str, Any]:
|
||||
"""Convert an attachment to a plain dict for output formatting."""
|
||||
if isinstance(attachment, dict):
|
||||
return {
|
||||
"attachment_id": attachment.get("attachment_id", ""),
|
||||
"validation_name": attachment.get("validation_name", ""),
|
||||
"resource_id": attachment.get("resource_id", ""),
|
||||
"mode": attachment.get("mode", "required"),
|
||||
"project_name": attachment.get("project_name"),
|
||||
"plan_id": attachment.get("plan_id"),
|
||||
"created_at": attachment.get("created_at", ""),
|
||||
}
|
||||
return {
|
||||
"attachment_id": getattr(attachment, "attachment_id", ""),
|
||||
"validation_name": getattr(attachment, "validation_name", ""),
|
||||
"resource_id": getattr(attachment, "resource_id", ""),
|
||||
"mode": getattr(attachment, "mode", "required"),
|
||||
"project_name": getattr(attachment, "project_name", None),
|
||||
"plan_id": getattr(attachment, "plan_id", None),
|
||||
"created_at": str(getattr(attachment, "created_at", "")),
|
||||
}
|
||||
|
||||
|
||||
def print_validation(
|
||||
tool: Any,
|
||||
title: str = "Validation",
|
||||
fmt: str = OutputFormat.RICH.value,
|
||||
) -> None:
|
||||
"""Print validation details in the requested format."""
|
||||
data = validation_spec_dict(tool)
|
||||
if fmt != OutputFormat.RICH.value:
|
||||
console.print(format_output(data, fmt))
|
||||
return
|
||||
|
||||
name = data.get("name", "")
|
||||
desc = data.get("description", "")
|
||||
source = data.get("source", "")
|
||||
mode = data.get("mode", "required")
|
||||
|
||||
details = (
|
||||
f"[bold]Name:[/bold] {name}\n"
|
||||
f"[bold]Description:[/bold] {desc}\n"
|
||||
f"[bold]Source:[/bold] {source}\n"
|
||||
f"[bold]Mode:[/bold] {mode}"
|
||||
)
|
||||
|
||||
wraps = data.get("wraps")
|
||||
if wraps:
|
||||
details += f"\n[bold]Wraps:[/bold] {wraps}"
|
||||
transform = data.get("transform")
|
||||
if transform:
|
||||
details += f"\n[bold]Transform:[/bold] {transform}"
|
||||
|
||||
console.print(Panel(details, title=title, expand=False))
|
||||
|
||||
|
||||
def compile_pattern(pattern: str) -> re.Pattern[str]:
|
||||
"""Compile a regex pattern, raising an error if invalid."""
|
||||
try:
|
||||
return re.compile(pattern)
|
||||
except re.error as exc:
|
||||
console.print(f"[red]Invalid regex pattern:[/red] {pattern}")
|
||||
raise ValueError(f"Invalid regex pattern: {pattern}") from exc
|
||||
Reference in New Issue
Block a user