Files
temp/features/steps/resource_list_lifecycle_state_steps.py
freemo 2637b272dd feat(cli): add devcontainer lifecycle state column to agents resource list output
Add a 'State' column to the 'agents resource list' rich table output that
shows the current lifecycle state for devcontainer-instance and
container-instance resources. For newly-discovered devcontainers the state
displays as 'detected (not built)'; running, stopped, and failed states are
also shown.

Changes:
- Add _get_lifecycle_state_str() helper that queries get_lifecycle_tracker()
  for container resource types and returns a human-readable state string
- Add 'State' column to the rich table in resource_list()
- Show warning banner '⚠ Devcontainer detected at ...' for resources in
  the detected state, matching the spec output for agents resource add
- Update _resource_dict() to include 'lifecycle_state' field in JSON/YAML
  output (null for non-container resources)
- Add BDD feature file and step definitions covering all lifecycle states,
  warning banner behaviour, and JSON output

ISSUES CLOSED: #2596
2026-04-05 18:13:11 +00:00

226 lines
7.7 KiB
Python

"""Step definitions for resource list lifecycle state feature tests.
Tests that ``agents resource list`` shows the devcontainer lifecycle state
column as required by spec line 10726 (issue #2596).
"""
from __future__ import annotations
import json
from io import StringIO
from typing import Any
from unittest.mock import patch
from behave import given, then, when # type: ignore[import-untyped]
from behave.runner import Context # type: ignore[import-untyped]
from rich.console import Console
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from cleveragents.application.services.resource_registry_service import (
ResourceRegistryService,
)
from cleveragents.domain.models.core.container_lifecycle import (
ContainerLifecycleState,
ContainerLifecycleTracker,
)
from cleveragents.infrastructure.database.models import Base
def _make_service(context: Context) -> ResourceRegistryService:
"""Create or return a cached ResourceRegistryService."""
if not hasattr(context, "resource_cli_service"):
engine = create_engine("sqlite:///:memory:", echo=False)
Base.metadata.create_all(engine)
factory = sessionmaker(bind=engine, expire_on_commit=False)
context.resource_cli_service = ResourceRegistryService(session_factory=factory)
return context.resource_cli_service
def _capture_output(func: Any, *args: Any, **kwargs: Any) -> tuple[str, bool]:
"""Run a CLI function capturing its console output and success status."""
import contextlib
buf = StringIO()
console = Console(
file=buf, width=200, no_color=True, highlight=False, force_terminal=False
)
import cleveragents.cli.commands.resource as resource_mod
orig_console = resource_mod.console
resource_mod.console = console
failed = False
try:
with contextlib.redirect_stdout(buf):
func(*args, **kwargs)
except SystemExit:
failed = True
except Exception:
failed = True
finally:
resource_mod.console = orig_console
return buf.getvalue(), failed
def _patch_service(context: Context) -> Any:
"""Monkey-patch the container to return our in-memory service."""
import cleveragents.cli.commands.resource as resource_mod
orig_fn = resource_mod._get_registry_service
def _mock_get() -> ResourceRegistryService:
return _make_service(context)
resource_mod._get_registry_service = _mock_get
return orig_fn
def _unpatch_service(orig_fn: Any) -> None:
"""Restore original service getter."""
import cleveragents.cli.commands.resource as resource_mod
resource_mod._get_registry_service = orig_fn
# ---- Given steps ----
@given("a devcontainer-instance resource is registered")
def step_register_devcontainer_resource(context: Context) -> None:
"""Register a devcontainer-instance resource in the registry."""
service = _make_service(context)
service.bootstrap_builtin_types()
resource = service.register_resource(
type_name="devcontainer-instance",
name=None, # auto-discovered child resources have no name
location="/tmp/test-project",
description="Test devcontainer",
read_only=False,
properties=None,
)
context.devcontainer_resource_id = resource.resource_id
# Default state is DETECTED — no tracker manipulation needed
@given("a container-instance resource is registered")
def step_register_container_resource(context: Context) -> None:
"""Register a container-instance resource in the registry."""
service = _make_service(context)
service.bootstrap_builtin_types()
resource = service.register_resource(
type_name="container-instance",
name=None, # auto-discovered child resources have no name
location="/tmp/test-container",
description="Test container",
read_only=False,
properties=None,
)
context.devcontainer_resource_id = resource.resource_id
# Default state is DETECTED — no tracker manipulation needed
@given('the devcontainer lifecycle state is "{state}"')
def step_set_devcontainer_lifecycle_state(context: Context, state: str) -> None:
"""Set the lifecycle state of the registered devcontainer resource."""
from cleveragents.resource.handlers._devcontainer_internals import (
set_lifecycle_tracker,
)
resource_id = context.devcontainer_resource_id
lifecycle_state = ContainerLifecycleState(state)
tracker = ContainerLifecycleTracker(
resource_id=resource_id,
current_state=lifecycle_state,
)
set_lifecycle_tracker(tracker)
@given("the lifecycle tracker raises an error for the resource")
def step_lifecycle_tracker_raises_error(context: Context) -> None:
"""Configure the lifecycle tracker to raise a ValueError for the resource."""
context.lifecycle_tracker_error_patch = patch(
"cleveragents.cli.commands.resource.get_lifecycle_tracker",
side_effect=ValueError("Simulated tracker failure"),
)
context.lifecycle_tracker_error_patch.start()
# ---- When steps ----
@when("I run resource list with all flag")
def step_run_resource_list_all(context: Context) -> None:
"""Run resource list --all command."""
from cleveragents.cli.commands.resource import resource_list
orig = _patch_service(context)
try:
output, failed = _capture_output(
resource_list, type_filter=None, show_all=True, fmt="rich"
)
context.resource_cli_output = output
context.resource_cli_failed = failed
finally:
_unpatch_service(orig)
# Stop any active error patch
if hasattr(context, "lifecycle_tracker_error_patch"):
context.lifecycle_tracker_error_patch.stop()
del context.lifecycle_tracker_error_patch
@when('I run resource list with all flag and format "{fmt}"')
def step_run_resource_list_all_fmt(context: Context, fmt: str) -> None:
"""Run resource list --all with specific format."""
from cleveragents.cli.commands.resource import resource_list
orig = _patch_service(context)
try:
output, failed = _capture_output(
resource_list, type_filter=None, show_all=True, fmt=fmt
)
context.resource_cli_output = output
context.resource_cli_failed = failed
finally:
_unpatch_service(orig)
# ---- Then steps ----
@then('the resource output should not contain "{text}"')
def step_output_should_not_contain(context: Context, text: str) -> None:
"""Assert the output does not contain the given text."""
output = context.resource_cli_output
assert text not in output, (
f"Expected output NOT to contain {text!r}, but it did.\nOutput:\n{output}"
)
@then("the resource JSON lifecycle_state should be null for non-container resources")
def step_json_lifecycle_state_null(context: Context) -> None:
"""Assert all resources in JSON output have null lifecycle_state."""
output = context.resource_cli_output
data = json.loads(output)
if isinstance(data, list):
for item in data:
assert item.get("lifecycle_state") is None, (
f"Expected lifecycle_state to be null for non-container resource, "
f"got {item.get('lifecycle_state')!r}"
)
else:
assert data.get("lifecycle_state") is None, (
f"Expected lifecycle_state to be null, got {data.get('lifecycle_state')!r}"
)
@then("the resource list command should succeed")
def step_resource_list_command_should_succeed(context: Context) -> None:
"""Assert the resource list command did not fail."""
assert not context.resource_cli_failed, (
f"Expected resource list command to succeed, but it failed.\n"
f"Output:\n{context.resource_cli_output}"
)