"""Step definitions for container_id_integration.feature.""" from __future__ import annotations import contextlib from io import StringIO from typing import Any from unittest.mock import MagicMock, patch import click from behave import given, then, when # type: ignore[import-untyped] from behave.runner import Context # type: ignore[import-untyped] from rich.console import Console from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from cleveragents.application.services.resource_registry_service import ( ResourceRegistryService, ) from cleveragents.domain.models.core.resource import Resource from cleveragents.infrastructure.database.models import Base from cleveragents.resource.handlers.devcontainer import DevcontainerHandler # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_cid_service(context: Context) -> ResourceRegistryService: """Create or return a cached ResourceRegistryService for container_id tests.""" if not hasattr(context, "cid_service"): engine = create_engine("sqlite:///:memory:", echo=False) Base.metadata.create_all(engine) factory = sessionmaker(bind=engine, expire_on_commit=False) context.cid_service = ResourceRegistryService(session_factory=factory) return context.cid_service def _patch_cid_service(context: Context) -> Any: """Monkey-patch the CLI to return our in-memory service.""" import cleveragents.cli.commands.resource as resource_mod orig_fn = resource_mod._get_registry_service def _mock_get() -> ResourceRegistryService: return _make_cid_service(context) resource_mod._get_registry_service = _mock_get # type: ignore[assignment] return orig_fn def _unpatch_cid_service(orig_fn: Any) -> None: """Restore original service getter.""" import cleveragents.cli.commands.resource as resource_mod resource_mod._get_registry_service = orig_fn # type: ignore[assignment] def _capture_cid_output(func: Any, *args: Any, **kwargs: Any) -> tuple[str, bool]: """Run a CLI function capturing its console output and success status.""" buf = StringIO() console = Console( file=buf, width=200, no_color=True, highlight=False, force_terminal=False ) import cleveragents.cli.commands.resource as resource_mod orig_console = resource_mod.console resource_mod.console = console # type: ignore[assignment] failed = False try: with contextlib.redirect_stdout(buf): func(*args, **kwargs) except (SystemExit, click.exceptions.Abort): failed = True except Exception: failed = True finally: resource_mod.console = orig_console # type: ignore[assignment] return buf.getvalue(), failed # --------------------------------------------------------------------------- # Given steps # --------------------------------------------------------------------------- @given("a fresh in-memory resource registry for container_id") def step_cid_fresh_registry(context: Context) -> None: """Set up a fresh in-memory database for container_id tests.""" engine = create_engine("sqlite:///:memory:", echo=False) Base.metadata.create_all(engine) factory = sessionmaker(bind=engine, expire_on_commit=False) context.cid_service = ResourceRegistryService(session_factory=factory) context.cid_output = "" context.cid_failed = False @given("built-in types are bootstrapped for container_id") def step_cid_bootstrap_builtins(context: Context) -> None: """Bootstrap built-in resource types for container_id tests.""" service = _make_cid_service(context) service.bootstrap_builtin_types() @given('a container-instance resource with stored container_id "{cid}"') def step_cid_resource_with_stored_id(context: Context, cid: str) -> None: """Create a Resource domain object with container_id in properties.""" resource = MagicMock(spec=Resource) resource.properties = {"container_id": cid} resource.location = None context.cid_mock_resource = resource context.cid_docker_called = False # --------------------------------------------------------------------------- # When steps # --------------------------------------------------------------------------- @when('I run resource add container-instance with container-id "{cid}"') def step_cid_run_add_with_container_id(context: Context, cid: str) -> None: """Run resource_add for container-instance with --container-id.""" from cleveragents.cli.commands.resource import resource_add orig = _patch_cid_service(context) try: output, failed = _capture_cid_output( resource_add, type_name="container-instance", name="local/test-ctr", path=None, branch=None, description=None, image=None, container_id=cid, mount=None, clone_into=None, update=False, read_only=False, fmt="rich", ) context.cid_output = output context.cid_failed = failed finally: _unpatch_cid_service(orig) @when("I run resource add container-instance with both container-id and image") def step_cid_run_add_both_flags(context: Context) -> None: """Run resource_add with both --container-id and --image (should fail).""" from cleveragents.cli.commands.resource import resource_add orig = _patch_cid_service(context) try: output, failed = _capture_cid_output( resource_add, type_name="container-instance", name="local/test-ctr", path=None, branch=None, description=None, image="ubuntu:latest", container_id="abc123def456", mount=None, clone_into=None, update=False, read_only=False, fmt="rich", ) context.cid_output = output context.cid_failed = failed finally: _unpatch_cid_service(orig) @when("I run resource add container-instance with no image or container-id") def step_cid_run_add_no_flags(context: Context) -> None: """Run resource_add for container-instance with neither flag (should fail).""" from cleveragents.cli.commands.resource import resource_add orig = _patch_cid_service(context) try: output, failed = _capture_cid_output( resource_add, type_name="container-instance", name="local/test-ctr", path=None, branch=None, description=None, image=None, container_id=None, mount=None, clone_into=None, update=False, read_only=False, fmt="rich", ) context.cid_output = output context.cid_failed = failed finally: _unpatch_cid_service(orig) @when('I show the container_id resource "{name}"') def step_cid_show_resource(context: Context, name: str) -> None: """Run resource_show for the given resource name.""" from cleveragents.cli.commands.resource import resource_show orig = _patch_cid_service(context) try: output, failed = _capture_cid_output( resource_show, resource=name, fmt="rich", ) context.cid_show_output = output context.cid_show_failed = failed finally: _unpatch_cid_service(orig) @when("I call _find_running_container on the resource") def step_cid_call_find_running_container(context: Context) -> None: """Call DevcontainerHandler._find_running_container with a mock subprocess.""" with patch("subprocess.run") as mock_run: context.cid_result = DevcontainerHandler._find_running_container( context.cid_mock_resource ) context.cid_docker_called = mock_run.called # --------------------------------------------------------------------------- # Then steps # --------------------------------------------------------------------------- @then("the container_id command should succeed") def step_cid_command_succeeded(context: Context) -> None: """Assert the last container_id CLI command succeeded.""" assert not context.cid_failed, ( f"Expected command to succeed but it failed. Output:\n{context.cid_output}" ) @then("the container_id command should fail") def step_cid_command_failed(context: Context) -> None: """Assert the last container_id CLI command failed.""" assert context.cid_failed, ( f"Expected command to fail but it succeeded. Output:\n{context.cid_output}" ) @then('the container_id output should contain "{text}"') def step_cid_output_contains(context: Context, text: str) -> None: """Assert the container_id CLI output contains the expected text.""" output = context.cid_output assert text.lower() in output.lower(), ( f"Expected '{text}' in output, got:\n{output}" ) @then('the container_id show output should contain "{text}"') def step_cid_show_output_contains(context: Context, text: str) -> None: """Assert the container_id show output contains the expected text.""" output = context.cid_show_output assert text in output, f"Expected '{text}' in show output, got:\n{output}" @then('the container_id find result should be "{expected}"') def step_cid_result_equals(context: Context, expected: str) -> None: """Assert _find_running_container returned the expected container ID.""" assert context.cid_result == expected, ( f"Expected result '{expected}', got '{context.cid_result}'" ) @then("no docker subprocess should have been called") def step_cid_no_docker_called(context: Context) -> None: """Assert that subprocess.run was NOT called (docker was not invoked).""" assert not context.cid_docker_called, ( "Expected no docker subprocess call, but subprocess.run was invoked" )