forked from HAL9000/cleveragents-core
2637b272dd
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
226 lines
7.7 KiB
Python
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}"
|
|
)
|