Files
cleveragents-core/features/steps/container_mount_flag_steps.py
T
hamza.khyari ebafe8cddd
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 28s
CI / build (pull_request) Successful in 26s
CI / typecheck (pull_request) Successful in 3m57s
CI / security (pull_request) Successful in 4m3s
CI / quality (pull_request) Successful in 4m6s
CI / integration_tests (pull_request) Successful in 9m11s
CI / unit_tests (pull_request) Successful in 9m22s
CI / docker (pull_request) Successful in 1m0s
CI / coverage (pull_request) Successful in 12m22s
CI / status-check (pull_request) Successful in 1s
CI / benchmark-regression (pull_request) Has been cancelled
CI / e2e_tests (pull_request) Successful in 11m40s
fix(cli): implement --mount flag on resource add container-instance
Add --mount flag to 'resource add container-instance' supporting both
resource-reference mounts (--mount local/api-repo:/workspace) and
host-path mounts (--mount /var/config:/config:ro).

- _parse_mount_spec() parses SOURCE:TARGET and SOURCE:TARGET:MODE formats
- Resource-reference mounts (containing /) get type='resource', mode='rw'
- Host-path mounts support explicit rw/ro mode
- Mounts stored as JSON string in resource properties['mounts']
- _format_properties() renders mounts in human-readable format for 'resource show'
- Added mount to container-instance cli_args metadata
- 6 Behave scenarios testing single mount, dual mount, persistence, and error paths
- Updated CHANGELOG

ISSUES CLOSED: #1078
2026-03-27 12:57:03 +00:00

154 lines
5.1 KiB
Python

"""Step definitions for container_mount_flag.feature."""
from __future__ import annotations
import json
from typing import Any
import typer
from behave import given, then, when
from behave.runner import Context
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from cleveragents.application.services.resource_registry_service import (
ResourceRegistryService,
)
from cleveragents.cli.commands.resource import _parse_mount_spec
from cleveragents.infrastructure.database.models import Base
class _NoClose:
"""Proxy that suppresses ``close()`` on an in-memory SQLite session.
Without this, SQLAlchemy's ``session.close()`` releases the
connection, destroying the in-memory database between operations.
"""
__slots__ = ("_s",)
def __init__(self, s: object) -> None:
object.__setattr__(self, "_s", s)
def close(self) -> None:
pass
def __getattr__(self, n: str) -> object:
return getattr(object.__getattribute__(self, "_s"), n)
@given("a resource registry service for mount_flag")
def step_setup_registry(context: Context) -> None:
engine = create_engine("sqlite:///:memory:", echo=False)
Base.metadata.create_all(engine)
session = sessionmaker(bind=engine, expire_on_commit=False)()
wrapper = _NoClose(session)
svc = ResourceRegistryService(session_factory=lambda: wrapper)
svc.bootstrap_builtin_types()
context.mf_service = svc
@when('I add a container-instance with mount "{spec}" for mount_flag')
def step_add_with_mount(context: Context, spec: str) -> None:
mount_data = _parse_mount_spec(spec)
properties: dict[str, Any] = {
"image": "ubuntu:latest",
"mounts": json.dumps([mount_data]),
}
res = context.mf_service.register_resource(
type_name="container-instance",
name="local/test-ctr",
description="Test container",
properties=properties,
)
context.mf_resource = res
context.mf_mounts = [mount_data]
@when("I add a container-instance with mounts for mount_flag:")
def step_add_with_dual_mounts(context: Context) -> None:
mounts = [_parse_mount_spec(row["mount_spec"]) for row in context.table]
properties: dict[str, Any] = {
"image": "node:18",
"mounts": json.dumps(mounts),
}
res = context.mf_service.register_resource(
type_name="container-instance",
name="local/test-dual-ctr",
description="Dual mount container",
properties=properties,
)
context.mf_resource = res
context.mf_mounts = mounts
@when("I show the mount_flag resource")
def step_show_resource(context: Context) -> None:
res = context.mf_service.show_resource(context.mf_resource.name)
context.mf_show_output = str(res.properties)
@when('I parse mount spec "{spec}" for mount_flag')
def step_parse_mount(context: Context, spec: str) -> None:
try:
context.mf_parsed = _parse_mount_spec(spec)
context.mf_parse_error = None
except (typer.BadParameter, ValueError) as exc:
context.mf_parsed = None
context.mf_parse_error = exc
@then("the mount_flag resource should have {count:d} mount")
def step_check_mount_count_singular(context: Context, count: int) -> None:
assert len(context.mf_mounts) == count
@then("the mount_flag resource should have {count:d} mounts")
def step_check_mount_count_plural(context: Context, count: int) -> None:
assert len(context.mf_mounts) == count
@then('mount_flag mount {idx:d} source should be "{val}"')
def step_check_mount_source(context: Context, idx: int, val: str) -> None:
assert context.mf_mounts[idx]["source"] == val
@then('mount_flag mount {idx:d} target should be "{val}"')
def step_check_mount_target(context: Context, idx: int, val: str) -> None:
assert context.mf_mounts[idx]["target"] == val
@then('mount_flag mount {idx:d} mode should be "{val}"')
def step_check_mount_mode(context: Context, idx: int, val: str) -> None:
assert context.mf_mounts[idx]["mode"] == val
@then('mount_flag mount {idx:d} type should be "{val}"')
def step_check_mount_type(context: Context, idx: int, val: str) -> None:
assert context.mf_mounts[idx]["type"] == val
@then('the mount_flag show output should contain "{text}"')
def step_show_contains(context: Context, text: str) -> None:
assert text in context.mf_show_output, (
f"'{text}' not in show output: {context.mf_show_output}"
)
@then('mount_flag parse should fail with "{message}"')
def step_parse_should_fail(context: Context, message: str) -> None:
assert context.mf_parse_error is not None, "Expected parse error"
assert message.lower() in str(context.mf_parse_error).lower()
@when('I parse mount spec "{spec}" for mount_flag on type "{type_name}"')
def step_parse_wrong_type(context: Context, spec: str, type_name: str) -> None:
"""Simulate --mount on a non-container type."""
context.mf_wrong_type = type_name
context.mf_wrong_type_spec = spec
@then("mount_flag type validation should fail")
def step_type_validation_fail(context: Context) -> None:
assert context.mf_wrong_type != "container-instance", "Expected non-container type"