feat(resource): add --clone-into to container-instance and fix devcontainer-instance sandbox strategy
- Adds a --clone-into option to the container-instance command to clone repository contents into a specified path during container setup. - Fixes the devcontainer-instance sandbox strategy to ensure proper isolation, correct mount permissions, and deterministic behavior across environments. - Updates related validation and error handling to reflect the new option and sandbox changes. ISSUES CLOSED: #7555
This commit is contained in:
+10
-1
@@ -447,6 +447,15 @@ _ALL_DATA_COLUMNS + ") " "SELECT " + _ALL_DATA_COLUMNS + " FROM v3_plans"`.
|
||||
state while preserving the underlying context directory via `ContextManager`
|
||||
(#6370).
|
||||
- **Quick Start Guide** (PR #9245): Added `docs/quickstart.md` with an end-to-end quick start guide covering prerequisites, installation, project creation, resource registration, plan/apply workflow, and troubleshooting. Updated `mkdocs.yml` navigation to include the Quick Start page.
|
||||
- **container-instance --clone-into and devcontainer-instance sandbox strategy** (#7555):
|
||||
Added `--clone-into` CLI argument to `container-instance` resource type for cloning
|
||||
a git repository into a running container. Implemented `CloneIntoHandler` with
|
||||
`clone_repo_into_container()` and `validate_clone_into_url()` helpers. Updated
|
||||
`devcontainer-instance` to use `snapshot` sandbox strategy (was `none`) to enable
|
||||
safe plan execution inside containers. Added `container-mount`, `container-exec-env`,
|
||||
and `container-port` as child types of `devcontainer-instance`. Renamed
|
||||
`ContainerLifecycleState.DETECTED` to `DISCOVERED` (value: `"discovered"`) to align
|
||||
with specification terminology.
|
||||
|
||||
- **Plan checkpoint management CLI commands** (#8683): Added `agents plan checkpoint-list <plan-id>` and `agents plan checkpoint-delete <checkpoint-id>` commands. Listing output now highlights checkpoint ID, type, created timestamp, reason, phase, and decision linkage with a concise field summary footer across rich/table/json/yaml formats. Deletion supports batch IDs, interactive confirmation (skip with `--yes`), and structured JSON/YAML responses for automation-friendly scripting.
|
||||
- **Invariant Remove CLI Command** (#8530): Implemented `agents invariant remove <id>` command that soft-deletes an invariant by ID. The command displays a confirmation prompt before removal (bypassable with `--yes`/`-y`), outputs the removed invariant ID on success, and shows a clear error message when the invariant ID does not exist. Supports `--format` flag for JSON and YAML output. Full BDD test coverage and Robot Framework integration tests included.
|
||||
@@ -1132,4 +1141,4 @@ iteration` and data corruption under concurrent plan execution. All public
|
||||
- **TUI -- Permission Question Widget**: A new inline `PermissionQuestionWidget`
|
||||
renders permission requests directly in the conversation stream for single-key
|
||||
operations. Users can allow/reject with single-key shortcuts (`a`/`A`/`r`/`R`),
|
||||
navigate with arrow keys, confirm with `Enter`, or press `v` to open the full
|
||||
navigate with arrow keys, confirm with `Enter`, or press `v` to open the full
|
||||
|
||||
@@ -80,7 +80,7 @@ class TimeTransitionValidation:
|
||||
"""Time valid transition check (1000 iterations)."""
|
||||
for _ in range(1000):
|
||||
validate_transition(
|
||||
ContainerLifecycleState.DETECTED,
|
||||
ContainerLifecycleState.DISCOVERED,
|
||||
ContainerLifecycleState.BUILDING,
|
||||
)
|
||||
|
||||
@@ -88,7 +88,7 @@ class TimeTransitionValidation:
|
||||
"""Time invalid transition check (1000 iterations)."""
|
||||
for _ in range(1000):
|
||||
validate_transition(
|
||||
ContainerLifecycleState.DETECTED,
|
||||
ContainerLifecycleState.DISCOVERED,
|
||||
ContainerLifecycleState.RUNNING,
|
||||
)
|
||||
|
||||
@@ -201,7 +201,7 @@ class TimeRegistryOperations:
|
||||
resource_id=f"01BENCHREG{i:016d}",
|
||||
current_state=ContainerLifecycleState.RUNNING
|
||||
if i % 2 == 0
|
||||
else ContainerLifecycleState.DETECTED,
|
||||
else ContainerLifecycleState.DISCOVERED,
|
||||
)
|
||||
set_lifecycle_tracker(tracker)
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
@clone-into
|
||||
Feature: container-instance --clone-into argument
|
||||
As a CleverAgents user
|
||||
I want to clone a git repository into a running container
|
||||
So that I can work with the repository inside the container environment
|
||||
|
||||
# ── URL validation ─────────────────────────────────────────
|
||||
|
||||
Scenario Outline: validate_clone_into_url accepts valid git URLs
|
||||
When I validate clone-into URL "<url>"
|
||||
Then the clone-into URL should be valid
|
||||
|
||||
Examples:
|
||||
| url |
|
||||
| https://github.com/org/repo.git |
|
||||
| http://internal.example.com/repo.git |
|
||||
| git@github.com:org/repo.git |
|
||||
| ssh://git@bitbucket.org/org/repo.git |
|
||||
| git://github.com/org/repo.git |
|
||||
| /local/path/to/repo |
|
||||
| ./relative/repo |
|
||||
|
||||
Scenario Outline: validate_clone_into_url rejects invalid URLs
|
||||
When I validate clone-into URL "<url>"
|
||||
Then the clone-into URL should be invalid
|
||||
|
||||
Examples:
|
||||
| url |
|
||||
| not-a-url |
|
||||
| ftp://example.com |
|
||||
|
||||
# ── clone_repo_into_container argument validation ──────────
|
||||
|
||||
Scenario: clone_repo_into_container rejects empty container_id
|
||||
When I call clone_repo_into_container with empty container_id
|
||||
Then it should raise ValueError mentioning "container_id"
|
||||
|
||||
Scenario: clone_repo_into_container rejects empty repo_url
|
||||
When I call clone_repo_into_container with empty repo_url
|
||||
Then it should raise ValueError mentioning "repo_url"
|
||||
|
||||
# ── Successful clone ────────────────────────────────────────
|
||||
|
||||
Scenario: clone_repo_into_container succeeds when docker exec returns 0
|
||||
Given a mock docker exec that succeeds for clone
|
||||
When I clone "https://github.com/org/repo.git" into container "abc123def456"
|
||||
Then the clone should succeed
|
||||
And the clone target should be "/workspace"
|
||||
|
||||
Scenario: clone_repo_into_container uses custom target directory
|
||||
Given a mock docker exec that succeeds for clone
|
||||
When I clone repo "https://github.com/org/repo.git" into container "abc123def456" with target "/custom/dir"
|
||||
Then the clone should succeed
|
||||
And the clone target should be "/custom/dir"
|
||||
|
||||
# ── Failure handling ────────────────────────────────────────
|
||||
|
||||
Scenario: clone_repo_into_container raises CloneIntoError when docker exec fails
|
||||
Given a mock docker exec that fails for clone with stderr "fatal: repository not found"
|
||||
When I clone "https://github.com/org/missing.git" into container "abc123def456"
|
||||
Then the clone should raise CloneIntoError
|
||||
And the CloneIntoError should mention "repository not found"
|
||||
|
||||
# ── CloneIntoError attributes ──────────────────────────────
|
||||
|
||||
Scenario: CloneIntoError stores repo_url, container_id, and stderr
|
||||
Given a CloneIntoError with repo "https://example.com/repo.git" container "ctr-001" stderr "auth failed"
|
||||
Then the CloneIntoError repo_url should be "https://example.com/repo.git"
|
||||
And the CloneIntoError container_id should be "ctr-001"
|
||||
And the CloneIntoError stderr should be "auth failed"
|
||||
|
||||
# ── container-instance resource type has --clone-into arg ──
|
||||
|
||||
Scenario: container-instance resource type has clone-into CLI argument
|
||||
When I look up the "container-instance" resource type spec
|
||||
Then the spec should have a CLI argument named "clone-into"
|
||||
And the clone-into argument should be of type "string"
|
||||
And the clone-into argument should not be required
|
||||
|
||||
# ── validate_clone_into_url edge cases ─────────────────────
|
||||
|
||||
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
|
||||
@@ -87,7 +87,7 @@ Feature: Devcontainer Session Cleanup and CLI Commands
|
||||
# ── R8: DevcontainerHandler coverage ───────────────────────
|
||||
|
||||
Scenario: DevcontainerHandler has expected class attributes and is instantiable
|
||||
Then DevcontainerHandler should have _default_strategy "none"
|
||||
Then DevcontainerHandler should have _default_strategy "snapshot"
|
||||
And DevcontainerHandler should have _type_label "devcontainer"
|
||||
And DevcontainerHandler should be instantiable
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ Feature: Devcontainer Resource Handler
|
||||
|
||||
Scenario: DevcontainerHandler has correct default strategy
|
||||
When I instantiate DevcontainerHandler
|
||||
Then its default strategy should be "none"
|
||||
Then its default strategy should be "snapshot"
|
||||
|
||||
Scenario: DevcontainerHandler has correct type label
|
||||
When I instantiate DevcontainerHandler
|
||||
|
||||
@@ -121,7 +121,7 @@ Feature: DevcontainerHandler missing protocol methods
|
||||
And dcproto the container is in RUNNING state
|
||||
And dcproto a mock sandbox manager that returns a valid sandbox
|
||||
When dcproto I create a sandbox for the resource with plan "plan-001"
|
||||
Then dcproto the sandbox result should have strategy "none"
|
||||
Then dcproto the sandbox result should have strategy "snapshot"
|
||||
And dcproto the sandbox result should have a sandbox path
|
||||
|
||||
Scenario: dcproto create_sandbox activates container when in DETECTED state
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
@devcontainer-sandbox
|
||||
Feature: devcontainer-instance uses snapshot sandbox strategy
|
||||
As a CleverAgents developer
|
||||
I want devcontainer-instance resources to use the snapshot sandbox strategy
|
||||
So that plan execution inside containers is safely isolated
|
||||
|
||||
# ── DevcontainerHandler default strategy ───────────────────
|
||||
|
||||
Scenario: DevcontainerHandler default strategy is snapshot
|
||||
When I inspect DevcontainerHandler class attributes
|
||||
Then the DevcontainerHandler _default_strategy should be "snapshot"
|
||||
|
||||
# ── devcontainer-instance resource type spec ───────────────
|
||||
|
||||
Scenario: devcontainer-instance resource type has snapshot sandbox strategy
|
||||
When I look up the "devcontainer-instance" resource type spec
|
||||
Then the spec sandbox_strategy should be "snapshot"
|
||||
|
||||
# ── devcontainer-instance child types ──────────────────────
|
||||
|
||||
Scenario: devcontainer-instance child_types includes container-mount
|
||||
When I look up the "devcontainer-instance" resource type spec
|
||||
Then the spec child_types should contain "container-mount"
|
||||
|
||||
Scenario: devcontainer-instance child_types includes container-exec-env
|
||||
When I look up the "devcontainer-instance" resource type spec
|
||||
Then the spec child_types should contain "container-exec-env"
|
||||
|
||||
Scenario: devcontainer-instance child_types includes container-port
|
||||
When I look up the "devcontainer-instance" resource type spec
|
||||
Then the spec child_types should contain "container-port"
|
||||
|
||||
Scenario: devcontainer-instance child_types includes devcontainer-file
|
||||
When I look up the "devcontainer-instance" resource type spec
|
||||
Then the spec child_types should contain "devcontainer-file"
|
||||
|
||||
# ── ContainerLifecycleState initial state ──────────────────
|
||||
|
||||
Scenario: ContainerLifecycleTracker default state is discovered
|
||||
When I create a new ContainerLifecycleTracker for resource "01SANDBOX0000000000000001"
|
||||
Then the tracker initial state should be "discovered"
|
||||
|
||||
Scenario: ContainerLifecycleState has discovered value
|
||||
When I inspect ContainerLifecycleState enum values
|
||||
Then the lifecycle enum values should contain "discovered"
|
||||
And the lifecycle enum values should not contain "detected"
|
||||
|
||||
# ── BUILTIN_TYPES registration ─────────────────────────────
|
||||
|
||||
Scenario: devcontainer-instance in BUILTIN_TYPES has snapshot strategy
|
||||
When I look up "devcontainer-instance" in BUILTIN_TYPES
|
||||
Then the BUILTIN_TYPES entry sandbox_strategy should be "snapshot"
|
||||
|
||||
Scenario: devcontainer-instance in BUILTIN_TYPES has correct child_types
|
||||
When I look up "devcontainer-instance" in BUILTIN_TYPES
|
||||
Then the BUILTIN_TYPES entry child_types should include "container-mount"
|
||||
And the BUILTIN_TYPES entry child_types should include "container-exec-env"
|
||||
And the BUILTIN_TYPES entry child_types should include "container-port"
|
||||
@@ -7,7 +7,7 @@ Feature: Devcontainer Lifecycle State Model
|
||||
|
||||
Scenario: ContainerLifecycleState has all required values
|
||||
When I inspect ContainerLifecycleState enum values
|
||||
Then the lifecycle enum should contain "detected"
|
||||
Then the lifecycle enum should contain "discovered"
|
||||
And the lifecycle enum should contain "building"
|
||||
And the lifecycle enum should contain "running"
|
||||
And the lifecycle enum should contain "stopping"
|
||||
@@ -16,7 +16,7 @@ Feature: Devcontainer Lifecycle State Model
|
||||
|
||||
Scenario: Valid transitions are accepted
|
||||
Given a lifecycle tracker for resource "01TESTLIFECYCLE0000000001"
|
||||
When I transition from "detected" to "building"
|
||||
When I transition from "discovered" to "building"
|
||||
Then the transition should succeed
|
||||
And the tracker state should be "building"
|
||||
|
||||
@@ -28,7 +28,7 @@ Feature: Devcontainer Lifecycle State Model
|
||||
|
||||
Scenario: Invalid transition is rejected
|
||||
Given a lifecycle tracker for resource "01TESTLIFECYCLE0000000003"
|
||||
When I attempt to transition from "detected" to "running"
|
||||
When I attempt to transition from "discovered" to "running"
|
||||
Then the transition should raise ValueError
|
||||
|
||||
Scenario: Stopping to error transition is valid
|
||||
@@ -50,14 +50,14 @@ Feature: Devcontainer Lifecycle State Model
|
||||
|
||||
Scenario: Transition history is recorded
|
||||
Given a lifecycle tracker for resource "01TESTLIFECYCLE0000000007"
|
||||
When I transition from "detected" to "building"
|
||||
When I transition from "discovered" to "building"
|
||||
Then the tracker should have 1 transition in history
|
||||
|
||||
# ── Lifecycle registry ─────────────────────────────────────
|
||||
|
||||
Scenario: Lifecycle tracker persists state across calls
|
||||
Given a lifecycle tracker for resource "01TESTLIFECYCLE0000000060"
|
||||
When I transition from "detected" to "building"
|
||||
When I transition from "discovered" to "building"
|
||||
And I retrieve the tracker for "01TESTLIFECYCLE0000000060"
|
||||
Then the retrieved tracker state should be "building"
|
||||
|
||||
@@ -132,14 +132,14 @@ Feature: Devcontainer Lifecycle State Model
|
||||
When I attempt to transition from "failed" to "running"
|
||||
Then the transition should raise ValueError
|
||||
|
||||
Scenario: Invalid transition detected to stopping is rejected
|
||||
Scenario: Invalid transition discovered to stopping is rejected
|
||||
Given a lifecycle tracker for resource "01TESTLIFECYCLE0000000244"
|
||||
When I attempt to transition from "detected" to "stopping"
|
||||
When I attempt to transition from "discovered" to "stopping"
|
||||
Then the transition should raise ValueError
|
||||
|
||||
Scenario: Invalid transition building to detected is rejected
|
||||
Scenario: Invalid transition building to discovered is rejected
|
||||
Given a lifecycle tracker in "building" state for resource "01TESTLIFECYCLE0000000245"
|
||||
When I attempt to transition from "building" to "detected"
|
||||
When I attempt to transition from "building" to "discovered"
|
||||
Then the transition should raise ValueError
|
||||
|
||||
# ── R4: Multi-line JSON output parsing ─────────────────────
|
||||
@@ -185,7 +185,7 @@ Feature: Devcontainer Lifecycle State Model
|
||||
|
||||
Scenario: get_lifecycle_tracker auto-creates tracker for new resource
|
||||
When I get tracker for new resource "01TESTLIFECYCLE0000000730"
|
||||
Then the auto-created tracker state should be "detected"
|
||||
Then the auto-created tracker state should be "discovered"
|
||||
And the auto-created tracker resource_id should be "01TESTLIFECYCLE0000000730"
|
||||
|
||||
Scenario: clear_lifecycle_registry stops health check threads
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Feature: agents resource list shows devcontainer lifecycle state column
|
||||
As a CleverAgents user
|
||||
I want the resource list to show the lifecycle state for devcontainer-instance resources
|
||||
So that I can see whether a devcontainer is detected, running, stopped, or failed
|
||||
So that I can see whether a devcontainer is discovered, running, stopped, or failed
|
||||
|
||||
Background:
|
||||
Given a fresh in-memory resource registry
|
||||
@@ -14,10 +14,10 @@ Feature: agents resource list shows devcontainer lifecycle state column
|
||||
And the resource output should contain "Children"
|
||||
And the resource output should contain "Projects"
|
||||
|
||||
Scenario: resource list shows "detected (not built)" for a new devcontainer-instance
|
||||
Scenario: resource list shows "discovered (not built)" for a new devcontainer-instance
|
||||
Given a devcontainer-instance resource is registered
|
||||
When I run resource list with all flag
|
||||
Then the resource output should contain "detected (not built)"
|
||||
Then the resource output should contain "discovered (not built)"
|
||||
|
||||
Scenario: resource list shows "running" state for a running devcontainer
|
||||
Given a devcontainer-instance resource is registered
|
||||
@@ -70,7 +70,7 @@ Feature: agents resource list shows devcontainer lifecycle state column
|
||||
Scenario: resource list shows lifecycle state for container-instance resources
|
||||
Given a container-instance resource is registered
|
||||
When I run resource list with all flag
|
||||
Then the resource output should contain "detected (not built)"
|
||||
Then the resource output should contain "discovered (not built)"
|
||||
|
||||
Scenario: resource list succeeds gracefully when lifecycle tracker raises an error
|
||||
Given a devcontainer-instance resource is registered
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
"""Step definitions for container_clone_into.feature.
|
||||
|
||||
Tests the CloneIntoHandler module: URL validation, argument validation,
|
||||
successful clone, failure handling, and container-instance resource type
|
||||
CLI argument registration.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from behave import given, then, when
|
||||
from behave.runner import Context
|
||||
|
||||
from cleveragents.resource.handlers.clone_into import (
|
||||
CloneIntoError,
|
||||
clone_repo_into_container,
|
||||
validate_clone_into_url,
|
||||
)
|
||||
|
||||
# ── When steps ───────────────────────────────────────────────
|
||||
|
||||
|
||||
@when('I validate clone-into URL "{url}"')
|
||||
def step_validate_url(context: Context, url: str) -> None:
|
||||
"""Validate a clone-into URL."""
|
||||
context.clone_url_valid = validate_clone_into_url(url)
|
||||
|
||||
|
||||
@when("I validate clone-into URL with whitespace only")
|
||||
def step_validate_whitespace_url(context: Context) -> None:
|
||||
"""Validate a whitespace-only clone-into URL."""
|
||||
context.clone_url_valid = validate_clone_into_url(" ")
|
||||
|
||||
|
||||
@when("I call clone_repo_into_container with empty container_id")
|
||||
def step_clone_empty_container_id(context: Context) -> None:
|
||||
"""Call clone_repo_into_container with an empty container_id."""
|
||||
context.clone_error = None
|
||||
try:
|
||||
clone_repo_into_container("", "https://github.com/org/repo.git")
|
||||
except ValueError as exc:
|
||||
context.clone_error = exc
|
||||
|
||||
|
||||
@when("I call clone_repo_into_container with empty repo_url")
|
||||
def step_clone_empty_repo_url(context: Context) -> None:
|
||||
"""Call clone_repo_into_container with an empty repo_url."""
|
||||
context.clone_error = None
|
||||
try:
|
||||
clone_repo_into_container("abc123def456", "")
|
||||
except ValueError as exc:
|
||||
context.clone_error = exc
|
||||
|
||||
|
||||
@when('I clone "{repo_url}" into container "{container_id}"')
|
||||
def step_clone_into_container(
|
||||
context: Context, repo_url: str, container_id: str
|
||||
) -> None:
|
||||
"""Clone a repository into a container."""
|
||||
context.clone_error = None
|
||||
context.clone_result = None
|
||||
try:
|
||||
context.clone_result = clone_repo_into_container(container_id, repo_url)
|
||||
except (CloneIntoError, ValueError) as exc:
|
||||
context.clone_error = exc
|
||||
|
||||
|
||||
@when(
|
||||
'I clone repo "{repo_url}" into container "{container_id}" with target "{target_dir}"'
|
||||
)
|
||||
def step_clone_into_container_at(
|
||||
context: Context, repo_url: str, container_id: str, target_dir: str
|
||||
) -> None:
|
||||
"""Clone a repository into a container at a specific target directory."""
|
||||
context.clone_error = None
|
||||
context.clone_result = None
|
||||
try:
|
||||
context.clone_result = clone_repo_into_container(
|
||||
container_id, repo_url, target_dir
|
||||
)
|
||||
except (CloneIntoError, ValueError) as exc:
|
||||
context.clone_error = exc
|
||||
|
||||
|
||||
@when('I look up the "{type_name}" resource type spec')
|
||||
def step_look_up_resource_type_spec(context: Context, type_name: str) -> None:
|
||||
"""Look up a resource type spec from BUILTIN_TYPES."""
|
||||
from cleveragents.application.services._resource_registry_data import BUILTIN_TYPES
|
||||
|
||||
context.resource_type_spec = None
|
||||
for entry in BUILTIN_TYPES:
|
||||
if entry.get("name") == type_name:
|
||||
context.resource_type_spec = entry
|
||||
break
|
||||
|
||||
|
||||
# ── Given steps ──────────────────────────────────────────────
|
||||
|
||||
|
||||
@given("a mock docker exec that succeeds for clone")
|
||||
def step_mock_docker_exec_success(context: Context) -> None:
|
||||
"""Patch subprocess.run to simulate a successful docker exec git clone."""
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
mock_result.stdout = ""
|
||||
mock_result.stderr = ""
|
||||
|
||||
patcher = patch(
|
||||
"cleveragents.resource.handlers.clone_into.subprocess.run",
|
||||
return_value=mock_result,
|
||||
)
|
||||
patcher.start()
|
||||
context.add_cleanup(patcher.stop)
|
||||
|
||||
|
||||
@given('a mock docker exec that fails for clone with stderr "{stderr_msg}"')
|
||||
def step_mock_docker_exec_failure(context: Context, stderr_msg: str) -> None:
|
||||
"""Patch subprocess.run to simulate a failed docker exec git clone."""
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 1
|
||||
mock_result.stdout = ""
|
||||
mock_result.stderr = stderr_msg
|
||||
|
||||
patcher = patch(
|
||||
"cleveragents.resource.handlers.clone_into.subprocess.run",
|
||||
return_value=mock_result,
|
||||
)
|
||||
patcher.start()
|
||||
context.add_cleanup(patcher.stop)
|
||||
|
||||
|
||||
@given(
|
||||
'a CloneIntoError with repo "{repo_url}" container "{container_id}" stderr "{stderr}"'
|
||||
)
|
||||
def step_create_clone_into_error(
|
||||
context: Context, repo_url: str, container_id: str, stderr: str
|
||||
) -> None:
|
||||
"""Create a CloneIntoError instance for attribute testing."""
|
||||
context.clone_into_error = CloneIntoError(
|
||||
repo_url=repo_url,
|
||||
container_id=container_id,
|
||||
stderr=stderr,
|
||||
)
|
||||
|
||||
|
||||
# ── Then steps ───────────────────────────────────────────────
|
||||
|
||||
|
||||
@then("the clone-into URL should be valid")
|
||||
def step_url_should_be_valid(context: Context) -> None:
|
||||
assert context.clone_url_valid is True, (
|
||||
"Expected URL to be valid but validate_clone_into_url returned False"
|
||||
)
|
||||
|
||||
|
||||
@then("the clone-into URL should be invalid")
|
||||
def step_url_should_be_invalid(context: Context) -> None:
|
||||
assert context.clone_url_valid is False, (
|
||||
"Expected URL to be invalid but validate_clone_into_url returned True"
|
||||
)
|
||||
|
||||
|
||||
@then('it should raise ValueError mentioning "{field}"')
|
||||
def step_should_raise_value_error(context: Context, field: str) -> None:
|
||||
assert context.clone_error is not None, (
|
||||
"Expected a ValueError but no error was raised"
|
||||
)
|
||||
assert isinstance(context.clone_error, ValueError), (
|
||||
f"Expected ValueError, got {type(context.clone_error).__name__}"
|
||||
)
|
||||
assert field.lower() in str(context.clone_error).lower(), (
|
||||
f"Expected '{field}' in error message, got: {context.clone_error!r}"
|
||||
)
|
||||
|
||||
|
||||
@then("the clone should succeed")
|
||||
def step_clone_should_succeed(context: Context) -> None:
|
||||
assert context.clone_error is None, (
|
||||
f"Expected clone to succeed but got error: {context.clone_error!r}"
|
||||
)
|
||||
assert context.clone_result is not None, "Expected a clone result but got None"
|
||||
|
||||
|
||||
@then('the clone target should be "{expected_target}"')
|
||||
def step_clone_target(context: Context, expected_target: str) -> None:
|
||||
assert context.clone_result == expected_target, (
|
||||
f"Expected clone target '{expected_target}', got '{context.clone_result}'"
|
||||
)
|
||||
|
||||
|
||||
@then("the clone should raise CloneIntoError")
|
||||
def step_clone_should_raise(context: Context) -> None:
|
||||
assert context.clone_error is not None, (
|
||||
"Expected CloneIntoError but no error was raised"
|
||||
)
|
||||
assert isinstance(context.clone_error, CloneIntoError), (
|
||||
f"Expected CloneIntoError, got {type(context.clone_error).__name__}"
|
||||
)
|
||||
|
||||
|
||||
@then('the CloneIntoError should mention "{text}"')
|
||||
def step_clone_error_mentions(context: Context, text: str) -> None:
|
||||
error_str = str(context.clone_error)
|
||||
assert text.lower() in error_str.lower(), (
|
||||
f"Expected '{text}' in CloneIntoError message, got: {error_str!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the CloneIntoError repo_url should be "{expected}"')
|
||||
def step_clone_error_repo_url(context: Context, expected: str) -> None:
|
||||
assert context.clone_into_error.repo_url == expected, (
|
||||
f"Expected repo_url '{expected}', got '{context.clone_into_error.repo_url}'"
|
||||
)
|
||||
|
||||
|
||||
@then('the CloneIntoError container_id should be "{expected}"')
|
||||
def step_clone_error_container_id(context: Context, expected: str) -> None:
|
||||
assert context.clone_into_error.container_id == expected, (
|
||||
f"Expected container_id '{expected}', "
|
||||
f"got '{context.clone_into_error.container_id}'"
|
||||
)
|
||||
|
||||
|
||||
@then('the CloneIntoError stderr should be "{expected}"')
|
||||
def step_clone_error_stderr(context: Context, expected: str) -> None:
|
||||
assert context.clone_into_error.stderr == expected, (
|
||||
f"Expected stderr '{expected}', got '{context.clone_into_error.stderr}'"
|
||||
)
|
||||
|
||||
|
||||
@then('the spec should have a CLI argument named "{arg_name}"')
|
||||
def step_spec_has_cli_arg(context: Context, arg_name: str) -> None:
|
||||
spec = context.resource_type_spec
|
||||
assert spec is not None, "No resource type spec found"
|
||||
cli_args: list[dict[str, Any]] = spec.get("cli_args", [])
|
||||
arg_names = [arg.get("name") for arg in cli_args]
|
||||
assert arg_name in arg_names, f"Expected CLI argument '{arg_name}' in {arg_names}"
|
||||
|
||||
|
||||
@then('the clone-into argument should be of type "{expected_type}"')
|
||||
def step_clone_into_arg_type(context: Context, expected_type: str) -> None:
|
||||
spec = context.resource_type_spec
|
||||
assert spec is not None, "No resource type spec found"
|
||||
cli_args: list[dict[str, Any]] = spec.get("cli_args", [])
|
||||
clone_into_arg = next(
|
||||
(arg for arg in cli_args if arg.get("name") == "clone-into"), None
|
||||
)
|
||||
assert clone_into_arg is not None, "clone-into argument not found"
|
||||
assert clone_into_arg.get("type") == expected_type, (
|
||||
f"Expected type '{expected_type}', got '{clone_into_arg.get('type')}'"
|
||||
)
|
||||
|
||||
|
||||
@then("the clone-into argument should not be required")
|
||||
def step_clone_into_not_required(context: Context) -> None:
|
||||
spec = context.resource_type_spec
|
||||
assert spec is not None, "No resource type spec found"
|
||||
cli_args: list[dict[str, Any]] = spec.get("cli_args", [])
|
||||
clone_into_arg = next(
|
||||
(arg for arg in cli_args if arg.get("name") == "clone-into"), None
|
||||
)
|
||||
assert clone_into_arg is not None, "clone-into argument not found"
|
||||
assert clone_into_arg.get("required") is False, (
|
||||
f"Expected clone-into to be optional (required=False), "
|
||||
f"got required={clone_into_arg.get('required')}"
|
||||
)
|
||||
@@ -158,12 +158,12 @@ def step_dcproto_container_detected(context: Context) -> None:
|
||||
set_lifecycle_tracker,
|
||||
)
|
||||
|
||||
# DETECTED is the initial state — no transition needed.
|
||||
# DISCOVERED is the initial state — no transition needed.
|
||||
tracker = ContainerLifecycleTracker(
|
||||
resource_id=context.dcproto_resource.resource_id,
|
||||
)
|
||||
set_lifecycle_tracker(tracker)
|
||||
context.dcproto_container_state = ContainerLifecycleState.DETECTED
|
||||
context.dcproto_container_state = ContainerLifecycleState.DISCOVERED
|
||||
|
||||
|
||||
@given("dcproto the container is in STOPPED state")
|
||||
|
||||
@@ -164,7 +164,7 @@ def step_validate_transition_bad_to_state(context: Context) -> None:
|
||||
context.type_error = None
|
||||
try:
|
||||
validate_transition(
|
||||
ContainerLifecycleState.DETECTED,
|
||||
ContainerLifecycleState.DISCOVERED,
|
||||
cast(ContainerLifecycleState, "starting"),
|
||||
)
|
||||
except TypeError as exc:
|
||||
@@ -205,9 +205,9 @@ def step_list_active(context: Context) -> None:
|
||||
|
||||
@when("I perform 105 transitions cycling through valid states")
|
||||
def step_perform_105_transitions(context: Context) -> None:
|
||||
"""Cycle detected->building->running->stopping->stopped->building->... 105 times."""
|
||||
"""Cycle discovered->building->running->stopping->stopped->building->... 105 times."""
|
||||
tracker = context.tracker
|
||||
# First transition: detected -> building
|
||||
# First transition: discovered -> building
|
||||
tracker = transition_state(
|
||||
tracker, ContainerLifecycleState.BUILDING, reason="t1-init"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Step definitions for devcontainer_sandbox_strategy.feature.
|
||||
|
||||
Tests that devcontainer-instance uses snapshot sandbox strategy,
|
||||
has the correct child types, and that ContainerLifecycleState uses
|
||||
'discovered' as the initial state.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from behave import then, when
|
||||
from behave.runner import Context
|
||||
|
||||
from cleveragents.domain.models.core.container_lifecycle import (
|
||||
ContainerLifecycleTracker,
|
||||
)
|
||||
from cleveragents.resource.handlers.devcontainer import DevcontainerHandler
|
||||
|
||||
# ── When steps ───────────────────────────────────────────────
|
||||
|
||||
|
||||
@when("I inspect DevcontainerHandler class attributes")
|
||||
def step_inspect_devcontainer_handler(context: Context) -> None:
|
||||
"""Capture DevcontainerHandler class attributes for inspection."""
|
||||
context.devcontainer_handler_strategy = DevcontainerHandler._default_strategy
|
||||
|
||||
|
||||
@when('I create a new ContainerLifecycleTracker for resource "{resource_id}"')
|
||||
def step_create_tracker_for_sandbox(context: Context, resource_id: str) -> None:
|
||||
"""Create a new ContainerLifecycleTracker with default state."""
|
||||
context.sandbox_tracker = ContainerLifecycleTracker(resource_id=resource_id)
|
||||
|
||||
|
||||
@when('I look up "{type_name}" in BUILTIN_TYPES')
|
||||
def step_look_up_in_builtin_types(context: Context, type_name: str) -> None:
|
||||
"""Look up a resource type entry in BUILTIN_TYPES."""
|
||||
from cleveragents.application.services._resource_registry_data import BUILTIN_TYPES
|
||||
|
||||
context.builtin_type_entry = None
|
||||
for entry in BUILTIN_TYPES:
|
||||
if entry.get("name") == type_name:
|
||||
context.builtin_type_entry = entry
|
||||
break
|
||||
|
||||
|
||||
# ── Then steps ───────────────────────────────────────────────
|
||||
|
||||
|
||||
@then('the DevcontainerHandler _default_strategy should be "{expected}"')
|
||||
def step_devcontainer_handler_strategy(context: Context, expected: str) -> None:
|
||||
strategy = context.devcontainer_handler_strategy
|
||||
strategy_value = strategy.value if hasattr(strategy, "value") else str(strategy)
|
||||
assert strategy_value == expected, (
|
||||
f"Expected DevcontainerHandler._default_strategy to be '{expected}', "
|
||||
f"got '{strategy_value}'"
|
||||
)
|
||||
|
||||
|
||||
@then('the spec sandbox_strategy should be "{expected}"')
|
||||
def step_spec_sandbox_strategy(context: Context, expected: str) -> None:
|
||||
spec = context.resource_type_spec
|
||||
assert spec is not None, "No resource type spec found"
|
||||
actual = spec.get("sandbox_strategy")
|
||||
assert actual == expected, f"Expected sandbox_strategy '{expected}', got '{actual}'"
|
||||
|
||||
|
||||
@then('the spec child_types should contain "{child_type}"')
|
||||
def step_spec_child_types_contains(context: Context, child_type: str) -> None:
|
||||
spec = context.resource_type_spec
|
||||
assert spec is not None, "No resource type spec found"
|
||||
child_types: list[str] = spec.get("child_types", [])
|
||||
assert child_type in child_types, (
|
||||
f"Expected '{child_type}' in child_types {child_types}"
|
||||
)
|
||||
|
||||
|
||||
@then('the tracker initial state should be "{expected}"')
|
||||
def step_tracker_initial_state(context: Context, expected: str) -> None:
|
||||
tracker = context.sandbox_tracker
|
||||
actual = tracker.current_state.value
|
||||
assert actual == expected, (
|
||||
f"Expected initial tracker state '{expected}', got '{actual}'"
|
||||
)
|
||||
|
||||
|
||||
@then('the lifecycle enum values should contain "{expected}"')
|
||||
def step_lifecycle_enum_contains(context: Context, expected: str) -> None:
|
||||
values = context.enum_values
|
||||
assert expected in values, (
|
||||
f"Expected '{expected}' in lifecycle enum values {values}"
|
||||
)
|
||||
|
||||
|
||||
@then('the lifecycle enum values should not contain "{unexpected}"')
|
||||
def step_lifecycle_enum_not_contains(context: Context, unexpected: str) -> None:
|
||||
values = context.enum_values
|
||||
assert unexpected not in values, (
|
||||
f"Expected '{unexpected}' NOT to be in lifecycle enum values {values}"
|
||||
)
|
||||
|
||||
|
||||
@then('the BUILTIN_TYPES entry sandbox_strategy should be "{expected}"')
|
||||
def step_builtin_types_sandbox_strategy(context: Context, expected: str) -> None:
|
||||
entry = context.builtin_type_entry
|
||||
assert entry is not None, "No BUILTIN_TYPES entry found"
|
||||
actual = entry.get("sandbox_strategy")
|
||||
assert actual == expected, (
|
||||
f"Expected BUILTIN_TYPES sandbox_strategy '{expected}', got '{actual}'"
|
||||
)
|
||||
|
||||
|
||||
@then('the BUILTIN_TYPES entry child_types should include "{child_type}"')
|
||||
def step_builtin_types_child_types(context: Context, child_type: str) -> None:
|
||||
entry = context.builtin_type_entry
|
||||
assert entry is not None, "No BUILTIN_TYPES entry found"
|
||||
child_types: list[str] = entry.get("child_types", [])
|
||||
assert child_type in child_types, (
|
||||
f"Expected '{child_type}' in BUILTIN_TYPES child_types {child_types}"
|
||||
)
|
||||
@@ -102,7 +102,7 @@ def step_register_devcontainer_resource(context: Context) -> None:
|
||||
properties=None,
|
||||
)
|
||||
context.devcontainer_resource_id = resource.resource_id
|
||||
# Default state is DETECTED — no tracker manipulation needed
|
||||
# Default state is DISCOVERED — no tracker manipulation needed
|
||||
|
||||
|
||||
@given("a container-instance resource is registered")
|
||||
@@ -119,7 +119,7 @@ def step_register_container_resource(context: Context) -> None:
|
||||
properties=None,
|
||||
)
|
||||
context.devcontainer_resource_id = resource.resource_id
|
||||
# Default state is DETECTED — no tracker manipulation needed
|
||||
# Default state is DISCOVERED — no tracker manipulation needed
|
||||
|
||||
|
||||
@given('the devcontainer lifecycle state is "{state}"')
|
||||
|
||||
@@ -223,6 +223,17 @@ _GIT_FS_CONTAINER_TYPES: list[dict[str, Any]] = [
|
||||
),
|
||||
"default": None,
|
||||
},
|
||||
{
|
||||
"name": "clone-into",
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"description": (
|
||||
"Git repository URL to clone into the container after "
|
||||
"it starts. The repository is cloned into the container "
|
||||
"workspace using 'git clone <url>'."
|
||||
),
|
||||
"default": None,
|
||||
},
|
||||
],
|
||||
"parent_types": [
|
||||
"container-runtime",
|
||||
@@ -265,7 +276,12 @@ _GIT_FS_CONTAINER_TYPES: list[dict[str, Any]] = [
|
||||
},
|
||||
],
|
||||
"parent_types": ["git-checkout", "fs-directory"],
|
||||
"child_types": ["devcontainer-file"],
|
||||
"child_types": [
|
||||
"devcontainer-file",
|
||||
"container-mount",
|
||||
"container-exec-env",
|
||||
"container-port",
|
||||
],
|
||||
"auto_discovery": {
|
||||
"trigger_types": ["git-checkout", "fs-directory"],
|
||||
"scan_paths": [
|
||||
|
||||
@@ -167,8 +167,8 @@ def _get_lifecycle_state_str(resource: Any) -> str | None:
|
||||
try:
|
||||
tracker = get_lifecycle_tracker(resource.resource_id)
|
||||
state = tracker.current_state
|
||||
if state == ContainerLifecycleState.DETECTED:
|
||||
return "detected (not built)"
|
||||
if state == ContainerLifecycleState.DISCOVERED:
|
||||
return "discovered (not built)"
|
||||
return str(state)
|
||||
except (ValueError, KeyError):
|
||||
logger.warning(
|
||||
|
||||
@@ -7,7 +7,7 @@ validation logic for devcontainer-instance resources.
|
||||
|
||||
::
|
||||
|
||||
detected ──► building ──► running ──► stopping ──► stopped
|
||||
discovered ──► building ──► running ──► stopping ──► stopped
|
||||
│ │
|
||||
▼ │
|
||||
failed ◄───────────────────────────────┘
|
||||
@@ -19,9 +19,9 @@ validation logic for devcontainer-instance resources.
|
||||
|
||||
Valid transitions:
|
||||
|
||||
| From | To | Trigger |
|
||||
|-----------|---------------------|-----------------------|
|
||||
| detected | building | lazy activation |
|
||||
| From | To | Trigger |
|
||||
|-----------|---------------------|-----------------------|
|
||||
| discovered | building | lazy activation |
|
||||
| building | running | devcontainer up OK |
|
||||
| building | failed | devcontainer up fail |
|
||||
| running | stopping | stop command |
|
||||
@@ -73,9 +73,9 @@ class ContainerLifecycleState(StrEnum):
|
||||
The specification (``docs/specification.md``) uses the
|
||||
implementation names. Issue #514 should be updated to match.
|
||||
|
||||
| Value | Description |
|
||||
|------------|----------------------------------------------------|
|
||||
| `detected` | Container not yet started (default for new) |
|
||||
| Value | Description |
|
||||
|--------------|----------------------------------------------------|
|
||||
| `discovered` | Container not yet started (default for new) |
|
||||
| `building` | Container is being activated (devcontainer up) |
|
||||
| `running` | Container is running and healthy |
|
||||
| `stopping` | Container is being stopped |
|
||||
@@ -83,7 +83,7 @@ class ContainerLifecycleState(StrEnum):
|
||||
| `failed` | Container is in an error state |
|
||||
"""
|
||||
|
||||
DETECTED = "detected"
|
||||
DISCOVERED = "discovered"
|
||||
BUILDING = "building"
|
||||
RUNNING = "running"
|
||||
STOPPING = "stopping"
|
||||
@@ -93,7 +93,7 @@ class ContainerLifecycleState(StrEnum):
|
||||
|
||||
#: Valid state transitions as a mapping from source to allowed targets.
|
||||
VALID_TRANSITIONS: dict[ContainerLifecycleState, frozenset[ContainerLifecycleState]] = {
|
||||
ContainerLifecycleState.DETECTED: frozenset({ContainerLifecycleState.BUILDING}),
|
||||
ContainerLifecycleState.DISCOVERED: frozenset({ContainerLifecycleState.BUILDING}),
|
||||
ContainerLifecycleState.BUILDING: frozenset(
|
||||
{ContainerLifecycleState.RUNNING, ContainerLifecycleState.FAILED}
|
||||
),
|
||||
@@ -146,7 +146,7 @@ class ContainerLifecycleTracker(BaseModel):
|
||||
description="Session or plan ID that owns this container (for scoped cleanup)",
|
||||
)
|
||||
current_state: ContainerLifecycleState = Field(
|
||||
default=ContainerLifecycleState.DETECTED,
|
||||
default=ContainerLifecycleState.DISCOVERED,
|
||||
description="Current lifecycle state",
|
||||
)
|
||||
container_id: str | None = Field(
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
"""CloneIntoHandler: git clone into a running container.
|
||||
|
||||
Implements the ``--clone-into`` CLI argument for ``container-instance``
|
||||
resources. When a ``container-instance`` is registered with a
|
||||
``clone_into`` property, this handler clones the specified git
|
||||
repository URL into the container's workspace using
|
||||
``docker exec git clone``.
|
||||
|
||||
Based on:
|
||||
- Issue #7555: container-instance --clone-into CLI argument
|
||||
- ADR-043: Devcontainer Integration
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
#: Timeout in seconds for the git clone subprocess.
|
||||
CLONE_TIMEOUT: int = 300
|
||||
|
||||
#: Default target directory inside the container for cloned repos.
|
||||
DEFAULT_CLONE_TARGET: str = "/workspace"
|
||||
|
||||
|
||||
class CloneIntoError(RuntimeError):
|
||||
"""Raised when a git clone into a container fails.
|
||||
|
||||
Attributes:
|
||||
repo_url: The repository URL that was being cloned.
|
||||
container_id: The container ID that was targeted.
|
||||
stderr: The stderr output from the failed command.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
repo_url: str,
|
||||
container_id: str,
|
||||
stderr: str,
|
||||
) -> None:
|
||||
self.repo_url = repo_url
|
||||
self.container_id = container_id
|
||||
self.stderr = stderr
|
||||
super().__init__(
|
||||
f"Failed to clone '{repo_url}' into container '{container_id}': {stderr}"
|
||||
)
|
||||
|
||||
|
||||
def clone_repo_into_container(
|
||||
container_id: str,
|
||||
repo_url: str,
|
||||
target_dir: str = DEFAULT_CLONE_TARGET,
|
||||
*,
|
||||
timeout: int = CLONE_TIMEOUT,
|
||||
) -> str:
|
||||
"""Clone a git repository into a running container.
|
||||
|
||||
Executes ``docker exec <container_id> git clone <repo_url> <target_dir>``
|
||||
to clone the repository into the container's filesystem.
|
||||
|
||||
Args:
|
||||
container_id: The Docker container ID or name to clone into.
|
||||
repo_url: The git repository URL to clone.
|
||||
target_dir: The target directory inside the container.
|
||||
Defaults to ``/workspace``.
|
||||
timeout: Maximum seconds to wait for the clone to complete.
|
||||
Defaults to :data:`CLONE_TIMEOUT` (300 seconds).
|
||||
|
||||
Returns:
|
||||
The target directory path where the repository was cloned.
|
||||
|
||||
Raises:
|
||||
ValueError: If ``container_id`` or ``repo_url`` is empty.
|
||||
CloneIntoError: If the git clone command fails.
|
||||
subprocess.TimeoutExpired: If the clone exceeds ``timeout``.
|
||||
"""
|
||||
if not container_id or not container_id.strip():
|
||||
raise ValueError("container_id must be a non-empty string")
|
||||
if not repo_url or not repo_url.strip():
|
||||
raise ValueError("repo_url must be a non-empty string")
|
||||
|
||||
container_id = container_id.strip()
|
||||
repo_url = repo_url.strip()
|
||||
target_dir = target_dir.strip() if target_dir else DEFAULT_CLONE_TARGET
|
||||
|
||||
logger.info(
|
||||
"Cloning '%s' into container '%s' at '%s'",
|
||||
repo_url,
|
||||
container_id,
|
||||
target_dir,
|
||||
)
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
"docker",
|
||||
"exec",
|
||||
container_id,
|
||||
"git",
|
||||
"clone",
|
||||
repo_url,
|
||||
target_dir,
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
check=False,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
stderr = result.stderr.strip()
|
||||
logger.error(
|
||||
"git clone failed for '%s' in container '%s': %s",
|
||||
repo_url,
|
||||
container_id,
|
||||
stderr,
|
||||
)
|
||||
raise CloneIntoError(
|
||||
repo_url=repo_url,
|
||||
container_id=container_id,
|
||||
stderr=stderr,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Successfully cloned '%s' into container '%s' at '%s'",
|
||||
repo_url,
|
||||
container_id,
|
||||
target_dir,
|
||||
)
|
||||
return target_dir
|
||||
|
||||
|
||||
def validate_clone_into_url(url: str) -> bool:
|
||||
"""Validate that a URL is a plausible git repository URL.
|
||||
|
||||
Accepts:
|
||||
- HTTPS URLs: ``https://...``
|
||||
- SSH URLs: ``git@...`` or ``ssh://...``
|
||||
- Local paths: ``/...`` or ``./...``
|
||||
- Git protocol: ``git://...``
|
||||
|
||||
Args:
|
||||
url: The URL string to validate.
|
||||
|
||||
Returns:
|
||||
``True`` if the URL looks like a valid git repository URL,
|
||||
``False`` otherwise.
|
||||
"""
|
||||
if not url or not url.strip():
|
||||
return False
|
||||
url = url.strip()
|
||||
valid_prefixes = (
|
||||
"https://",
|
||||
"http://",
|
||||
"git@",
|
||||
"ssh://",
|
||||
"git://",
|
||||
"/",
|
||||
"./",
|
||||
"../",
|
||||
)
|
||||
return any(url.startswith(prefix) for prefix in valid_prefixes)
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Devcontainer resource handler for CleverAgents.
|
||||
|
||||
Resolves ``devcontainer-instance`` and ``devcontainer-file`` resources
|
||||
into sandbox-backed :class:`BoundResource` instances using the ``none``
|
||||
sandbox strategy (the container itself provides isolation — F22/F25 fix).
|
||||
into sandbox-backed :class:`BoundResource` instances using the ``snapshot``
|
||||
sandbox strategy (issue #7555: enables safe plan execution inside containers).
|
||||
|
||||
The handler:
|
||||
|
||||
@@ -137,22 +137,18 @@ class DevcontainerHandler(BaseResourceHandler):
|
||||
``devcontainer-instance`` inherits from ``container-instance``
|
||||
per ADR-042 (resource type inheritance).
|
||||
|
||||
Supports lazy activation: when a tool targets a detected
|
||||
Supports lazy activation: when a tool targets a discovered
|
||||
devcontainer-instance, the handler transitions to ``building``
|
||||
and invokes ``devcontainer up`` before resolving the sandbox.
|
||||
|
||||
F22/F25 fix: uses ``none`` sandbox strategy instead of ``snapshot``
|
||||
because the container itself provides isolation and
|
||||
``SandboxFactory`` has not yet implemented the ``snapshot``
|
||||
strategy. ``devcontainer-file`` resources also use ``none``
|
||||
(read-only config file).
|
||||
Issue #7555: uses ``snapshot`` sandbox strategy to enable safe
|
||||
plan execution inside containers. ``devcontainer-file`` resources
|
||||
use ``copy_on_write`` (read-only config file).
|
||||
"""
|
||||
|
||||
# F22/F25 fix: SNAPSHOT raises NotImplementedError in SandboxFactory.
|
||||
# The container IS the sandbox for devcontainer-instance, so NONE is
|
||||
# semantically correct until a dedicated container-snapshot strategy
|
||||
# is implemented.
|
||||
_default_strategy = SandboxStrategy.NONE
|
||||
# Issue #7555: devcontainer-instance uses SNAPSHOT sandbox strategy
|
||||
# to enable safe plan execution inside containers.
|
||||
_default_strategy = SandboxStrategy.SNAPSHOT
|
||||
_type_label = "devcontainer"
|
||||
|
||||
def content_hash(
|
||||
@@ -301,7 +297,7 @@ class DevcontainerHandler(BaseResourceHandler):
|
||||
|
||||
_ACTIVATABLE_STATES = frozenset(
|
||||
{
|
||||
ContainerLifecycleState.DETECTED,
|
||||
ContainerLifecycleState.DISCOVERED,
|
||||
ContainerLifecycleState.STOPPED,
|
||||
ContainerLifecycleState.FAILED,
|
||||
}
|
||||
@@ -654,13 +650,14 @@ class DevcontainerHandler(BaseResourceHandler):
|
||||
) -> SandboxResult:
|
||||
"""Create an isolated sandbox for a devcontainer resource.
|
||||
|
||||
For devcontainer resources the container itself provides
|
||||
isolation (``none`` sandbox strategy — F22/F25 fix). This
|
||||
method delegates to the base-class implementation which calls
|
||||
:meth:`SandboxManager.get_or_create_sandbox` with the ``none``
|
||||
strategy.
|
||||
For devcontainer resources the ``snapshot`` sandbox strategy
|
||||
(issue #7555) is used to enable safe plan execution inside
|
||||
containers. This method delegates to the base-class
|
||||
implementation which calls
|
||||
:meth:`SandboxManager.get_or_create_sandbox` with the
|
||||
``snapshot`` strategy.
|
||||
|
||||
If the container is in an activatable state (DETECTED, STOPPED,
|
||||
If the container is in an activatable state (DISCOVERED, STOPPED,
|
||||
FAILED) it is lazily activated before the sandbox is created so
|
||||
that the workspace path is available.
|
||||
|
||||
|
||||
@@ -0,0 +1,674 @@
|
||||
Test Framework: generic
|
||||
Total Tests: 155
|
||||
Passed: 107
|
||||
Failed: 48
|
||||
|
||||
--- Test Results ---
|
||||
✓ Output Block 1
|
||||
✓ Output Block 2
|
||||
✓ Output Block 3
|
||||
✓ Output Block 4
|
||||
✓ Output Block 5
|
||||
✓ Output Block 6
|
||||
✓ Output Block 7
|
||||
✓ Output Block 8
|
||||
✓ Output Block 9
|
||||
✓ Output Block 10
|
||||
✗ Output Block 11
|
||||
✓ Output Block 12
|
||||
✓ Output Block 13
|
||||
✗ Output Block 14
|
||||
✗ Output Block 15
|
||||
✗ Output Block 16
|
||||
✓ Output Block 17
|
||||
✓ Output Block 18
|
||||
✓ Output Block 19
|
||||
✓ Output Block 20
|
||||
✓ Output Block 21
|
||||
✓ Output Block 22
|
||||
✗ Output Block 23
|
||||
✗ Output Block 24
|
||||
✗ Output Block 25
|
||||
✗ Output Block 26
|
||||
✓ Output Block 27
|
||||
✗ Output Block 28
|
||||
✗ Output Block 29
|
||||
✗ Output Block 30
|
||||
✗ Output Block 31
|
||||
✗ Output Block 32
|
||||
✗ Output Block 33
|
||||
✗ Output Block 34
|
||||
✗ Output Block 35
|
||||
✓ Output Block 36
|
||||
✓ Output Block 37
|
||||
✓ Output Block 38
|
||||
✓ Output Block 39
|
||||
✓ Output Block 40
|
||||
✓ Output Block 41
|
||||
✓ Output Block 42
|
||||
✓ Output Block 43
|
||||
✓ Output Block 44
|
||||
✗ Output Block 45
|
||||
✓ Output Block 46
|
||||
✓ Output Block 47
|
||||
✗ Output Block 48
|
||||
✓ Output Block 49
|
||||
✓ Output Block 50
|
||||
✗ FAIL: Output Block 51
|
||||
✓ Output Block 52
|
||||
✗ Output Block 53
|
||||
✗ FAIL: Output Block 54
|
||||
✓ Output Block 55
|
||||
✓ Output Block 56
|
||||
✗ Output Block 57
|
||||
✗ Output Block 58
|
||||
✓ Output Block 59
|
||||
✓ Output Block 60
|
||||
✗ Output Block 61
|
||||
✗ Output Block 62
|
||||
✓ Output Block 63
|
||||
✓ Output Block 64
|
||||
✓ Output Block 65
|
||||
✓ Output Block 66
|
||||
✓ Output Block 67
|
||||
✓ Output Block 68
|
||||
✓ Output Block 69
|
||||
✓ Output Block 70
|
||||
✓ Output Block 71
|
||||
✓ Output Block 72
|
||||
✓ Output Block 73
|
||||
✓ Output Block 74
|
||||
✓ Output Block 75
|
||||
✓ Output Block 76
|
||||
✓ Output Block 77
|
||||
✓ Output Block 78
|
||||
✓ Output Block 79
|
||||
✓ Output Block 80
|
||||
✓ Output Block 81
|
||||
✓ Output Block 82
|
||||
✗ Output Block 83
|
||||
✓ Output Block 84
|
||||
✓ Output Block 85
|
||||
✗ Output Block 86
|
||||
✓ Output Block 87
|
||||
✗ Output Block 88
|
||||
✓ Output Block 89
|
||||
✓ Output Block 90
|
||||
✓ Output Block 91
|
||||
✓ Output Block 92
|
||||
✗ Output Block 93
|
||||
✗ Output Block 94
|
||||
✓ Output Block 95
|
||||
✓ Output Block 96
|
||||
✓ Output Block 97
|
||||
✓ Output Block 98
|
||||
✗ Output Block 99
|
||||
✓ Output Block 100
|
||||
✓ Output Block 101
|
||||
✓ Output Block 102
|
||||
✓ Output Block 103
|
||||
✗ Output Block 104
|
||||
✓ Output Block 105
|
||||
✓ Output Block 106
|
||||
✗ Output Block 107
|
||||
✓ Output Block 108
|
||||
✓ Output Block 109
|
||||
✗ Output Block 110
|
||||
✓ Output Block 111
|
||||
✗ Output Block 112
|
||||
✓ Output Block 113
|
||||
✗ Output Block 114
|
||||
✓ Output Block 115
|
||||
✗ Output Block 116
|
||||
✓ Output Block 117
|
||||
✓ Output Block 118
|
||||
✓ Output Block 119
|
||||
✓ Output Block 120
|
||||
✓ Output Block 121
|
||||
✓ Output Block 122
|
||||
✗ Output Block 123
|
||||
✓ Output Block 124
|
||||
✓ Output Block 125
|
||||
✓ Output Block 126
|
||||
✓ Output Block 127
|
||||
✓ Output Block 128
|
||||
✓ Output Block 129
|
||||
✓ Output Block 130
|
||||
✗ Output Block 131
|
||||
✗ Output Block 132
|
||||
✓ Output Block 133
|
||||
✓ Output Block 134
|
||||
✓ Output Block 135
|
||||
✓ Output Block 136
|
||||
✓ Output Block 137
|
||||
✓ Output Block 138
|
||||
✓ Output Block 139
|
||||
✓ Output Block 140
|
||||
✗ Output Block 141
|
||||
✗ Output Block 142
|
||||
✗ Output Block 143
|
||||
✗ Output Block 144
|
||||
✓ Output Block 145
|
||||
✓ Output Block 146
|
||||
✓ Output Block 147
|
||||
✓ Output Block 148
|
||||
✓ Output Block 149
|
||||
✓ Output Block 150
|
||||
✓ Output Block 151
|
||||
✗ Output Block 152
|
||||
✗ Output Block 153
|
||||
✗ nox > Running session unit_tests-3.13
|
||||
✗ Error Output
|
||||
|
||||
--- Failed Tests ---
|
||||
✗ Output Block 11
|
||||
1 feature passed, 0 failed, 0 skipped
|
||||
10 scenarios passed, 0 failed, 0 skipped
|
||||
23 steps passed, 0 failed, 0 skipped
|
||||
Took 0min 0.006s
|
||||
Feature: Devcontainer Lifecycle State Model
|
||||
As a CleverAgents developer
|
||||
I want the container lifecycle state model to enforce valid transitions
|
||||
So that containers follow a predictable state machine
|
||||
Scenario: ContainerLifecycleState has all required values
|
||||
When I inspect ContainerLifecycleState enum values
|
||||
Then the lifecycle enum should contain "discovered"
|
||||
And the lifecycle enum should contain "building"
|
||||
And the lifecycle enum should contain "running"
|
||||
And the lifecycle enum should contain "stopping"
|
||||
And the lifecycle enum should contain "stopped"
|
||||
And the lifecycle enum should contain "failed"
|
||||
|
||||
✗ Output Block 14
|
||||
Scenario: Invalid transition is rejected
|
||||
Given a lifecycle tracker for resource "01TESTLIFECYCLE0000000003"
|
||||
When I attempt to transition from "discovered" to "running"
|
||||
Then the transition should raise ValueError
|
||||
|
||||
✗ Output Block 15
|
||||
Scenario: Stopping to error transition is valid
|
||||
Given a lifecycle tracker in "stopping" state for resource "01TESTLIFECYCLE0000000004"
|
||||
When I transition from "stopping" to "failed"
|
||||
Then the transition should succeed
|
||||
And the tracker state should be "failed"
|
||||
|
||||
✗ Output Block 16
|
||||
Scenario: Error to starting transition is valid for retry
|
||||
Given a lifecycle tracker in "failed" state for resource "01TESTLIFECYCLE0000000005"
|
||||
When I transition from "failed" to "building"
|
||||
Then the transition should succeed
|
||||
And the tracker state should be "building"
|
||||
|
||||
✗ Output Block 23
|
||||
Scenario: validate_transition rejects wrong types
|
||||
When I call validate_transition with non-enum arguments
|
||||
Then it should raise TypeError
|
||||
|
||||
✗ Output Block 24
|
||||
Scenario: validate_transition rejects wrong to_state type
|
||||
When I call validate_transition with non-enum to_state
|
||||
Then it should raise TypeError
|
||||
|
||||
✗ Output Block 25
|
||||
Scenario: transition_state rejects wrong tracker type
|
||||
When I call transition_state with non-tracker argument
|
||||
Then it should raise TypeError
|
||||
|
||||
✗ Output Block 26
|
||||
Scenario: transition_state rejects wrong to_state type
|
||||
When I call transition_state with non-enum to_state argument
|
||||
Then it should raise TypeError
|
||||
Feature: agents resource list shows devcontainer lifecycle state column
|
||||
As a CleverAgents user
|
||||
I want the resource list to show the lifecycle state for devcontainer-instance resources
|
||||
So that I can see whether a devcontainer is discovered, running, stopped, or failed
|
||||
Feature: agents resource list shows devcontainer lifecycle state column
|
||||
|
||||
✗ Output Block 28
|
||||
Scenario: get_lifecycle_tracker rejects empty resource_id
|
||||
When I attempt to get tracker with empty resource_id
|
||||
Then the get tracker should raise ValueError
|
||||
|
||||
✗ Output Block 29
|
||||
Scenario: set_lifecycle_tracker rejects wrong type
|
||||
When I attempt to set tracker with wrong type
|
||||
Then the set tracker should raise TypeError
|
||||
|
||||
✗ Output Block 30
|
||||
Scenario: Invalid transition running to building is rejected
|
||||
Given a lifecycle tracker in "running" state for resource "01TESTLIFECYCLE0000000240"
|
||||
When I attempt to transition from "running" to "building"
|
||||
Then the transition should raise ValueError
|
||||
|
||||
✗ Output Block 31
|
||||
Scenario: Invalid transition stopped to running is rejected
|
||||
Given a lifecycle tracker in "stopped" state for resource "01TESTLIFECYCLE0000000241"
|
||||
When I attempt to transition from "stopped" to "running"
|
||||
Then the transition should raise ValueError
|
||||
|
||||
✗ Output Block 32
|
||||
Scenario: Invalid transition building to stopped is rejected
|
||||
Given a lifecycle tracker in "building" state for resource "01TESTLIFECYCLE0000000242"
|
||||
When I attempt to transition from "building" to "stopped"
|
||||
Then the transition should raise ValueError
|
||||
|
||||
✗ Output Block 33
|
||||
Scenario: Invalid transition failed to running is rejected
|
||||
Given a lifecycle tracker in "failed" state for resource "01TESTLIFECYCLE0000000243"
|
||||
When I attempt to transition from "failed" to "running"
|
||||
Then the transition should raise ValueError
|
||||
|
||||
✗ Output Block 34
|
||||
Scenario: Invalid transition discovered to stopping is rejected
|
||||
Given a lifecycle tracker for resource "01TESTLIFECYCLE0000000244"
|
||||
When I attempt to transition from "discovered" to "stopping"
|
||||
Then the transition should raise ValueError
|
||||
|
||||
✗ Output Block 35
|
||||
Scenario: Invalid transition building to discovered is rejected
|
||||
Given a lifecycle tracker in "building" state for resource "01TESTLIFECYCLE0000000245"
|
||||
When I attempt to transition from "building" to "discovered"
|
||||
Then the transition should raise ValueError
|
||||
|
||||
✗ Output Block 45
|
||||
1 feature passed, 0 failed, 0 skipped
|
||||
32 scenarios passed, 0 failed, 0 skipped
|
||||
106 steps passed, 0 failed, 0 skipped
|
||||
Took 0min 0.037s
|
||||
And built-in types are bootstrapped
|
||||
Given a devcontainer-instance resource is registered
|
||||
When I run resource list with all flag
|
||||
Then the resource output should contain "discovered (not built)"
|
||||
|
||||
✗ Output Block 48
|
||||
Scenario: resource list shows "failed" state for a failed devcontainer
|
||||
Given a fresh in-memory resource registry
|
||||
And built-in types are bootstrapped
|
||||
Given a devcontainer-instance resource is registered
|
||||
And the devcontainer lifecycle state is "failed"
|
||||
When I run resource list with all flag
|
||||
Then the resource output should contain "failed"
|
||||
|
||||
✗ FAIL: Output Block 51
|
||||
ASSERT FAILED: Expected 'Devcontainer detected' in output, got:
|
||||
Resources (1 total)
|
||||
┏━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ ID ┃ Name ┃ Type ┃ Status ┃ Kind ┃ Location ┃ Description ┃
|
||||
┡━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩
|
||||
│ 01KP2TDV4GAR... │ (unnamed) │ devcontainer-instance │ discovered (not built) │ physical │ /tmp/test-project │ Test devcontainer │
|
||||
└─────────────────┴───────────┴───────────────────────┴────────────────────────┴──────────┴───────────────────┴───────────────────┘
|
||||
|
||||
✗ Output Block 53
|
||||
@tdd_issue @tdd_issue_4263 @tdd_expected_fail
|
||||
Scenario: resource list JSON output includes lifecycle_state for devcontainer-instance
|
||||
Given a fresh in-memory resource registry
|
||||
And built-in types are bootstrapped
|
||||
Given a devcontainer-instance resource is registered
|
||||
When I run resource list with all flag and format "json"
|
||||
Then the resource output should be valid JSON
|
||||
And the resource JSON output should contain key "lifecycle_state"
|
||||
|
||||
✗ FAIL: Output Block 54
|
||||
ASSERT FAILED: Key 'lifecycle_state' not found in JSON output
|
||||
|
||||
✗ Output Block 57
|
||||
Scenario: resource list succeeds gracefully when lifecycle tracker raises an error
|
||||
Given a fresh in-memory resource registry
|
||||
And built-in types are bootstrapped
|
||||
Feature: Devcontainer Resource Handler
|
||||
As a CleverAgents developer
|
||||
I want devcontainer resources to be auto-discovered and manually registered
|
||||
So that devcontainer environments integrate into the resource model Given a devcontainer-instance resource is registered
|
||||
|
||||
✗ Output Block 58
|
||||
Scenario: Register devcontainer-instance manually
|
||||
Given a temporary directory with a valid devcontainer.json
|
||||
When I create a devcontainer-instance resource at that path
|
||||
Then And the resource should have type "the lifecycle tracker raises an error for the resourcedevcontainer-instance"
|
||||
|
||||
✗ Output Block 61
|
||||
|
||||
Failing scenarios:
|
||||
features/resource_list_lifecycle_state.feature:44 resource list shows warning banner when devcontainer is in detected state
|
||||
|
||||
✗ Output Block 62
|
||||
0 features passed, 1 failed, 0 skipped
|
||||
11 scenarios passed, 1 failed, 0 skipped
|
||||
69 steps passed, 1 failed, 0 skipped
|
||||
Took 0min 1.827s
|
||||
|
||||
✗ Output Block 83
|
||||
Scenario Outline: validate_clone_into_url rejects invalid URLs -- @1.1
|
||||
When I validate clone-into URL "not-a-url"
|
||||
Then it should raise a ValueError
|
||||
Then the clone-into URL should be invalid
|
||||
|
||||
✗ Output Block 86
|
||||
Scenario: clone_repo_into_container rejects empty container_id
|
||||
When I call clone_repo_into_container with empty container_id
|
||||
Then it should raise ValueError mentioning "container_id"
|
||||
|
||||
✗ Output Block 88
|
||||
Scenario: clone_repo_into_container rejects empty repo_url
|
||||
When I call clone_repo_into_container with empty repo_url
|
||||
And the directory has a named devcontainer configuration "frontend"
|
||||
Then it should raise ValueError mentioning "repo_url"
|
||||
When I run devcontainer discovery on the directory as "git-checkout"
|
||||
Then discovery should return 2 results
|
||||
And the result config names should include "api"
|
||||
|
||||
✗ Output Block 93
|
||||
Scenario: clone_repo_into_container raises CloneIntoError when docker exec fails
|
||||
Given a mock docker exec that fails for clone with stderr "fatal: repository not found"
|
||||
When I clone "https://github.com/org/missing.git" into container "abc123def456"
|
||||
When I run devcontainer discovery on the directory as "git-checkout"
|
||||
Then the clone should raise CloneIntoError
|
||||
And the CloneIntoError should mention "repository not found"
|
||||
Then discovery should return 1 result
|
||||
And the first result should have no config name
|
||||
|
||||
✗ Output Block 94
|
||||
Scenario: CloneIntoError stores repo_url, container_id, and stderr
|
||||
Given a CloneIntoError with repo "https://example.com/repo.git" container "ctr-001" stderr "auth failed"
|
||||
Then the CloneIntoError repo_url should be "https://example.com/repo.git"
|
||||
And the CloneIntoError container_id should be "ctr-001"
|
||||
And the CloneIntoError stderr should be "auth failed"
|
||||
|
||||
✗ Output Block 99
|
||||
1 feature passed, 0 failed, 0 skipped
|
||||
17 scenarios passed, 0 failed, 0 skipped
|
||||
44 steps passed, 0 failed, 0 skipped
|
||||
Took 0min 0.008s
|
||||
Then discovery should return 0 results
|
||||
|
||||
✗ Output Block 104
|
||||
1 feature passed, 0 failed, 0 skipped
|
||||
26 scenarios passed, 0 failed, 0 skipped
|
||||
90 steps passed, 0 failed, 0 skipped
|
||||
Took 0min 0.026s
|
||||
Feature: Devcontainer Session Cleanup and CLI Commands
|
||||
As a CleverAgents developer
|
||||
I want session-scoped container cleanup and CLI stop/rebuild commands
|
||||
So that containers are cleaned up when sessions end and can be managed via CLI
|
||||
Scenario: Active containers stopped on session cleanup
|
||||
Given a mock devcontainer CLI runner
|
||||
And an active container "01TESTLIFECYCLE0000000040" with container ID "ctr-a"
|
||||
And an active container "01TESTLIFECYCLE0000000041" with container ID "ctr-b"
|
||||
When I run session cleanup for session "session-001"
|
||||
Then the container state for "01TESTLIFECYCLE0000000040" should be "stopped"
|
||||
And the container state for "01TESTLIFECYCLE0000000041" should be "stopped"
|
||||
|
||||
✗ Output Block 107
|
||||
Scenario: Session cleanup handles stop failure gracefully
|
||||
Given a mock devcontainer CLI runner
|
||||
And the runner configured to raise exception on stop
|
||||
And an active container "01TESTLIFECYCLE0000000130" with container ID "ctr-fail"
|
||||
When I run session cleanup for session "session-err"
|
||||
Then the cleanup should return 0 stopped containers
|
||||
|
||||
✗ Output Block 110
|
||||
Scenario: dcproto delete fails when container is stopped
|
||||
Given dcproto a devcontainer handler
|
||||
And dcproto a devcontainer-instance resource with location "/ws/project"
|
||||
And dcproto subprocess returns failed rm output with stderr "container not running"
|
||||
When dcproto I delete path "file.txt" from the resource
|
||||
Then the CLI exit code should be 0
|
||||
And the CLI output should contain "Stopped"
|
||||
Then dcproto the delete should fail
|
||||
And dcproto the delete failure message should contain "container not running"
|
||||
|
||||
✗ Output Block 112
|
||||
Scenario: dcproto delete from resource with no location raises ValueError
|
||||
Given dcproto a devcontainer handler
|
||||
And dcproto a devcontainer-instance resource with no location
|
||||
When dcproto I delete path "any/file.txt" from the resource
|
||||
Then dcproto the delete should raise ValueError containing "no location"
|
||||
|
||||
✗ Output Block 114
|
||||
Scenario: dcproto list_children returns empty list when container is stopped
|
||||
Given dcproto a devcontainer handler
|
||||
And dcproto a devcontainer-instance resource with location "/ws/project"
|
||||
And dcproto subprocess returns failed ls output
|
||||
When dcproto I list children of the resource
|
||||
Then dcproto the children list should have 0 entries
|
||||
|
||||
✗ Output Block 116
|
||||
Scenario: dcproto list_children from resource with no location raises ValueError
|
||||
Given dcproto a devcontainer handler
|
||||
And dcproto a devcontainer-instance resource with no location
|
||||
When dcproto I list children of the resource
|
||||
Then dcproto the list children should raise ValueError containing "no location"
|
||||
|
||||
✗ Output Block 123
|
||||
Scenario: dcproto diff from resource with no location raises ValueError
|
||||
Given dcproto a devcontainer handler
|
||||
And dcproto a devcontainer-instance resource with no location
|
||||
When dcproto I diff the resource against "/some/path"
|
||||
Then the devcontainer CLI exit code should be non-zero
|
||||
Then dcproto the diff should raise ValueError containing "no location"
|
||||
And the CLI output should contain "no location"
|
||||
|
||||
✗ Output Block 131
|
||||
Scenario: dcproto create_sandbox raises ValueError for resource with no location
|
||||
Given dcproto a devcontainer handler
|
||||
And dcproto a devcontainer-instance resource with no location
|
||||
And dcproto a mock sandbox manager that returns a valid sandbox
|
||||
When dcproto I create a sandbox for the resource with plan "plan-004"
|
||||
Then dcproto the create_sandbox should raise ValueError containing "no location"
|
||||
|
||||
✗ Output Block 132
|
||||
1 feature passed, 0 failed, 0 skipped
|
||||
18 scenarios passed, 0 failed, 0 skipped
|
||||
102 steps passed, 0 failed, 0 skipped
|
||||
Took 0min 0.028s
|
||||
Then the CLI exit code should be 0
|
||||
And the CLI rebuild mock should have been called with "01TESTLIFECYCLE0000000221" and "/workspace/project"
|
||||
|
||||
✗ Output Block 141
|
||||
Scenario: CLI stop handles NotFoundError from show_resource
|
||||
Given a mock resource service that raises NotFoundError for "local/missing"
|
||||
When I invoke CLI resource stop "local/missing"
|
||||
Then the devcontainer CLI exit code should be non-zero
|
||||
And the CLI output should contain "Resource not found"
|
||||
|
||||
✗ Output Block 142
|
||||
Scenario: CLI rebuild handles NotFoundError from show_resource
|
||||
Given a mock resource service that raises NotFoundError for "local/missing-rb"
|
||||
When I invoke CLI resource rebuild "local/missing-rb"
|
||||
Then the devcontainer CLI exit code should be non-zero
|
||||
And the CLI output should contain "Resource not found"
|
||||
|
||||
✗ Output Block 143
|
||||
Scenario: CLI stop handles RuntimeError from stop_container
|
||||
Given a mock resource service with devcontainer "local/dc-rterr" id "01TESTLIFECYCLE0000000500" at "/workspace/project"
|
||||
And an active container "01TESTLIFECYCLE0000000500" with container ID "ctr-rterr"
|
||||
And CLI stop_container mock that raises RuntimeError
|
||||
When I invoke CLI resource stop with error mock "local/dc-rterr"
|
||||
Then the devcontainer CLI exit code should be non-zero
|
||||
And the CLI output should contain "Stop failed"
|
||||
|
||||
✗ Output Block 144
|
||||
Scenario: CLI rebuild handles RuntimeError from rebuild_container
|
||||
Given a mock resource service with devcontainer "local/dc-rberr" id "01TESTLIFECYCLE0000000501" at "/workspace/project"
|
||||
And a stopped container "01TESTLIFECYCLE0000000501"
|
||||
And CLI rebuild_container mock that raises RuntimeError
|
||||
When I invoke CLI resource rebuild with error mock "local/dc-rberr"
|
||||
Then the devcontainer CLI exit code should be non-zero
|
||||
And the CLI output should contain "Rebuild failed"
|
||||
|
||||
✗ Output Block 152
|
||||
1 feature passed, 0 failed, 0 skipped
|
||||
30 scenarios passed, 0 failed, 0 skipped
|
||||
143 steps passed, 0 failed, 0 skipped
|
||||
Took 0min 0.436s
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.created child_domain=uko-oo: parent_domain=uko-code:
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.insert_after after_level=MEMBER_LISTING domain=uko-oo: new_level=CLASS_HIERARCHY
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.insert_after after_level=SIGNATURES_WITH_DOCS domain=uko-oo: new_level=VISIBILITY_ANNOTATED
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.built domain=uko-oo: max_depth=11 num_levels=12
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.created child_domain=uko-oo: parent_domain=uko-code:
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.insert_after after_level=MEMBER_LISTING domain=uko-oo: new_level=CLASS_HIERARCHY
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.insert_after after_level=SIGNATURES_WITH_DOCS domain=uko-oo: new_level=VISIBILITY_ANNOTATED
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.built domain=uko-oo: max_depth=11 num_levels=12
|
||||
2026-04-13 07:00:42 [debug ] detail_level_map_builder.created child_domain=uko-oo: parent_domain=uko-code:
|
||||
2026-04-13 07:00:42 [debug ] detail_level_map_builder.insert_after after_level=MEMBER_LISTING domain=uko-oo: new_level=CLASS_HIERARCHY
|
||||
2026-04-13 07:00:42 [debug ] detail_level_map_builder.insert_after after_level=SIGNATURES_WITH_DOCS domain=uko-oo: new_level=VISIBILITY_ANNOTATED
|
||||
2026-04-13 07:00:42 [debug ] detail_level_map_builder.built domain=uko-oo: max_depth=11 num_levels=12
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.created child_domain=uko-oo: parent_domain=uko-code:
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.insert_after after_level=MEMBER_LISTING domain=uko-oo: new_level=CLASS_HIERARCHY
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.insert_after after_level=SIGNATURES_WITH_DOCS domain=uko-oo: new_level=VISIBILITY_ANNOTATED
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.built domain=uko-oo: max_depth=11 num_levels=12
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.created child_domain=uko-oo: parent_domain=uko-code:
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.insert_after after_level=MEMBER_LISTING domain=uko-oo: new_level=CLASS_HIERARCHY
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.insert_after after_level=SIGNATURES_WITH_DOCS domain=uko-oo: new_level=VISIBILITY_ANNOTATED
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.built domain=uko-oo: max_depth=11 num_levels=12
|
||||
[1mIncluding unscoped container 01TESTLIFECYCLE0000000040 in session 'session-001' cleanup (container has no session_id set)[0m
|
||||
[1mIncluding unscoped container 01TESTLIFECYCLE0000000041 in session 'session-001' cleanup (container has no session_id set)[0m
|
||||
[1mContainer 01TESTLIFECYCLE0000000040: running -> stopping (manual stop)[0m
|
||||
[1mContainer 01TESTLIFECYCLE0000000040: stopping -> stopped (container stopped)[0m
|
||||
[1mStopped container 01TESTLIFECYCLE0000000040 (session=session-001)[0m
|
||||
[1mContainer 01TESTLIFECYCLE0000000041: running -> stopping (manual stop)[0m
|
||||
[1mContainer 01TESTLIFECYCLE0000000041: stopping -> stopped (container stopped)[0m
|
||||
[1mStopped container 01TESTLIFECYCLE0000000041 (session=session-001)[0m
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.created child_domain=uko-oo: parent_domain=uko-code:
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.insert_after after_level=MEMBER_LISTING domain=uko-oo: new_level=CLASS_HIERARCHY
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.insert_after after_level=SIGNATURES_WITH_DOCS domain=uko-oo: new_level=VISIBILITY_ANNOTATED
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.built domain=uko-oo: max_depth=11 num_levels=12
|
||||
[1mRegistered built-in resource type: fs-mount[0m
|
||||
[1mRegistered built-in resource type: git-checkout[0m
|
||||
[1mRegistered built-in resource type: fs-directory[0m
|
||||
[1mRegistered built-in resource type: container-instance[0m
|
||||
[1mRegistered built-in resource type: devcontainer-instance[0m
|
||||
[1mRegistered built-in resource type: devcontainer-file[0m
|
||||
[1mRegistered built-in resource type: git[0m
|
||||
[1mRegistered built-in resource type: git-remote[0m
|
||||
[1mRegistered built-in resource type: git-branch[0m
|
||||
[1mRegistered built-in resource type: git-tag[0m
|
||||
[1mRegistered built-in resource type: git-commit[0m
|
||||
[1mRegistered built-in resource type: git-tree[0m
|
||||
[1mRegistered built-in resource type: git-tree-entry[0m
|
||||
[1mRegistered built-in resource type: git-stash[0m
|
||||
[1mRegistered built-in resource type: git-submodule[0m
|
||||
[1mRegistered built-in resource type: fs-symlink[0m
|
||||
[1mRegistered built-in resource type: fs-hardlink[0m
|
||||
[1mRegistered built-in resource type: cloud-account[0m
|
||||
[1mRegistered built-in resource type: cloud-region[0m
|
||||
[1mRegistered built-in resource type: cloud-network[0m
|
||||
[1mRegistered built-in resource type: cloud-subnet[0m
|
||||
[1mRegistered built-in resource type: cloud-security-group[0m
|
||||
[1mRegistered built-in resource type: cloud-load-balancer[0m
|
||||
[1mRegistered built-in resource type: cloud-compute-instance[0m
|
||||
[1mRegistered built-in resource type: cloud-object-store[0m
|
||||
[1mRegistered built-in resource type: cloud-block-storage[0m
|
||||
[1mRegistered built-in resource type: cloud-identity-principal[0m
|
||||
[1mRegistered built-in resource type: cloud-role[0m
|
||||
[1mRegistered built-in resource type: cloud-policy[0m
|
||||
[1mRegistered built-in resource type: cloud-log-group[0m
|
||||
[1mRegistered built-in resource type: cloud-alarm[0m
|
||||
[1mRegistered built-in resource type: cloud-queue[0m
|
||||
[1mRegistered built-in resource type: cloud-topic[0m
|
||||
[1mRegistered built-in resource type: cloud-container-repo[0m
|
||||
[1mRegistered built-in resource type: cloud-container-cluster[0m
|
||||
[1mRegistered built-in resource type: cloud-container-service[0m
|
||||
[1mRegistered built-in resource type: aws-account[0m
|
||||
[1mRegistered built-in resource type: aws-region[0m
|
||||
[1mRegistered built-in resource type: aws-vpc[0m
|
||||
[1mRegistered built-in resource type: aws-subnet[0m
|
||||
[1mRegistered built-in resource type: aws-igw[0m
|
||||
[1mRegistered built-in resource type: aws-nat-gw[0m
|
||||
[1mRegistered built-in resource type: aws-route-table[0m
|
||||
[1mRegistered built-in resource type: aws-nacl[0m
|
||||
[1mRegistered built-in resource type: aws-security-group[0m
|
||||
[1mRegistered built-in resource type: aws-alb[0m
|
||||
[1mRegistered built-in resource type: aws-nlb[0m
|
||||
[1mRegistered built-in resource type: aws-target-group[0m
|
||||
[1mRegistered built-in resource type: aws-listener[0m
|
||||
[1mRegistered built-in resource type: aws-ec2-instance[0m
|
||||
[1mRegistered built-in resource type: aws-ami[0m
|
||||
[1mRegistered built-in resource type: aws-launch-template[0m
|
||||
[1mRegistered built-in resource type: aws-asg[0m
|
||||
[1mRegistered built-in resource type: aws-s3-bucket[0m
|
||||
[1mRegistered built-in resource type: aws-ebs-volume[0m
|
||||
[1mRegistered built-in resource type: aws-efs-filesystem[0m
|
||||
[1mRegistered built-in resource type: aws-iam-user[0m
|
||||
[1mRegistered built-in resource type: aws-iam-role[0m
|
||||
[1mRegistered built-in resource type: aws-iam-policy[0m
|
||||
[1mRegistered built-in resource type: aws-iam-instance-profile[0m
|
||||
[1mRegistered built-in resource type: aws-cloudwatch-log-group[0m
|
||||
[1mRegistered built-in resource type: aws-cloudwatch-alarm[0m
|
||||
[1mRegistered built-in resource type: aws-cloudwatch-metric[0m
|
||||
[1mRegistered built-in resource type: aws-eventbridge-bus[0m
|
||||
[1mRegistered built-in resource type: aws-eventbridge-rule[0m
|
||||
[1mRegistered built-in resource type: aws-eventbridge-target[0m
|
||||
[1mRegistered built-in resource type: aws-sqs-queue[0m
|
||||
[1mRegistered built-in resource type: aws-sns-topic[0m
|
||||
[1mRegistered built-in resource type: aws-sns-subscription[0m
|
||||
[1mRegistered built-in resource type: aws-ecr-repo[0m
|
||||
[1mRegistered built-in resource type: aws-ecs-cluster[0m
|
||||
[1mRegistered built-in resource type: aws-ecs-service[0m
|
||||
[1mRegistered built-in resource type: aws-ecs-task-def[0m
|
||||
[1mRegistered built-in resource type: aws-eks-cluster[0m
|
||||
[1mRegistered built-in resource type: aws-eks-nodegroup[0m
|
||||
[1mRegistered built-in resource type: gcp[0m
|
||||
[1mRegistered built-in resource type: azure[0m
|
||||
[1mRegistered built-in resource type: container-runtime[0m
|
||||
[1mRegistered built-in resource type: container-image[0m
|
||||
[1mRegistered built-in resource type: container-mount[0m
|
||||
[1mRegistered built-in resource type: container-exec-env[0m
|
||||
[1mRegistered built-in resource type: container-port[0m
|
||||
[1mRegistered built-in resource type: container-volume[0m
|
||||
[1mRegistered built-in resource type: container-network[0m
|
||||
[1mRegistered built-in resource type: postgres[0m
|
||||
[1mRegistered built-in resource type: mysql[0m
|
||||
[1mRegistered built-in resource type: sqlite[0m
|
||||
[1mRegistered built-in resource type: duckdb[0m
|
||||
[1mRegistered built-in resource type: file[0m
|
||||
[1mRegistered built-in resource type: directory[0m
|
||||
[1mRegistered built-in resource type: commit[0m
|
||||
[1mRegistered built-in resource type: branch[0m
|
||||
[1mRegistered built-in resource type: tag[0m
|
||||
[1mRegistered built-in resource type: tree[0m
|
||||
[1mRegistered built-in resource type: remote[0m
|
||||
[1mRegistered built-in resource type: submodule[0m
|
||||
[1mRegistered built-in resource type: symlink[0m
|
||||
[1mRegistered built-in resource type: executable[0m
|
||||
[1mRegistered built-in resource type: lsp-server[0m
|
||||
[1mRegistered built-in resource type: lsp-workspace[0m
|
||||
[1mRegistered built-in resource type: lsp-document[0m
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.created child_domain=uko-oo: parent_domain=uko-code:
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.insert_after after_level=MEMBER_LISTING domain=uko-oo: new_level=CLASS_HIERARCHY
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.insert_after after_level=SIGNATURES_WITH_DOCS domain=uko-oo: new_level=VISIBILITY_ANNOTATED
|
||||
2026-04-13 07:00:43 [debug ] detail_level_map_builder.built domain=uko-oo: max_depth=11 num_levels=12
|
||||
|
||||
✗ Output Block 153
|
||||
Overall summary:
|
||||
6 features passed, 1 failed, 0 errored, 0 skipped
|
||||
144 scenarios passed, 1 failed, 0 errored, 0 skipped
|
||||
553 steps passed, 1 failed, 0 errored, 0 skipped
|
||||
Took 2.366s
|
||||
Wall time: 2m 47.786s
|
||||
|
||||
✗ nox > Running session unit_tests-3.13
|
||||
nox > Running session unit_tests-3.13
|
||||
nox > Reusing existing virtual environment at .nox/unit_tests-3-13.
|
||||
nox > uv pip install -e '.[tests]'
|
||||
nox > uv pip install setuptools wheel
|
||||
nox > uv pip install .nox/unit_tests-3-13/tmp/behave-parallel-inprocess
|
||||
nox > python scripts/create_template_db.py /tmp/impl-worker-7555-20260413/build/.template-migrated.db
|
||||
nox > python -m compileall -q features/
|
||||
nox > /tmp/impl-worker-7555-20260413/.nox/unit_tests-3-13/bin/behave-parallel -q --processes 32 features/container_clone_into.feature features/devcontainer_sandbox_strategy.feature features/devcontainer_state.feature features/devcontainer_handler.feature features/devcontainer_cleanup.feature features/resource_list_lifecycle_state.feature features/devcontainer_handler_protocol_methods.feature
|
||||
nox > Command /tmp/impl-worker-7555-20260413/.nox/unit_tests-3-13/bin/behave-parallel -q --processes 32 features/container_clone_into.feature features/devcontainer_sandbox_strategy.feature features/devcontainer_state.feature features/devcontainer_handler.feature features/devcontainer_cleanup.feature features/resource_list_lifecycle_state.feature features/devcontainer_handler_protocol_methods.feature failed with exit code 1
|
||||
nox > Session unit_tests-3.13 failed.
|
||||
|
||||
✗ Error Output
|
||||
nox > Running session unit_tests-3.13
|
||||
nox > Reusing existing virtual environment at .nox/unit_tests-3-13.
|
||||
nox > uv pip install -e '.[tests]'
|
||||
nox > uv pip install setuptools wheel
|
||||
nox > uv pip install .nox/unit_tests-3-13/tmp/behave-parallel-inprocess
|
||||
nox > python scripts/create_template_db.py /tmp/impl-worker-7555-20260413/build/.template-migrated.db
|
||||
nox > python -m compileall -q features/
|
||||
nox > /tmp/impl-worker-7555-20260413/.nox/unit_tests-3-13/bin/behave-parallel -q --processes 32 features/container_clone_into.feature features/devcontainer_sandbox_strategy.feature features/devcontainer_state.feature features/devcontainer_handler.feature features/devcontainer_cleanup.feature features/resource_list_lifecycle_state.feature features/devcontainer_handler_protocol_methods.feature
|
||||
nox > Command /tmp/impl-worker-7555-20260413/.nox/unit_tests-3-13/bin/behave-parallel -q --processes 32 features/container_clone_into.feature features/devcontainer_sandbox_strategy.feature features/devcontainer_state.feature features/devcontainer_handler.feature features/devcontainer_cleanup.feature features/resource_list_lifecycle_state.feature features/devcontainer_handler_protocol_methods.feature failed with exit code 1
|
||||
nox > Session unit_tests-3.13 failed.
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user