Files
cleveragents-core/features/container_lifecycle.feature
freemo 9c4655fd5c feat(server): container/devcontainer support lifecycle
Implement server-side container and devcontainer lifecycle management
for the CleverAgents server infrastructure.

New modules in src/cleveragents/infrastructure/server/containers/:
- DevcontainerSpec: Pydantic model for devcontainer.json parsing and
  validation (image, build, features, mounts, env vars, ports,
  hostRequirements, postCreateCommand, remoteUser, workspaceFolder).
- ResourceLimits: CPU, memory, storage, and PID limits model with
  Docker CLI argument generation and hostRequirements conversion.
- ContainerManager: Server-side lifecycle orchestration with
  create/start/stop/destroy operations using pluggable Docker CLI
  runner (mocked for tests).
- HealthMonitor: Periodic container health probing via docker
  inspect with background thread management and probe history.
- ContainerStatus enum: pending/created/running/stopped/destroyed/error
  with validated state transitions.

Tests:
- Behave BDD: 67 scenarios covering spec parsing, resource limits,
  lifecycle operations, health monitoring, and error handling
  (features/container_lifecycle.feature).
- Robot Framework: 13 integration test cases with mock Docker
  runner (robot/container_lifecycle_server.robot).

ISSUES CLOSED: #865
2026-03-23 22:50:51 +00:00

402 lines
18 KiB
Gherkin

Feature: Container and Devcontainer Lifecycle Management
As a CleverAgents server operator
I want to manage container lifecycles (create/start/stop/destroy)
So that development containers are reliably orchestrated
# DevcontainerSpec parsing
Scenario: Parse devcontainer.json with image
Given a devcontainer.json string with image "mcr.microsoft.com/devcontainers/python:3.13"
When I parse the devcontainer spec string
Then the spec image should be "mcr.microsoft.com/devcontainers/python:3.13"
And the spec should have image set
And the spec should not have build set
Scenario: Parse devcontainer.json with build config
Given a devcontainer.json string with build dockerfile "Dockerfile.dev"
When I parse the devcontainer spec string
Then the spec should have build set
And the build dockerfile should be "Dockerfile.dev"
And the spec should not have image set
Scenario: Parse devcontainer.json with features
Given a devcontainer.json string with features
When I parse the devcontainer spec string
Then the spec should have features
And the feature "ghcr.io/devcontainers/features/python:1" should be present
Scenario: Parse devcontainer.json with mounts
Given a devcontainer.json string with mounts
When I parse the devcontainer spec string
Then the spec should have 1 mount
And mount 0 source should be "/host/data"
And mount 0 target should be "/container/data"
Scenario: Parse devcontainer.json with environment variables
Given a devcontainer.json string with container environment variables
When I parse the devcontainer spec string
Then the container env should contain "MY_VAR" with value "hello"
Scenario: Parse devcontainer.json with forwarded ports
Given a devcontainer.json string with forwarded ports 8080 and 3000
When I parse the devcontainer spec string
Then the forwarded ports should contain 8080
And the forwarded ports should contain 3000
Scenario: Parse devcontainer.json with host requirements
Given a devcontainer.json string with host requirements cpus 4 and memory "8gb"
When I parse the devcontainer spec string
Then the resource limits cpu should be 4.0
And the resource limits memory should be 8192
Scenario: Parse devcontainer.json from file
Given a temporary devcontainer.json file with image "ubuntu:22.04"
When I parse the devcontainer spec file
Then the spec image should be "ubuntu:22.04"
Scenario: Parse devcontainer.json rejects empty path
When I attempt to parse devcontainer spec from empty path
Then a container ValueError should be raised with "path must not be empty"
Scenario: Parse devcontainer.json rejects relative path
When I attempt to parse devcontainer spec from relative path "devcontainer.json"
Then a container ValueError should be raised with "path must be an absolute path"
Scenario: Parse devcontainer.json rejects missing file
When I attempt to parse devcontainer spec from missing file "/nonexistent/devcontainer.json"
Then a container FileNotFoundError should be raised
Scenario: Parse devcontainer.json string rejects empty content
When I attempt to parse devcontainer spec from empty string
Then a container ValueError should be raised with "content must not be empty"
Scenario: Parse mount string in Docker format
Given a devcontainer.json string with string mount "type=bind,source=/src,target=/dst"
When I parse the devcontainer spec string
Then the spec should have 1 mount
And mount 0 source should be "/src"
And mount 0 target should be "/dst"
Scenario: Parse mount string in shorthand format
Given a devcontainer.json string with string mount "/host/path:/container/path"
When I parse the devcontainer spec string
Then the spec should have 1 mount
And mount 0 source should be "/host/path"
And mount 0 target should be "/container/path"
Scenario: Spec with post-create command
Given a devcontainer.json string with postCreateCommand "pip install -e ."
When I parse the devcontainer spec string
Then the post create command should be "pip install -e ."
Scenario: Spec with remote user
Given a devcontainer.json string with remoteUser "vscode"
When I parse the devcontainer spec string
Then the remote user should be "vscode"
Scenario: Spec with workspace folder
Given a devcontainer.json string with workspaceFolder "/workspaces/myproject"
When I parse the devcontainer spec string
Then the workspace folder should be "/workspaces/myproject"
# ── ResourceLimits ───────────────────────────────────────
Scenario: Default resource limits
When I create default resource limits
Then the cpu limit should be 2.0
And the memory limit should be 2048
Scenario: Custom resource limits
When I create resource limits with cpu 4.0 and memory 4096
Then the cpu limit should be 4.0
And the memory limit should be 4096
Scenario: Resource limits from host requirements
Given host requirements with cpus 8 and memory "16gb"
When I create resource limits from host requirements
Then the cpu limit should be 8.0
And the memory limit should be 16384
Scenario: Resource limits to docker args
When I create resource limits with cpu 2.0 and memory 1024
Then the docker args should contain "--cpus"
And the docker args should contain "--memory"
Scenario: Resource limits rejects invalid cpu
When I attempt to create resource limits with cpu 0.0
Then a container validation error should be raised
Scenario: Resource limits rejects invalid memory
When I attempt to create resource limits with memory 0
Then a container validation error should be raised
Scenario: Parse memory string gb
When I parse memory string "4gb"
Then the parsed memory should be 4096
Scenario: Parse memory string mb
When I parse memory string "512mb"
Then the parsed memory should be 512
Scenario: Parse memory string plain integer
When I parse memory string "2048"
Then the parsed memory should be 2048
Scenario: Parse memory string rejects empty
When I attempt to parse empty memory string
Then a container ValueError should be raised with "memory string must not be empty"
Scenario: Resource limits from host requirements rejects non-dict
When I attempt to create resource limits from non-dict host requirements
Then a container TypeError should be raised
Scenario: Resource limits with pids limit
When I create resource limits with pids limit 500
Then the docker args should contain "--pids-limit"
Scenario: Resource limits coerces string values
When I create resource limits from string values cpu "4.0" and memory "2048"
Then the cpu limit should be 4.0
And the memory limit should be 2048
# ── ContainerManager lifecycle ────────────────────────────
Scenario: Create container with mock runner
Given a mock container command runner
And the container runner configured for successful creation
When I create container "test-container" with workspace "/workspace"
Then the container "test-container" should have status "created"
And the container "test-container" should have a container ID
Scenario: Start a created container
Given a mock container command runner
And a created container "start-test" with container ID "abc123"
When I start container "start-test"
Then the container "start-test" should have status "running"
Scenario: Stop a running container
Given a mock container command runner
And a running container "stop-test" with container ID "def456"
When I stop managed container "stop-test"
Then the container "stop-test" should have status "stopped"
Scenario: Destroy a stopped container
Given a mock container command runner
And a stopped managed container "destroy-test" with container ID "ghi789"
When I destroy container "destroy-test"
Then the container "destroy-test" should have status "destroyed"
Scenario: Create container rejects empty name
Given a mock container command runner
When I attempt to create container with empty name
Then a container ValueError should be raised with "name must not be empty"
Scenario: Create container rejects duplicate name
Given a mock container command runner
And the container runner configured for successful creation
And an existing container "duplicate" in the manager
When I attempt to create duplicate container "duplicate"
Then a container ValueError should be raised with "already exists"
Scenario: Start non-existent container raises ValueError
Given a mock container command runner
When I attempt to start non-existent container "ghost"
Then a container ValueError should be raised with "not found"
Scenario: Stop non-running container raises ValueError
Given a mock container command runner
And a created container "not-running" with container ID "xyz"
When I attempt to stop managed container "not-running"
Then a container ValueError should be raised with "Cannot stop"
Scenario: Destroy running container raises ValueError
Given a mock container command runner
And a running container "still-running" with container ID "rrr"
When I attempt to destroy container "still-running"
Then a container ValueError should be raised with "Cannot destroy"
Scenario: Create container transitions to error on failure
Given a mock container command runner
And the container runner configured for failed creation
When I attempt to create container "fail-create" with workspace "/workspace"
Then a container RuntimeError should be raised
And the container "fail-create" should have status "error"
Scenario: Start container transitions to error on failure
Given a mock container command runner
And the container runner configured for failed start
And a created container "fail-start" with container ID "fstart"
When I attempt to start container "fail-start"
Then a container RuntimeError should be raised
And the container "fail-start" should have status "error"
Scenario: Stop container transitions to error on failure
Given a mock container command runner
And the container runner configured for failed stop
And a running container "fail-stop" with container ID "fstop"
When I attempt to stop managed container "fail-stop"
Then a container RuntimeError should be raised
And the container "fail-stop" should have status "error"
Scenario: List containers by status
Given a mock container command runner
And the container runner configured for successful creation
And a running container "running-1" with container ID "r1"
And a stopped managed container "stopped-1" with container ID "s1"
When I list containers with status "running"
Then the list should contain 1 container
And the list should contain container "running-1"
Scenario: List all containers
Given a mock container command runner
And the container runner configured for successful creation
And a running container "all-1" with container ID "a1"
And a stopped managed container "all-2" with container ID "a2"
When I list all containers
Then the list should contain 2 containers
Scenario: Get non-existent container returns None
Given a mock container command runner
When I get container "non-existent"
Then the container result should be None
Scenario: Get container rejects empty name
Given a mock container command runner
When I attempt to get container with empty name
Then a container ValueError should be raised with "name must not be empty"
Scenario: Container status enum has all expected values
Then the ContainerStatus enum should have 6 values
And ContainerStatus should include "pending"
And ContainerStatus should include "created"
And ContainerStatus should include "running"
And ContainerStatus should include "stopped"
And ContainerStatus should include "destroyed"
And ContainerStatus should include "error"
Scenario: Valid status transitions are accepted
Then container transition from "pending" to "created" should be valid
And container transition from "created" to "running" should be valid
And container transition from "running" to "stopped" should be valid
And container transition from "stopped" to "destroyed" should be valid
Scenario: Invalid status transitions are rejected
Then container transition from "pending" to "running" should be invalid
And container transition from "destroyed" to "running" should be invalid
And container transition from "running" to "created" should be invalid
Scenario: Create container with spec and resource limits
Given a mock container command runner
And the container runner configured for successful creation
And a devcontainer spec with cpu limit 4.0 and memory 4096
When I create container "spec-container" with spec and limits
Then the container "spec-container" should have status "created"
And the container "spec-container" resource limits cpu should be 4.0
# ── HealthMonitor ─────────────────────────────────────────
Scenario: Health monitor probes running container
Given a mock container command runner
And a health monitor with interval 10.0
And a running monitored container "health-test" with container ID "h123"
And the container runner configured for healthy probe
When I probe container "health-test"
Then the probe result should be healthy
Scenario: Health monitor detects unhealthy container
Given a mock container command runner
And a health monitor with interval 10.0
And a running monitored container "unhealthy-test" with container ID "h456"
And the container runner configured for unhealthy probe
When I probe container "unhealthy-test"
Then the probe result should be unhealthy
Scenario: Health monitor rejects empty container name
Given a mock container command runner
And a health monitor with interval 10.0
When I attempt to probe container with empty name
Then a container ValueError should be raised with "container_name must not be empty"
Scenario: Health monitor probe returns unhealthy for missing container
Given a mock container command runner
And a health monitor with interval 10.0
When I probe container "missing-container"
Then the probe result should be unhealthy
And the probe message should contain "not found"
Scenario: Health monitor probe returns unhealthy when no container_id
Given a mock container command runner
And a health monitor with interval 10.0
And a running monitored container "no-id" without container ID
When I probe container "no-id"
Then the probe result should be unhealthy
And the probe message should contain "no container_id"
Scenario: Health monitor start monitoring rejects non-running container
Given a mock container command runner
And a health monitor with interval 10.0
And a stopped monitored container "stopped-mon"
When I attempt to start monitoring "stopped-mon"
Then a container ValueError should be raised with "not running"
Scenario: Health monitor start monitoring rejects missing container
Given a mock container command runner
And a health monitor with interval 10.0
When I attempt to start monitoring "missing-mon"
Then a container ValueError should be raised with "not found"
Scenario: Health monitor rejects invalid interval
Given a mock container command runner
When I attempt to create health monitor with interval 1.0
Then a container ValueError should be raised with "interval must be between"
Scenario: Health monitor rejects non-manager type
When I attempt to create health monitor with non-manager
Then a container TypeError should be raised
Scenario: Health monitor stop monitoring does not fail for unknown container
Given a mock container command runner
And a health monitor with interval 10.0
When I stop monitoring "unknown-container"
Then no container error should be raised
Scenario: Health monitor stop all monitoring
Given a mock container command runner
And a health monitor with interval 10.0
And a running monitored container "monitored-1" with container ID "m1"
And a running monitored container "monitored-2" with container ID "m2"
And I start monitoring "monitored-1"
And I start monitoring "monitored-2"
When I stop all monitoring
Then no containers should be monitored
Scenario: Health monitor probe history is maintained
Given a mock container command runner
And a health monitor with interval 10.0
And a running monitored container "history-test" with container ID "ht1"
And the container runner configured for healthy probe
When I probe container "history-test"
Then the probe history should have 1 entry
Scenario: Health probe result repr
Given a health probe result for "test-repr" that is healthy
Then the probe repr should contain "healthy"
Scenario: Health probe result rejects empty name
When I attempt to create health probe with empty name
Then a container ValueError should be raised with "container_name must not be empty"
Scenario: Validate status transition rejects non-enum types
When I attempt to validate transition with non-enum current
Then a container TypeError should be raised
Scenario: Validate status transition rejects non-enum target
When I attempt to validate transition with non-enum target
Then a container TypeError should be raised
Scenario: Destroy container from error state
Given a mock container command runner
And an errored managed container "err-destroy" with container ID "errd"
When I destroy container "err-destroy"
Then the container "err-destroy" should have status "destroyed"