Files
temp/features/steps/repositories_remaining_branches_coverage_steps.py
freemo 2eb31a598c test(coverage): add Behave BDD tests for 8 under-covered modules
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
2026-03-01 03:09:51 +00:00

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}"