565e7918a3
CI / typecheck (pull_request) Successful in 44s
CI / security (pull_request) Successful in 53s
CI / quality (pull_request) Successful in 32s
CI / lint (pull_request) Successful in 3m28s
CI / helm (pull_request) Successful in 23s
CI / build (pull_request) Successful in 54s
CI / unit_tests (pull_request) Failing after 6m21s
CI / docker (pull_request) Has been skipped
CI / e2e_tests (pull_request) Failing after 16m7s
CI / integration_tests (pull_request) Successful in 22m39s
CI / coverage (pull_request) Successful in 10m36s
CI / status-check (pull_request) Failing after 1s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Successful in 56m9s
Implemented an explicit --container-id option as an alternative to --image for container-instance resources and added comprehensive validation, data storage, and fast-path container resolution. - Added --container-id option to resource_add() in src/cleveragents/cli/commands/resource.py as an alternative to --image for container-instance resources - Enforced mutual exclusion: --container-id and --image are mutually exclusive; container-instance requires one or the other - Restricted --container-id to container-instance only (not devcontainer-instance) - Persisted container_id in resource properties when provided - Updated DevcontainerHandler._find_running_container() in src/cleveragents/resource/handlers/devcontainer.py to use the stored container_id as a fast path before falling back to docker label search - Added container_id to container-instance cli_args in src/cleveragents/application/services/_resource_registry_data.py - Created features/container_id_flag.feature and features/steps/container_id_flag_steps.py with 5 unit test scenarios - Created features/container_id_integration.feature and features/steps/container_id_integration_steps.py with 5 integration test scenarios Key design decisions: - Used isinstance(stored_id, str) guard in handler for Pyright type narrowing - Validation raises typer.Abort() for clean CLI error handling - No behavioral changes for devcontainer-instance resources ISSUES CLOSED: #2598
286 lines
9.9 KiB
Python
286 lines
9.9 KiB
Python
"""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"
|
|
)
|