forked from HAL9000/cleveragents-core
a808c395f9
Add 53 new .feature files and corresponding step definition files targeting uncovered lines identified in build/coverage.xml. Fix AmbiguousStep conflicts in 7 pre-existing step files by disambiguating step text. New tests cover: ACP clients/facade, actor CLI/config, application container, ACMS service/strategies, async worker, automation profile CLI, autonomy guardrail, bridge, change model, config CLI/service, context service, cross-plan correction, database models, decision service, decomposition clustering/service, discovery handler, langchain chat provider, langgraph nodes, materializers, multi-project service, plan apply/CLI/lifecycle/model/ preflight/resume/service, PostgreSQL analyzer, project CLI/context CLI, provider registry, reactive application/route, repositories, resolver handler, resource registry service, resume model, retry patterns, sandbox protocol, server CLI, skill CLI/service, skills registry, subplan execution/service, system CLI, UKO loader, UoW, and YAML template engine. Closes #645
320 lines
12 KiB
Python
320 lines
12 KiB
Python
"""Step definitions for project_cli_coverage_r2.feature.
|
|
|
|
Exercises the remaining uncovered lines in
|
|
``cleveragents.cli.commands.project``:
|
|
|
|
* Lines 73, 75, 76 - ``_get_namespaced_project_repo()`` body
|
|
* Lines 81, 83, 84 - ``_get_resource_link_repo()`` body
|
|
* Lines 89, 91, 92 - ``_get_resource_registry_service()`` body
|
|
* Line 152 - ``str(lr.linked_at)`` fallback (no ``.isoformat``)
|
|
* Line 164 - ``str(project.created_at)`` fallback
|
|
* Line 167 - ``str(project.updated_at)`` fallback
|
|
* Line 843 - ``str(proj.created_at)[:10]`` fallback in list table
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from behave import given, then, when
|
|
from typer.testing import CliRunner
|
|
|
|
from cleveragents.cli.commands.project import (
|
|
_project_spec_dict,
|
|
app,
|
|
)
|
|
|
|
runner = CliRunner()
|
|
|
|
# Patch targets
|
|
_PATCH_PROJECT_REPO = "cleveragents.cli.commands.project._get_namespaced_project_repo"
|
|
_PATCH_CONTAINER = "cleveragents.cli.commands.project.get_container"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_linked_resource(
|
|
resource_id: str = "res-100",
|
|
project_read_only: bool = False,
|
|
alias: str | None = None,
|
|
linked_at: Any = None,
|
|
) -> MagicMock:
|
|
"""Build a mock linked-resource object."""
|
|
lr = MagicMock()
|
|
lr.resource_id = resource_id
|
|
lr.project_read_only = project_read_only
|
|
lr.alias = alias
|
|
lr.linked_at = (
|
|
linked_at if linked_at is not None else datetime(2025, 3, 1, tzinfo=UTC)
|
|
)
|
|
return lr
|
|
|
|
|
|
def _make_project(
|
|
namespaced_name: str = "local/r2-proj",
|
|
namespace: str = "local",
|
|
name: str = "r2-proj",
|
|
description: str | None = "Round-2 project",
|
|
linked_resources: list | None = None,
|
|
created_at: Any = None,
|
|
updated_at: Any = None,
|
|
) -> MagicMock:
|
|
"""Build a mock NamespacedProject."""
|
|
proj = MagicMock()
|
|
proj.namespaced_name = namespaced_name
|
|
proj.namespace = namespace
|
|
proj.name = name
|
|
proj.description = description
|
|
proj.linked_resources = linked_resources if linked_resources is not None else []
|
|
proj.created_at = (
|
|
created_at if created_at is not None else datetime(2025, 1, 1, tzinfo=UTC)
|
|
)
|
|
proj.updated_at = (
|
|
updated_at if updated_at is not None else datetime(2025, 1, 2, tzinfo=UTC)
|
|
)
|
|
return proj
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Background
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("the r2 project CLI mocks are prepared")
|
|
def step_r2_prepare(context: Any) -> None:
|
|
context.r2_result = None
|
|
context.r2_helper_result = None
|
|
context.r2_spec_dict = None
|
|
context.r2_project = None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _get_namespaced_project_repo (lines 73, 75, 76)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I call the actual _get_namespaced_project_repo helper with a mocked container")
|
|
def step_call_get_namespaced_project_repo(context: Any) -> None:
|
|
mock_container = MagicMock()
|
|
mock_repo = MagicMock(spec_name="MockNamespacedProjectRepo")
|
|
mock_container.namespaced_project_repo.return_value = mock_repo
|
|
|
|
# Patch get_container where it is imported inside the function body.
|
|
# The function does: ``from cleveragents.application.container import get_container``
|
|
# so we patch the canonical location.
|
|
with patch(
|
|
"cleveragents.application.container.get_container",
|
|
return_value=mock_container,
|
|
):
|
|
from cleveragents.cli.commands.project import _get_namespaced_project_repo
|
|
|
|
context.r2_helper_result = _get_namespaced_project_repo()
|
|
context.r2_expected_repo = mock_repo
|
|
|
|
|
|
@then("the r2 helper result should be the mock namespaced project repo")
|
|
def step_assert_namespaced_project_repo(context: Any) -> None:
|
|
assert context.r2_helper_result is context.r2_expected_repo, (
|
|
f"Expected the mock repo, got {context.r2_helper_result!r}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _get_resource_link_repo (lines 81, 83, 84)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I call the actual _get_resource_link_repo helper with a mocked container")
|
|
def step_call_get_resource_link_repo(context: Any) -> None:
|
|
mock_container = MagicMock()
|
|
mock_link_repo = MagicMock(spec_name="MockResourceLinkRepo")
|
|
mock_container.project_resource_link_repo.return_value = mock_link_repo
|
|
|
|
with patch(
|
|
"cleveragents.application.container.get_container",
|
|
return_value=mock_container,
|
|
):
|
|
from cleveragents.cli.commands.project import _get_resource_link_repo
|
|
|
|
context.r2_helper_result = _get_resource_link_repo()
|
|
context.r2_expected_repo = mock_link_repo
|
|
|
|
|
|
@then("the r2 helper result should be the mock resource link repo")
|
|
def step_assert_resource_link_repo(context: Any) -> None:
|
|
assert context.r2_helper_result is context.r2_expected_repo, (
|
|
f"Expected the mock link repo, got {context.r2_helper_result!r}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _get_resource_registry_service (lines 89, 91, 92)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I call the actual _get_resource_registry_service helper with a mocked container")
|
|
def step_call_get_resource_registry_service(context: Any) -> None:
|
|
mock_container = MagicMock()
|
|
mock_registry = MagicMock(spec_name="MockResourceRegistryService")
|
|
mock_container.resource_registry_service.return_value = mock_registry
|
|
|
|
with patch(
|
|
"cleveragents.application.container.get_container",
|
|
return_value=mock_container,
|
|
):
|
|
from cleveragents.cli.commands.project import _get_resource_registry_service
|
|
|
|
context.r2_helper_result = _get_resource_registry_service()
|
|
context.r2_expected_repo = mock_registry
|
|
|
|
|
|
@then("the r2 helper result should be the mock resource registry service")
|
|
def step_assert_resource_registry_service(context: Any) -> None:
|
|
assert context.r2_helper_result is context.r2_expected_repo, (
|
|
f"Expected the mock registry, got {context.r2_helper_result!r}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _project_spec_dict fallback: linked_at as string (line 152)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a mock project with a linked resource whose linked_at is a plain string")
|
|
def step_project_with_string_linked_at(context: Any) -> None:
|
|
lr = _make_linked_resource(linked_at="2025-03-01-string")
|
|
# Remove the isoformat attribute so hasattr(lr.linked_at, "isoformat") is False
|
|
# A plain string does NOT have isoformat, so just using a str is enough.
|
|
context.r2_project = _make_project(linked_resources=[lr])
|
|
|
|
|
|
@when("I call _project_spec_dict on that project")
|
|
def step_call_project_spec_dict(context: Any) -> None:
|
|
context.r2_spec_dict = _project_spec_dict(context.r2_project)
|
|
|
|
|
|
@then("the linked resource linked_at should be the plain string value")
|
|
def step_assert_linked_at_string(context: Any) -> None:
|
|
lr_data = context.r2_spec_dict["linked_resources"][0]
|
|
assert lr_data["linked_at"] == "2025-03-01-string", (
|
|
f"Expected '2025-03-01-string', got {lr_data['linked_at']!r}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _project_spec_dict fallback: created_at as string (line 164)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a mock project whose created_at is a plain string")
|
|
def step_project_with_string_created_at(context: Any) -> None:
|
|
context.r2_project = _make_project(
|
|
created_at="2025-01-01-string",
|
|
# Keep updated_at as a proper datetime so only line 164 fires
|
|
updated_at=datetime(2025, 1, 2, tzinfo=UTC),
|
|
)
|
|
|
|
|
|
@then("the result created_at should be the plain string value")
|
|
def step_assert_created_at_string(context: Any) -> None:
|
|
assert context.r2_spec_dict["created_at"] == "2025-01-01-string", (
|
|
f"Expected '2025-01-01-string', got {context.r2_spec_dict['created_at']!r}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _project_spec_dict fallback: updated_at as string (line 167)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a mock project whose updated_at is a plain string")
|
|
def step_project_with_string_updated_at(context: Any) -> None:
|
|
context.r2_project = _make_project(
|
|
# Keep created_at as a proper datetime so only line 167 fires
|
|
created_at=datetime(2025, 1, 1, tzinfo=UTC),
|
|
updated_at="2025-01-02-string",
|
|
)
|
|
|
|
|
|
@then("the result updated_at should be the plain string value")
|
|
def step_assert_updated_at_string(context: Any) -> None:
|
|
assert context.r2_spec_dict["updated_at"] == "2025-01-02-string", (
|
|
f"Expected '2025-01-02-string', got {context.r2_spec_dict['updated_at']!r}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _project_spec_dict: all dates are plain strings at once
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a mock project where all dates and linked_at are plain strings")
|
|
def step_project_with_all_string_dates(context: Any) -> None:
|
|
lr = _make_linked_resource(linked_at="2025-03-01-lr-str")
|
|
context.r2_project = _make_project(
|
|
linked_resources=[lr],
|
|
created_at="2025-01-01-cr-str",
|
|
updated_at="2025-01-02-up-str",
|
|
)
|
|
|
|
|
|
@then("all date fields in the result should be plain string values")
|
|
def step_assert_all_dates_string(context: Any) -> None:
|
|
d = context.r2_spec_dict
|
|
assert d["created_at"] == "2025-01-01-cr-str", (
|
|
f"created_at mismatch: {d['created_at']!r}"
|
|
)
|
|
assert d["updated_at"] == "2025-01-02-up-str", (
|
|
f"updated_at mismatch: {d['updated_at']!r}"
|
|
)
|
|
lr_data = d["linked_resources"][0]
|
|
assert lr_data["linked_at"] == "2025-03-01-lr-str", (
|
|
f"linked_at mismatch: {lr_data['linked_at']!r}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# list_projects rich table: created_at without strftime (line 843)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("the r2 project repo returns projects with string created_at values")
|
|
def step_project_repo_returns_string_dates(context: Any) -> None:
|
|
# A project whose created_at is a plain string (no .strftime method)
|
|
proj = _make_project(
|
|
namespaced_name="local/str-date-proj",
|
|
namespace="local",
|
|
name="str-date-proj",
|
|
description="Has string dates",
|
|
created_at="2025-06-15T12:00:00",
|
|
updated_at="2025-06-16T12:00:00",
|
|
)
|
|
context.r2_mock_repo = MagicMock()
|
|
context.r2_mock_repo.list_projects.return_value = [proj]
|
|
|
|
|
|
@when("I invoke the r2 CLI list command with rich format")
|
|
def step_invoke_list_rich_format(context: Any) -> None:
|
|
with patch(_PATCH_PROJECT_REPO, return_value=context.r2_mock_repo):
|
|
context.r2_result = runner.invoke(app, ["list", "--format", "rich"])
|
|
|
|
|
|
@then("the r2 CLI output should contain the truncated string date")
|
|
def step_assert_list_contains_truncated_date(context: Any) -> None:
|
|
output = context.r2_result.output
|
|
# str(proj.created_at)[:10] should yield "2025-06-15"
|
|
assert "2025-06-15" in output, f"Expected '2025-06-15' in output, got:\n{output}"
|
|
|
|
|
|
@then("the r2 CLI exit code should be {code:d}")
|
|
def step_r2_exit_code(context: Any, code: int) -> None:
|
|
actual = context.r2_result.exit_code
|
|
assert actual == code, (
|
|
f"Expected exit code {code}, got {actual}. Output:\n{context.r2_result.output}"
|
|
)
|