Files
placeholder/features/steps/project_cli_coverage_r2_steps.py
freemo a808c395f9 test(coverage): add Behave BDD tests to improve unit test coverage across 53 source modules
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
2026-03-09 13:01:58 -04:00

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}"
)