fix(resource): wire --clone-into into DevcontainerHandler.resolve() runtime path
The --clone-into CLI argument was registered and the helper clone_repo_into_container() was implemented, but DevcontainerHandler.resolve() never read the clone_into property or called the helper. This meant that agents resource add container-instance --clone-into <url> silently ignored the flag at runtime (acceptance criterion #2 from issue #7555 was unmet). Wire the clone step into DevcontainerHandler.resolve(): after activate_container() returns and the lifecycle tracker has a container_id, validate the URL and call clone_repo_into_container(). Also add an end-to-end BDD scenario that exercises the full handler to clone path via mocks. ISSUES CLOSED: #7555
This commit is contained in:
@@ -82,3 +82,11 @@ Feature: container-instance --clone-into argument
|
||||
Scenario: validate_clone_into_url rejects whitespace-only string
|
||||
When I validate clone-into URL with whitespace only
|
||||
Then the clone-into URL should be invalid
|
||||
|
||||
# -- End-to-end handler wiring --
|
||||
|
||||
Scenario: DevcontainerHandler.resolve() calls clone_repo_into_container when clone_into property is set
|
||||
Given a devcontainer-instance resource with clone_into "https://github.com/org/repo.git" and tracker container_id "abc123def456"
|
||||
And clone_repo_into_container is mocked to succeed
|
||||
When DevcontainerHandler.resolve() is called for the resource
|
||||
Then clone_repo_into_container should have been called with container_id "abc123def456" and url "https://github.com/org/repo.git"
|
||||
|
||||
@@ -278,3 +278,121 @@ def step_clone_into_not_required(context: Context) -> None:
|
||||
f"Expected clone-into to be optional (required=False), "
|
||||
f"got required={clone_into_arg.get('required')}"
|
||||
)
|
||||
|
||||
|
||||
# -- End-to-end handler wiring steps --
|
||||
|
||||
|
||||
@given(
|
||||
'a devcontainer-instance resource with clone_into "{clone_url}" and tracker container_id "{container_id}"'
|
||||
)
|
||||
def step_devcontainer_resource_with_clone_into(
|
||||
context: Context, clone_url: str, container_id: str
|
||||
) -> None:
|
||||
"""Set up a devcontainer-instance resource with clone_into property and tracker."""
|
||||
from cleveragents.domain.models.core.container_lifecycle import (
|
||||
ContainerLifecycleState,
|
||||
ContainerLifecycleTracker,
|
||||
)
|
||||
from cleveragents.domain.models.core.resource import Resource
|
||||
from cleveragents.resource.handlers.devcontainer import (
|
||||
clear_lifecycle_registry,
|
||||
set_lifecycle_tracker,
|
||||
)
|
||||
|
||||
# Clear any existing lifecycle state
|
||||
clear_lifecycle_registry()
|
||||
|
||||
# Create a resource with clone_into in properties
|
||||
context.handler_resource = Resource(
|
||||
resource_id="01HANDLER0000000000000001",
|
||||
name="test-devcontainer",
|
||||
resource_type_name="devcontainer-instance",
|
||||
classification="tool",
|
||||
description="Test devcontainer for clone-into wiring",
|
||||
location="/tmp/test-workspace",
|
||||
properties={"clone_into": clone_url},
|
||||
)
|
||||
|
||||
# Set up the lifecycle tracker in DISCOVERED state with container_id pre-set
|
||||
# (simulates the state after activate_container() has run and set container_id)
|
||||
tracker = ContainerLifecycleTracker(
|
||||
resource_id="01HANDLER0000000000000001",
|
||||
current_state=ContainerLifecycleState.DISCOVERED,
|
||||
container_id=container_id,
|
||||
)
|
||||
set_lifecycle_tracker(tracker)
|
||||
|
||||
context.handler_clone_url = clone_url
|
||||
context.handler_container_id = container_id
|
||||
|
||||
# Store cleanup reference
|
||||
context.add_cleanup(clear_lifecycle_registry)
|
||||
|
||||
|
||||
@given("clone_repo_into_container is mocked to succeed")
|
||||
def step_mock_clone_repo_into_container(context: Context) -> None:
|
||||
"""Mock clone_repo_into_container to capture calls without running docker."""
|
||||
mock_clone = MagicMock(return_value="/workspace")
|
||||
patcher = patch(
|
||||
"cleveragents.resource.handlers.devcontainer.clone_repo_into_container",
|
||||
mock_clone,
|
||||
)
|
||||
patcher.start()
|
||||
context.mock_clone_repo = mock_clone
|
||||
context.add_cleanup(patcher.stop)
|
||||
|
||||
|
||||
@when("DevcontainerHandler.resolve() is called for the resource")
|
||||
def step_call_devcontainer_handler_resolve(context: Context) -> None:
|
||||
"""Call DevcontainerHandler.resolve() with the test resource."""
|
||||
from cleveragents.resource.handlers.devcontainer import DevcontainerHandler
|
||||
|
||||
handler = DevcontainerHandler()
|
||||
mock_sandbox_manager = MagicMock()
|
||||
mock_bound_resource = MagicMock()
|
||||
|
||||
# Patch activate_container to be a no-op (tracker already has container_id)
|
||||
# Patch BaseResourceHandler.resolve to return a mock BoundResource
|
||||
with patch(
|
||||
"cleveragents.resource.handlers.devcontainer.activate_container"
|
||||
), patch(
|
||||
"cleveragents.resource.handlers._base.BaseResourceHandler.resolve",
|
||||
return_value=mock_bound_resource,
|
||||
):
|
||||
context.handler_resolve_error = None
|
||||
context.handler_resolve_result = None
|
||||
try:
|
||||
context.handler_resolve_result = handler.resolve(
|
||||
resource=context.handler_resource,
|
||||
plan_id="test-plan-001",
|
||||
slot_name="test-slot",
|
||||
sandbox_manager=mock_sandbox_manager,
|
||||
)
|
||||
except Exception as exc:
|
||||
context.handler_resolve_error = exc
|
||||
|
||||
|
||||
@then(
|
||||
'clone_repo_into_container should have been called with container_id "{expected_container_id}" and url "{expected_url}"'
|
||||
)
|
||||
def step_assert_clone_called(
|
||||
context: Context, expected_container_id: str, expected_url: str
|
||||
) -> None:
|
||||
"""Assert that clone_repo_into_container was called with the expected arguments."""
|
||||
assert context.handler_resolve_error is None, (
|
||||
f"Expected resolve() to succeed but got error: {context.handler_resolve_error!r}"
|
||||
)
|
||||
mock_clone = context.mock_clone_repo
|
||||
assert mock_clone.called, (
|
||||
"Expected clone_repo_into_container to have been called, but it was not"
|
||||
)
|
||||
call_args = mock_clone.call_args
|
||||
actual_container_id = call_args[0][0] if call_args[0] else call_args[1].get("container_id")
|
||||
actual_url = call_args[0][1] if len(call_args[0]) > 1 else call_args[1].get("repo_url")
|
||||
assert actual_container_id == expected_container_id, (
|
||||
f"Expected container_id '{expected_container_id}', got '{actual_container_id}'"
|
||||
)
|
||||
assert actual_url == expected_url, (
|
||||
f"Expected url '{expected_url}', got '{actual_url}'"
|
||||
)
|
||||
|
||||
@@ -72,6 +72,10 @@ from cleveragents.resource.handlers._devcontainer_internals import (
|
||||
list_active_containers,
|
||||
set_lifecycle_tracker,
|
||||
)
|
||||
from cleveragents.resource.handlers.clone_into import (
|
||||
clone_repo_into_container,
|
||||
validate_clone_into_url,
|
||||
)
|
||||
from cleveragents.resource.handlers.devcontainer_cleanup import (
|
||||
evict_terminal_trackers,
|
||||
list_active_containers_for_session,
|
||||
@@ -119,6 +123,7 @@ __all__ = [
|
||||
"_stop_health_check",
|
||||
"activate_container",
|
||||
"clear_lifecycle_registry",
|
||||
"clone_repo_into_container",
|
||||
"evict_terminal_trackers",
|
||||
"get_lifecycle_tracker",
|
||||
"list_active_containers",
|
||||
@@ -128,6 +133,7 @@ __all__ = [
|
||||
"start_health_check",
|
||||
"stop_all_active_containers",
|
||||
"stop_container",
|
||||
"validate_clone_into_url",
|
||||
]
|
||||
|
||||
|
||||
@@ -319,6 +325,10 @@ class DevcontainerHandler(BaseResourceHandler):
|
||||
base-class sandbox resolution. For other resource types (e.g.
|
||||
``devcontainer-file``), it delegates directly.
|
||||
|
||||
If the resource has a ``clone_into`` property, the specified git
|
||||
repository is cloned into the container after activation (issue
|
||||
#7555).
|
||||
|
||||
Args:
|
||||
resource: The resource domain object to resolve.
|
||||
plan_id: The plan requesting the sandbox.
|
||||
@@ -343,6 +353,26 @@ class DevcontainerHandler(BaseResourceHandler):
|
||||
session_id=plan_id,
|
||||
)
|
||||
|
||||
# Issue #7555: wire --clone-into after container is running.
|
||||
# Read the updated tracker to get the container_id assigned
|
||||
# during activation, then clone the repository if requested.
|
||||
if resource.properties:
|
||||
clone_into_url = resource.properties.get("clone_into")
|
||||
if clone_into_url and isinstance(clone_into_url, str):
|
||||
validate_clone_into_url(clone_into_url)
|
||||
updated_tracker = get_lifecycle_tracker(resource.resource_id)
|
||||
if updated_tracker.container_id:
|
||||
logger.info(
|
||||
"Cloning '%s' into container '%s' (resource %s)",
|
||||
clone_into_url,
|
||||
updated_tracker.container_id,
|
||||
resource.resource_id,
|
||||
)
|
||||
clone_repo_into_container(
|
||||
updated_tracker.container_id,
|
||||
clone_into_url,
|
||||
)
|
||||
|
||||
return super().resolve(
|
||||
resource=resource,
|
||||
plan_id=plan_id,
|
||||
|
||||
Reference in New Issue
Block a user