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