forked from HAL9000/cleveragents-core
2eb31a598c
Added targeted Behave BDD feature files and step definitions to improve unit test coverage for: - decision_service.py: Full coverage of all 7 service methods (18 scenarios) - plan_apply_service.py: Branch coverage for handle_merge_failure (2 scenarios) - plan_executor.py: Edge cases for rollback, checkpoint, and parse_steps (15 scenarios) - cli/commands/plan.py: Uncovered region lines 1950-2273 (23 scenarios) - repositories.py: Remaining missed branches and lines (14 scenarios) - sandbox/checkpoint.py: Full coverage of CheckpointManager (26 scenarios) - langgraph/bridge.py: Remaining uncovered lines and branches (10 scenarios) - cli/commands/config.py: Safety net to maintain 100% coverage (42 scenarios) Total: 150 new scenarios, 596 steps, all passing. Also fixed a step definition collision in plan_lifecycle_coverage by renaming "the delete result should be false" to "the plan delete result should be false". ISSUES CLOSED: #475
810 lines
32 KiB
Python
810 lines
32 KiB
Python
"""Step definitions for repositories_remaining_branches_coverage.feature.
|
|
|
|
Targets the last uncovered lines and branches in repositories.py:
|
|
- ToolRepository.add: DuplicateToolError re-raise (L3541-3542),
|
|
DatabaseError wrapping (L3542)
|
|
- ToolRepository.get_by_name: success path (L3558)
|
|
- ResourceRepository.link_child: parent_type_row absent (branch 2298->2310)
|
|
- ResourceRepository.resolve_namespaced_name: ULID fallback (branch 2711->2714)
|
|
- ResourceRepository.get_children: populated list (branch 2426->2420)
|
|
- ResourceRepository.get_parents: populated list (branch 2459->2453)
|
|
- ResourceRepository._get_ancestors + _build_cycle_path (branches 2635->2633, 2661->2659)
|
|
- NamespacedProjectRepository.update: success path (branch 2928->2931)
|
|
- AutomationProfileRepository.upsert: new insert (branch 4193->4200)
|
|
- ValidationAttachmentRepository.attach: project_name scope (branch 3633->3634)
|
|
- LifecyclePlanRepository.update: multiple project links (branch 1395->1394)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from cleveragents.infrastructure.database.models import (
|
|
Base,
|
|
ResourceLinkModel,
|
|
ResourceModel,
|
|
ResourceTypeModel,
|
|
)
|
|
from cleveragents.infrastructure.database.repositories import (
|
|
CycleDetectedError,
|
|
DuplicateToolError,
|
|
NamespacedProjectRepository,
|
|
ResourceRepository,
|
|
ToolRepository,
|
|
ValidationAttachmentRepository,
|
|
)
|
|
|
|
# ── helpers ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
def _make_engine_and_factory():
|
|
"""Create a fresh in-memory SQLite DB with all tables."""
|
|
engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(engine)
|
|
factory = sessionmaker(bind=engine)
|
|
return engine, factory
|
|
|
|
|
|
def _now_iso() -> str:
|
|
return datetime.now(tz=UTC).isoformat()
|
|
|
|
|
|
def _make_tool_ns(
|
|
name: str = "local/test-tool", tool_type: str = "tool"
|
|
) -> SimpleNamespace:
|
|
"""Create a tool domain-like SimpleNamespace suitable for ToolRepository.add."""
|
|
return SimpleNamespace(
|
|
name=name,
|
|
description=f"Tool {name}",
|
|
tool_type=tool_type,
|
|
source="builtin",
|
|
input_schema=None,
|
|
output_schema=None,
|
|
capability=None,
|
|
resource_slots=None,
|
|
lifecycle_json=None,
|
|
code=None,
|
|
mcp_server=None,
|
|
mcp_tool_name=None,
|
|
agent_skill_path=None,
|
|
timeout=300,
|
|
wraps=None,
|
|
transform=None,
|
|
mode=None,
|
|
argument_mapping_json=None,
|
|
)
|
|
|
|
|
|
def _ensure_resource_type_row(factory, type_name: str) -> None:
|
|
"""Insert a ResourceTypeModel row directly if absent."""
|
|
session = factory()
|
|
existing = session.query(ResourceTypeModel).filter_by(name=type_name).first()
|
|
if existing:
|
|
return
|
|
now = _now_iso()
|
|
ns = type_name.split("/")[0] if "/" in type_name else "builtin"
|
|
row = ResourceTypeModel(
|
|
name=type_name,
|
|
namespace=ns,
|
|
description="test type",
|
|
resource_kind="physical",
|
|
sandbox_strategy="none",
|
|
user_addable=True,
|
|
handler_ref=None,
|
|
args_schema_json=None,
|
|
allowed_parent_types_json=None,
|
|
allowed_child_types_json=None,
|
|
auto_discover_json=None,
|
|
capabilities_json='{"read": true, "write": true, "sandbox": true, "checkpoint": false}',
|
|
equivalence_json=None,
|
|
source=None,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
session.add(row)
|
|
session.commit()
|
|
|
|
|
|
def _create_resource_row(
|
|
factory,
|
|
resource_id: str,
|
|
type_name: str = "test/default-type",
|
|
name: str | None = None,
|
|
namespace: str | None = None,
|
|
) -> str:
|
|
"""Insert a ResourceModel row directly and return the resource_id."""
|
|
session = factory()
|
|
now = _now_iso()
|
|
row = ResourceModel(
|
|
resource_id=resource_id,
|
|
namespaced_name=name,
|
|
namespace=namespace,
|
|
type_name=type_name,
|
|
resource_kind="physical",
|
|
location=None,
|
|
description="test resource",
|
|
read_only=False,
|
|
auto_discovered=False,
|
|
sandbox_strategy=None,
|
|
content_hash=None,
|
|
properties_json=None,
|
|
metadata_json=None,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
session.add(row)
|
|
session.commit()
|
|
return resource_id
|
|
|
|
|
|
def _create_link(factory, parent_id: str, child_id: str) -> None:
|
|
"""Insert a ResourceLinkModel row directly."""
|
|
session = factory()
|
|
now = _now_iso()
|
|
session.add(
|
|
ResourceLinkModel(parent_id=parent_id, child_id=child_id, created_at=now)
|
|
)
|
|
session.commit()
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# ToolRepository.add: DuplicateToolError re-raise (lines 3541-3542)
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@given("remaining cov an in-memory database with tool tables")
|
|
def step_remaining_cov_tool_db(context: Context) -> None:
|
|
engine, factory = _make_engine_and_factory()
|
|
context.rbc_engine = engine
|
|
context.rbc_session_factory = factory
|
|
context.rbc_tool_repo = ToolRepository(session_factory=factory)
|
|
context.rbc_error = None
|
|
|
|
|
|
@given('remaining cov a tool "{name}" has been added')
|
|
def step_remaining_cov_tool_added(context: Context, name: str) -> None:
|
|
context.rbc_tool_repo.add(_make_tool_ns(name))
|
|
|
|
|
|
@when('remaining cov the same tool "{name}" is added again')
|
|
def step_remaining_cov_tool_add_dup(context: Context, name: str) -> None:
|
|
# Mock create() to raise DuplicateToolError directly.
|
|
# This is necessary because the real create() is wrapped with
|
|
# @database_retry, which retries DatabaseError subclasses (including
|
|
# DuplicateToolError). By mocking create(), we bypass the retry
|
|
# decorator and directly exercise the ``except DuplicateToolError: raise``
|
|
# path in add() (lines 3540-3541).
|
|
from unittest.mock import patch
|
|
|
|
dup_error = DuplicateToolError(name)
|
|
with patch.object(context.rbc_tool_repo, "create", side_effect=dup_error):
|
|
try:
|
|
context.rbc_tool_repo.add(_make_tool_ns(name))
|
|
context.rbc_error = None
|
|
except Exception as exc:
|
|
context.rbc_error = exc
|
|
|
|
|
|
@then("remaining cov a DuplicateToolError should be raised for the duplicate")
|
|
def step_remaining_cov_assert_dup_tool(context: Context) -> None:
|
|
assert isinstance(context.rbc_error, DuplicateToolError), (
|
|
f"Expected DuplicateToolError, got {type(context.rbc_error).__name__}: "
|
|
f"{context.rbc_error}"
|
|
)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# ToolRepository.add: DatabaseError wrapping (line 3542)
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@given(
|
|
"remaining cov a ToolRepository with a session that raises DatabaseError on create"
|
|
)
|
|
def step_remaining_cov_tool_repo_db_error(context: Context) -> None:
|
|
from cleveragents.core.exceptions import DatabaseError as CoreDatabaseError
|
|
|
|
_engine, factory = _make_engine_and_factory()
|
|
|
|
# Create a subclass that overrides create() to raise a DatabaseError
|
|
# (not DuplicateToolError) to exercise line 3542-3543
|
|
class _FailingToolRepo(ToolRepository):
|
|
def create(self, tool: Any) -> Any:
|
|
raise CoreDatabaseError("Simulated create failure")
|
|
|
|
context.rbc_tool_repo = _FailingToolRepo(session_factory=factory)
|
|
context.rbc_error = None
|
|
|
|
|
|
@when("remaining cov a tool is added and a DatabaseError is expected")
|
|
def step_remaining_cov_tool_add_db_error(context: Context) -> None:
|
|
from cleveragents.core.exceptions import DatabaseError as CoreDatabaseError
|
|
|
|
try:
|
|
context.rbc_tool_repo.add(_make_tool_ns("local/db-error-tool"))
|
|
context.rbc_error = None
|
|
except CoreDatabaseError as exc:
|
|
context.rbc_error = exc
|
|
except Exception as exc:
|
|
context.rbc_error = exc
|
|
|
|
|
|
@then('remaining cov a DatabaseError mentioning "Failed to add tool" should be raised')
|
|
def step_remaining_cov_assert_db_error(context: Context) -> None:
|
|
from cleveragents.core.exceptions import DatabaseError as CoreDatabaseError
|
|
|
|
assert context.rbc_error is not None, "Expected a DatabaseError"
|
|
assert isinstance(context.rbc_error, CoreDatabaseError), (
|
|
f"Expected DatabaseError, got {type(context.rbc_error).__name__}: "
|
|
f"{context.rbc_error}"
|
|
)
|
|
assert "Failed to add tool" in str(context.rbc_error), (
|
|
f"Expected 'Failed to add tool' in: {context.rbc_error}"
|
|
)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# ToolRepository.get_by_name: success returns domain (line 3558)
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@when('remaining cov get_by_name is invoked for "{name}"')
|
|
def step_remaining_cov_get_by_name(context: Context, name: str) -> None:
|
|
context.rbc_result = context.rbc_tool_repo.get_by_name(name)
|
|
|
|
|
|
@then('remaining cov get_by_name should return a non-None result with name "{name}"')
|
|
def step_remaining_cov_assert_get_by_name(context: Context, name: str) -> None:
|
|
result = context.rbc_result
|
|
assert result is not None, f"Expected non-None result for '{name}'"
|
|
# _to_legacy_domain returns a SimpleNamespace with .name
|
|
actual_name = result.name if hasattr(result, "name") else result.get("name", "")
|
|
assert actual_name == name, f"Expected name '{name}', got '{actual_name}'"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# ResourceRepository.link_child: parent type row absent (branch 2298→2310)
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@given("remaining cov an in-memory resource database")
|
|
def step_remaining_cov_res_db(context: Context) -> None:
|
|
engine, factory = _make_engine_and_factory()
|
|
context.rbc_engine = engine
|
|
context.rbc_factory = factory
|
|
context.rbc_res_repo = ResourceRepository(factory)
|
|
context.rbc_error = None
|
|
|
|
|
|
@given("remaining cov two resources with a type that has no ResourceTypeModel row")
|
|
def step_remaining_cov_res_no_type_row(context: Context) -> None:
|
|
# Insert resources directly with a type_name whose ResourceTypeModel
|
|
# does NOT exist. This means parent_type_row will be None, exercising
|
|
# the branch where we skip allowed_children validation.
|
|
session = context.rbc_factory()
|
|
now = _now_iso()
|
|
for rid in ["res-notype-parent", "res-notype-child"]:
|
|
row = ResourceModel(
|
|
resource_id=rid,
|
|
namespaced_name=None,
|
|
namespace=None,
|
|
type_name="ghost-type",
|
|
resource_kind="physical",
|
|
location=None,
|
|
description="test",
|
|
read_only=False,
|
|
auto_discovered=False,
|
|
sandbox_strategy=None,
|
|
content_hash=None,
|
|
properties_json=None,
|
|
metadata_json=None,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
session.add(row)
|
|
session.commit()
|
|
context.rbc_link_parent = "res-notype-parent"
|
|
context.rbc_link_child = "res-notype-child"
|
|
|
|
|
|
@when("remaining cov link_child is called for those resources")
|
|
def step_remaining_cov_link_child_no_type(context: Context) -> None:
|
|
try:
|
|
context.rbc_res_repo.link_child(context.rbc_link_parent, context.rbc_link_child)
|
|
context.rbc_error = None
|
|
except Exception as exc:
|
|
context.rbc_error = exc
|
|
|
|
|
|
@then("remaining cov the link should be created successfully")
|
|
def step_remaining_cov_assert_link_ok(context: Context) -> None:
|
|
assert context.rbc_error is None, f"Unexpected error: {context.rbc_error}"
|
|
session = context.rbc_factory()
|
|
link = (
|
|
session.query(ResourceLinkModel)
|
|
.filter_by(parent_id=context.rbc_link_parent, child_id=context.rbc_link_child)
|
|
.first()
|
|
)
|
|
assert link is not None, "Expected a ResourceLinkModel row"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# ResourceRepository.resolve_namespaced_name: ULID fallback (branch 2711→2714)
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@given("remaining cov a resource exists with a known ULID but no namespaced name")
|
|
def step_remaining_cov_res_ulid_only(context: Context) -> None:
|
|
from ulid import ULID
|
|
|
|
rid = str(ULID())
|
|
_ensure_resource_type_row(context.rbc_factory, "test/ulid-type")
|
|
_create_resource_row(
|
|
context.rbc_factory,
|
|
rid,
|
|
type_name="test/ulid-type",
|
|
name=None,
|
|
)
|
|
context.rbc_ulid_resource_id = rid
|
|
|
|
|
|
@when("remaining cov resolve_namespaced_name is called with the ULID")
|
|
def step_remaining_cov_resolve_ulid(context: Context) -> None:
|
|
context.rbc_result = context.rbc_res_repo.resolve_namespaced_name(
|
|
context.rbc_ulid_resource_id
|
|
)
|
|
|
|
|
|
@then("remaining cov the resource should be resolved successfully")
|
|
def step_remaining_cov_assert_resolved(context: Context) -> None:
|
|
assert context.rbc_result is not None, "Expected resource, got None"
|
|
actual_id = (
|
|
context.rbc_result.resource_id
|
|
if hasattr(context.rbc_result, "resource_id")
|
|
else context.rbc_result.get("resource_id", "")
|
|
)
|
|
assert actual_id == context.rbc_ulid_resource_id, (
|
|
f"Expected ID {context.rbc_ulid_resource_id}, got {actual_id}"
|
|
)
|
|
|
|
|
|
@when('remaining cov resolve_namespaced_name is called with unknown ULID "{ulid}"')
|
|
def step_remaining_cov_resolve_unknown(context: Context, ulid: str) -> None:
|
|
context.rbc_result = context.rbc_res_repo.resolve_namespaced_name(ulid)
|
|
|
|
|
|
@then("remaining cov resolve_namespaced_name should return None")
|
|
def step_remaining_cov_assert_resolve_none(context: Context) -> None:
|
|
assert context.rbc_result is None, f"Expected None, got {context.rbc_result}"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# ResourceRepository.get_children: populated list (branch 2426→2420)
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@given("remaining cov a parent resource linked to two child resources")
|
|
def step_remaining_cov_parent_with_children(context: Context) -> None:
|
|
from ulid import ULID
|
|
|
|
_ensure_resource_type_row(context.rbc_factory, "test/link-type")
|
|
parent_id = str(ULID())
|
|
child1_id = str(ULID())
|
|
child2_id = str(ULID())
|
|
for rid in [parent_id, child1_id, child2_id]:
|
|
_create_resource_row(context.rbc_factory, rid, type_name="test/link-type")
|
|
_create_link(context.rbc_factory, parent_id, child1_id)
|
|
_create_link(context.rbc_factory, parent_id, child2_id)
|
|
context.rbc_parent_id = parent_id
|
|
|
|
|
|
@when("remaining cov get_children is called on the parent resource")
|
|
def step_remaining_cov_get_children(context: Context) -> None:
|
|
context.rbc_children = context.rbc_res_repo.get_children(context.rbc_parent_id)
|
|
|
|
|
|
@then("remaining cov {n:d} child resources should be returned")
|
|
def step_remaining_cov_assert_children_count(context: Context, n: int) -> None:
|
|
assert len(context.rbc_children) == n, (
|
|
f"Expected {n} children, got {len(context.rbc_children)}"
|
|
)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# ResourceRepository.get_parents: populated list (branch 2459→2453)
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@given("remaining cov a child resource linked from two parent resources")
|
|
def step_remaining_cov_child_with_parents(context: Context) -> None:
|
|
from ulid import ULID
|
|
|
|
_ensure_resource_type_row(context.rbc_factory, "test/link-type")
|
|
child_id = str(ULID())
|
|
parent1_id = str(ULID())
|
|
parent2_id = str(ULID())
|
|
for rid in [child_id, parent1_id, parent2_id]:
|
|
_create_resource_row(context.rbc_factory, rid, type_name="test/link-type")
|
|
_create_link(context.rbc_factory, parent1_id, child_id)
|
|
_create_link(context.rbc_factory, parent2_id, child_id)
|
|
context.rbc_child_id = child_id
|
|
|
|
|
|
@when("remaining cov get_parents is called on the child resource")
|
|
def step_remaining_cov_get_parents(context: Context) -> None:
|
|
context.rbc_parents = context.rbc_res_repo.get_parents(context.rbc_child_id)
|
|
|
|
|
|
@then("remaining cov {n:d} parent resources should be returned")
|
|
def step_remaining_cov_assert_parents_count(context: Context, n: int) -> None:
|
|
assert len(context.rbc_parents) == n, (
|
|
f"Expected {n} parents, got {len(context.rbc_parents)}"
|
|
)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# ResourceRepository._get_ancestors + _build_cycle_path (branches 2635→2633, 2661→2659)
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@given('remaining cov resources "cyc-A" and "cyc-B" linked as A->B')
|
|
def step_remaining_cov_cycle_setup(context: Context) -> None:
|
|
_ensure_resource_type_row(context.rbc_factory, "test/link-type")
|
|
_create_resource_row(context.rbc_factory, "cyc-A", type_name="test/link-type")
|
|
_create_resource_row(context.rbc_factory, "cyc-B", type_name="test/link-type")
|
|
_create_link(context.rbc_factory, "cyc-A", "cyc-B")
|
|
|
|
|
|
@when("remaining cov link_child is called to create B->A forming a cycle")
|
|
def step_remaining_cov_link_cycle(context: Context) -> None:
|
|
try:
|
|
context.rbc_res_repo.link_child("cyc-B", "cyc-A")
|
|
context.rbc_error = None
|
|
except Exception as exc:
|
|
context.rbc_error = exc
|
|
|
|
|
|
@then("remaining cov a CycleDetectedError should be raised with a path")
|
|
def step_remaining_cov_assert_cycle(context: Context) -> None:
|
|
assert isinstance(context.rbc_error, CycleDetectedError), (
|
|
f"Expected CycleDetectedError, got "
|
|
f"{type(context.rbc_error).__name__}: {context.rbc_error}"
|
|
)
|
|
assert hasattr(context.rbc_error, "path"), "Error should have 'path' attribute"
|
|
assert len(context.rbc_error.path) > 0, "Cycle path should not be empty"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# NamespacedProjectRepository.update: success path (branch 2928→2931)
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@given("remaining cov an in-memory project database")
|
|
def step_remaining_cov_proj_db(context: Context) -> None:
|
|
engine, factory = _make_engine_and_factory()
|
|
context.rbc_engine = engine
|
|
context.rbc_proj_factory = factory
|
|
context.rbc_proj_repo = NamespacedProjectRepository(session_factory=factory)
|
|
context.rbc_error = None
|
|
|
|
|
|
@given('remaining cov a project "{ns_name}" exists')
|
|
def step_remaining_cov_proj_exists(context: Context, ns_name: str) -> None:
|
|
from cleveragents.domain.models.core.project import NamespacedProject
|
|
|
|
parts = ns_name.split("/", 1)
|
|
namespace = parts[0] if len(parts) == 2 else "local"
|
|
name = parts[1] if len(parts) == 2 else parts[0]
|
|
|
|
project = NamespacedProject(
|
|
name=name,
|
|
namespace=namespace,
|
|
description="Original description",
|
|
)
|
|
context.rbc_proj_repo.create(project)
|
|
context.rbc_project = project
|
|
|
|
|
|
@when('remaining cov the project "{ns_name}" is updated with new description')
|
|
def step_remaining_cov_proj_update(context: Context, ns_name: str) -> None:
|
|
from cleveragents.domain.models.core.project import NamespacedProject
|
|
|
|
parts = ns_name.split("/", 1)
|
|
namespace = parts[0] if len(parts) == 2 else "local"
|
|
name = parts[1] if len(parts) == 2 else parts[0]
|
|
|
|
updated = NamespacedProject(
|
|
name=name,
|
|
namespace=namespace,
|
|
description="Updated description via remaining cov test",
|
|
)
|
|
try:
|
|
context.rbc_proj_repo.update(updated)
|
|
context.rbc_error = None
|
|
except Exception as exc:
|
|
context.rbc_error = exc
|
|
|
|
|
|
@then("remaining cov the project update should succeed")
|
|
def step_remaining_cov_assert_proj_updated(context: Context) -> None:
|
|
assert context.rbc_error is None, f"Unexpected error: {context.rbc_error}"
|
|
# Verify the update persisted
|
|
result = context.rbc_proj_repo.get(context.rbc_project.namespaced_name)
|
|
assert result is not None, "Project not found after update"
|
|
assert result.description == "Updated description via remaining cov test", (
|
|
f"Expected updated description, got '{result.description}'"
|
|
)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# AutomationProfileRepository.upsert: new insert (branch 4193→4200)
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@given("remaining cov an in-memory automation profile database")
|
|
def step_remaining_cov_ap_db(context: Context) -> None:
|
|
engine, factory = _make_engine_and_factory()
|
|
context.rbc_engine = engine
|
|
context.rbc_ap_factory = factory
|
|
|
|
from cleveragents.infrastructure.database.repositories import (
|
|
AutomationProfileRepository,
|
|
)
|
|
|
|
context.rbc_ap_repo = AutomationProfileRepository(session_factory=factory)
|
|
context.rbc_error = None
|
|
|
|
|
|
@when('remaining cov a brand new profile "{name}" is upserted')
|
|
def step_remaining_cov_ap_new_upsert(context: Context, name: str) -> None:
|
|
from cleveragents.domain.models.core.automation_profile import AutomationProfile
|
|
|
|
profile = AutomationProfile(
|
|
name=name,
|
|
description="Brand new profile",
|
|
schema_version="1.0",
|
|
)
|
|
try:
|
|
context.rbc_ap_repo.upsert(profile)
|
|
context.rbc_error = None
|
|
except Exception as exc:
|
|
context.rbc_error = exc
|
|
|
|
|
|
@then('remaining cov the profile "{name}" should be retrievable')
|
|
def step_remaining_cov_assert_ap_exists(context: Context, name: str) -> None:
|
|
assert context.rbc_error is None, f"Unexpected error: {context.rbc_error}"
|
|
result = context.rbc_ap_repo.get_by_name(name)
|
|
assert result is not None, f"Profile '{name}' not found after upsert"
|
|
assert result.name == name, f"Expected name '{name}', got '{result.name}'"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# ValidationAttachmentRepository.attach: project_name scope (branch 3633→3634)
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@given("remaining cov an in-memory database with validation tables")
|
|
def step_remaining_cov_val_db(context: Context) -> None:
|
|
engine, factory = _make_engine_and_factory()
|
|
context.rbc_engine = engine
|
|
context.rbc_val_factory = factory
|
|
context.rbc_val_repo = ValidationAttachmentRepository(session_factory=factory)
|
|
context.rbc_error = None
|
|
|
|
|
|
@when('remaining cov a validation is attached using project scope "{proj}"')
|
|
def step_remaining_cov_attach_with_project(context: Context, proj: str) -> None:
|
|
try:
|
|
context.rbc_attachment = context.rbc_val_repo.attach(
|
|
validation_name="local/check-proj",
|
|
resource_id="res-proj-1",
|
|
mode="required",
|
|
project_name=proj,
|
|
)
|
|
context.rbc_error = None
|
|
except Exception as exc:
|
|
context.rbc_error = exc
|
|
|
|
|
|
@then('remaining cov the returned attachment has project_name "{proj}"')
|
|
def step_remaining_cov_assert_attach_project(context: Context, proj: str) -> None:
|
|
assert context.rbc_error is None, f"Unexpected error: {context.rbc_error}"
|
|
att = context.rbc_attachment
|
|
assert isinstance(att, dict), f"Expected dict, got {type(att)}"
|
|
assert att.get("project_name") == proj, (
|
|
f"Expected project_name '{proj}', got '{att.get('project_name')}'"
|
|
)
|
|
|
|
|
|
@when(
|
|
'remaining cov a validation is attached using project-plan scope "{proj}" "{plan}"'
|
|
)
|
|
def step_remaining_cov_attach_with_project_and_plan(
|
|
context: Context,
|
|
proj: str,
|
|
plan: str,
|
|
) -> None:
|
|
try:
|
|
context.rbc_attachment = context.rbc_val_repo.attach(
|
|
validation_name="local/check-proj-plan",
|
|
resource_id="res-proj-plan-1",
|
|
mode="informational",
|
|
project_name=proj,
|
|
plan_id=plan,
|
|
)
|
|
context.rbc_error = None
|
|
except Exception as exc:
|
|
context.rbc_error = exc
|
|
|
|
|
|
@then('remaining cov the returned attachment has project "{proj}" and plan "{plan}"')
|
|
def step_remaining_cov_assert_attach_project_plan(
|
|
context: Context,
|
|
proj: str,
|
|
plan: str,
|
|
) -> None:
|
|
assert context.rbc_error is None, f"Unexpected error: {context.rbc_error}"
|
|
att = context.rbc_attachment
|
|
assert isinstance(att, dict), f"Expected dict, got {type(att)}"
|
|
assert att.get("project_name") == proj, (
|
|
f"Expected project_name '{proj}', got '{att.get('project_name')}'"
|
|
)
|
|
assert att.get("plan_id") == plan, (
|
|
f"Expected plan_id '{plan}', got '{att.get('plan_id')}'"
|
|
)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
# LifecyclePlanRepository.update: multiple project links (branch 1395→1394)
|
|
# ═══════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@given("remaining cov an in-memory lifecycle plan database")
|
|
def step_remaining_cov_plan_db(context: Context) -> None:
|
|
engine, factory = _make_engine_and_factory()
|
|
session = factory()
|
|
context.rbc_engine = engine
|
|
context.rbc_plan_factory = factory
|
|
context.rbc_plan_session = session
|
|
# Use a single-session factory so flush + commit are visible
|
|
context.rbc_plan_session_factory = lambda: session
|
|
|
|
from cleveragents.infrastructure.database.repositories import (
|
|
ActionRepository,
|
|
LifecyclePlanRepository,
|
|
)
|
|
|
|
context.rbc_action_repo = ActionRepository(
|
|
session_factory=context.rbc_plan_session_factory,
|
|
)
|
|
context.rbc_plan_repo = LifecyclePlanRepository(
|
|
session_factory=context.rbc_plan_session_factory,
|
|
)
|
|
context.rbc_error = None
|
|
|
|
|
|
@given('remaining cov a plan with action "{action_name}" exists')
|
|
def step_remaining_cov_plan_exists(context: Context, action_name: str) -> None:
|
|
from ulid import ULID
|
|
|
|
from cleveragents.domain.models.core.action import Action, ActionState
|
|
from cleveragents.domain.models.core.plan import (
|
|
NamespacedName,
|
|
Plan,
|
|
PlanIdentity,
|
|
PlanPhase,
|
|
PlanTimestamps,
|
|
ProcessingState,
|
|
)
|
|
|
|
parts = action_name.split("/", 1)
|
|
namespace = parts[0] if len(parts) == 2 else "local"
|
|
short = parts[1] if len(parts) == 2 else parts[0]
|
|
|
|
action = Action(
|
|
namespaced_name=NamespacedName(namespace=namespace, name=short),
|
|
description="test action",
|
|
long_description=None,
|
|
definition_of_done="verify it works",
|
|
strategy_actor="local/strategist",
|
|
execution_actor="local/executor",
|
|
estimation_actor=None,
|
|
review_actor=None,
|
|
arguments=[],
|
|
reusable=True,
|
|
read_only=False,
|
|
state=ActionState("available"),
|
|
created_at=datetime.now(),
|
|
updated_at=datetime.now(),
|
|
created_by=None,
|
|
tags=[],
|
|
)
|
|
context.rbc_action_repo.create(action)
|
|
context.rbc_plan_session.commit()
|
|
|
|
now = datetime.now()
|
|
plan = Plan(
|
|
identity=PlanIdentity(plan_id=str(ULID()), attempt=1),
|
|
namespaced_name=NamespacedName(namespace="local", name="multi-link-plan"),
|
|
action_name=action_name,
|
|
description="plan for multi-link test",
|
|
definition_of_done="verify links",
|
|
phase=PlanPhase.STRATEGIZE,
|
|
processing_state=ProcessingState.QUEUED,
|
|
strategy_actor="local/strategist",
|
|
execution_actor="local/executor",
|
|
timestamps=PlanTimestamps(created_at=now, updated_at=now),
|
|
error_message=None,
|
|
error_details=None,
|
|
created_by=None,
|
|
tags=[],
|
|
reusable=True,
|
|
read_only=False,
|
|
)
|
|
context.rbc_plan_repo.create(plan)
|
|
context.rbc_plan_session.commit()
|
|
context.rbc_plan = plan
|
|
|
|
|
|
@when("remaining cov the plan is updated with 3 project links")
|
|
def step_remaining_cov_plan_update_links(context: Context) -> None:
|
|
from cleveragents.domain.models.core.plan import (
|
|
PlanTimestamps,
|
|
ProjectLink,
|
|
)
|
|
|
|
plan = context.rbc_plan
|
|
updated = plan.model_copy(
|
|
update={
|
|
"project_links": [
|
|
ProjectLink(
|
|
project_name="local/proj-alpha", alias="alpha", read_only=False
|
|
),
|
|
ProjectLink(
|
|
project_name="local/proj-beta", alias="beta", read_only=True
|
|
),
|
|
ProjectLink(
|
|
project_name="local/proj-gamma", alias=None, read_only=False
|
|
),
|
|
],
|
|
"timestamps": PlanTimestamps(
|
|
created_at=plan.timestamps.created_at,
|
|
updated_at=datetime.now(),
|
|
),
|
|
}
|
|
)
|
|
try:
|
|
context.rbc_plan_repo.update(updated)
|
|
context.rbc_plan_session.commit()
|
|
context.rbc_plan = updated
|
|
context.rbc_error = None
|
|
except Exception as exc:
|
|
context.rbc_error = exc
|
|
|
|
|
|
@then("remaining cov the plan should have 3 project links after retrieval")
|
|
def step_remaining_cov_assert_plan_links(context: Context) -> None:
|
|
assert context.rbc_error is None, f"Unexpected error: {context.rbc_error}"
|
|
plan_id = context.rbc_plan.identity.plan_id
|
|
retrieved = context.rbc_plan_repo.get(plan_id)
|
|
assert retrieved is not None, f"Plan '{plan_id}' not found after update"
|
|
assert len(retrieved.project_links) == 3, (
|
|
f"Expected 3 project links, got {len(retrieved.project_links)}"
|
|
)
|
|
link_names = sorted(pl.project_name for pl in retrieved.project_links)
|
|
assert link_names == sorted(
|
|
[
|
|
"local/proj-alpha",
|
|
"local/proj-beta",
|
|
"local/proj-gamma",
|
|
]
|
|
), f"Unexpected link names: {link_names}"
|