759 lines
25 KiB
Python
759 lines
25 KiB
Python
"""Step definitions for plan lifecycle coverage scenarios.
|
|
|
|
Targets uncovered lines in:
|
|
- repositories.py: get_by_name, update (children), delete, list_plans filters,
|
|
count, error paths, action update with inputs_schema/arguments/invariants
|
|
- plan_lifecycle_service.py: get_plan from persistence fallback,
|
|
constrain_apply, auto_progress no-op, _persist_action_update (archive)
|
|
- unit_of_work.py: lifecycle_plans lazy property, add/flush/refresh
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
from typing import Any
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
from sqlalchemy import create_engine, event
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
|
|
from cleveragents.application.services.plan_lifecycle_service import (
|
|
PlanLifecycleService,
|
|
)
|
|
from cleveragents.config.settings import Settings
|
|
from cleveragents.domain.models.core.action import (
|
|
ActionArgument,
|
|
ArgumentRequirement,
|
|
ArgumentType,
|
|
)
|
|
from cleveragents.domain.models.core.plan import (
|
|
InvariantSource,
|
|
PlanInvariant,
|
|
PlanPhase,
|
|
ProcessingState,
|
|
ProjectLink,
|
|
)
|
|
from cleveragents.infrastructure.database.models import (
|
|
Base,
|
|
LifecyclePlanModel,
|
|
)
|
|
from cleveragents.infrastructure.database.repositories import (
|
|
DuplicatePlanError,
|
|
LifecyclePlanRepository,
|
|
PlanNotFoundError,
|
|
)
|
|
from cleveragents.infrastructure.database.unit_of_work import UnitOfWork
|
|
|
|
# ── Helpers ──
|
|
|
|
|
|
def _build_cov_uow() -> tuple[UnitOfWork, sessionmaker[Session], Any]:
|
|
"""Build an in-memory UoW for coverage testing."""
|
|
engine = create_engine(
|
|
"sqlite:///:memory:",
|
|
echo=False,
|
|
future=True,
|
|
connect_args={"check_same_thread": False},
|
|
)
|
|
|
|
@event.listens_for(engine, "connect")
|
|
def _fk(dbapi_conn: Any, _rec: Any) -> None:
|
|
cursor = dbapi_conn.cursor()
|
|
cursor.execute("PRAGMA foreign_keys=ON")
|
|
cursor.close()
|
|
|
|
Base.metadata.create_all(engine)
|
|
sf: sessionmaker[Session] = sessionmaker(
|
|
bind=engine,
|
|
expire_on_commit=False,
|
|
autoflush=False,
|
|
autocommit=False,
|
|
class_=Session,
|
|
)
|
|
|
|
uow = UnitOfWork.__new__(UnitOfWork)
|
|
uow.database_url = "sqlite:///:memory:"
|
|
uow._engine = engine
|
|
uow._session_factory = sf
|
|
uow._database_initialized = True
|
|
uow._prompt_for_migration = None
|
|
|
|
return uow, sf, engine
|
|
|
|
|
|
# ── Background ──
|
|
|
|
|
|
@given("a coverage test service backed by SQLite")
|
|
def step_cov_bg(context: Context) -> None:
|
|
uow, sf, engine = _build_cov_uow()
|
|
settings = Settings()
|
|
service = PlanLifecycleService(settings=settings, unit_of_work=uow)
|
|
context.cov_service = service
|
|
context.cov_uow = uow
|
|
context.cov_sf = sf
|
|
context.cov_engine = engine
|
|
context.cov_plan = None
|
|
context.cov_error = None
|
|
context.cov_result = None
|
|
context.cov_action_name = None
|
|
|
|
|
|
# ── Helpers for creating plans ──
|
|
|
|
|
|
def _create_cov_plan(context: Context) -> Any:
|
|
"""Create a plan via the service and store it on context."""
|
|
svc: PlanLifecycleService = context.cov_service
|
|
action_name = "local/cov-basic"
|
|
with contextlib.suppress(Exception):
|
|
svc.create_action(
|
|
name=action_name,
|
|
description="Coverage basic action",
|
|
definition_of_done="Tests pass",
|
|
strategy_actor="openai/gpt-4",
|
|
execution_actor="openai/gpt-4",
|
|
reusable=True,
|
|
)
|
|
plan = svc.use_action(action_name=action_name)
|
|
context.cov_plan = plan
|
|
context.cov_action_name = action_name
|
|
return plan
|
|
|
|
|
|
@given("a coverage plan exists via the service")
|
|
def step_cov_plan_exists(context: Context) -> None:
|
|
_create_cov_plan(context)
|
|
|
|
|
|
@given("a coverage plan with project links and invariants exists")
|
|
def step_cov_plan_with_children(context: Context) -> None:
|
|
svc: PlanLifecycleService = context.cov_service
|
|
action_name = "local/cov-children"
|
|
with contextlib.suppress(Exception):
|
|
svc.create_action(
|
|
name=action_name,
|
|
description="Action with children",
|
|
definition_of_done="All green",
|
|
strategy_actor="openai/gpt-4",
|
|
execution_actor="openai/gpt-4",
|
|
invariants=["Code must compile", "No regressions"],
|
|
reusable=True,
|
|
)
|
|
|
|
links = [
|
|
ProjectLink(project_name="local/api-service", alias="api", read_only=False),
|
|
ProjectLink(project_name="local/frontend", alias="web", read_only=True),
|
|
]
|
|
invariants = [
|
|
PlanInvariant(text="Security audit required", source=InvariantSource.PLAN),
|
|
]
|
|
plan = svc.use_action(
|
|
action_name=action_name,
|
|
project_links=links,
|
|
invariants=invariants,
|
|
arguments={},
|
|
)
|
|
context.cov_plan = plan
|
|
context.cov_action_name = action_name
|
|
|
|
|
|
@given("a coverage plan in apply-queued state")
|
|
def step_cov_plan_apply_queued(context: Context) -> None:
|
|
svc: PlanLifecycleService = context.cov_service
|
|
action_name = "local/cov-apply-q"
|
|
with contextlib.suppress(Exception):
|
|
svc.create_action(
|
|
name=action_name,
|
|
description="Apply test action",
|
|
definition_of_done="Done",
|
|
strategy_actor="openai/gpt-4",
|
|
execution_actor="openai/gpt-4",
|
|
reusable=True,
|
|
)
|
|
plan = svc.use_action(action_name=action_name)
|
|
pid = plan.identity.plan_id
|
|
svc.start_strategize(pid)
|
|
svc.complete_strategize(pid)
|
|
svc.execute_plan(pid)
|
|
svc.start_execute(pid)
|
|
svc.complete_execute(pid)
|
|
svc.apply_plan(pid)
|
|
context.cov_plan = svc.get_plan(pid)
|
|
|
|
|
|
@given('a coverage action "{name}" exists')
|
|
def step_cov_action_exists(context: Context, name: str) -> None:
|
|
svc: PlanLifecycleService = context.cov_service
|
|
svc.create_action(
|
|
name=name,
|
|
description=f"Coverage action {name}",
|
|
definition_of_done="Tests pass",
|
|
strategy_actor="openai/gpt-4",
|
|
execution_actor="openai/gpt-4",
|
|
reusable=True,
|
|
)
|
|
|
|
|
|
@given("a coverage action with inputs_schema and arguments exists")
|
|
def step_cov_action_with_inputs(context: Context) -> None:
|
|
svc: PlanLifecycleService = context.cov_service
|
|
args = [
|
|
ActionArgument(
|
|
name="target_dir",
|
|
arg_type=ArgumentType.STRING,
|
|
requirement=ArgumentRequirement.REQUIRED,
|
|
description="Target directory",
|
|
default_value="/tmp",
|
|
),
|
|
ActionArgument(
|
|
name="max_retries",
|
|
arg_type=ArgumentType.INTEGER,
|
|
requirement=ArgumentRequirement.OPTIONAL,
|
|
description="Max retries",
|
|
default_value=3,
|
|
),
|
|
]
|
|
svc.create_action(
|
|
name="local/cov-inputs-act",
|
|
description="Action with inputs",
|
|
definition_of_done="Done",
|
|
strategy_actor="openai/gpt-4",
|
|
execution_actor="openai/gpt-4",
|
|
arguments=args,
|
|
invariants=["Must not break API", "Must be backwards compatible"],
|
|
reusable=True,
|
|
tags=["test", "coverage"],
|
|
)
|
|
# Manually set inputs_schema on the action to cover line 924
|
|
action = svc.get_action("local/cov-inputs-act")
|
|
action.inputs_schema = {
|
|
"type": "object",
|
|
"properties": {"target_dir": {"type": "string"}},
|
|
}
|
|
context.cov_action_name = "local/cov-inputs-act"
|
|
|
|
|
|
# ── Get plan by name ──
|
|
|
|
|
|
@when("I retrieve the plan by its namespaced name from the repository")
|
|
def step_get_by_name(context: Context) -> None:
|
|
plan = context.cov_plan
|
|
name = str(plan.namespaced_name)
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = LifecyclePlanRepository(session_factory=lambda: session)
|
|
context.cov_result = repo.get_by_name(name)
|
|
session.close()
|
|
|
|
|
|
@then("the plan retrieved by name should match the original")
|
|
def step_verify_get_by_name(context: Context) -> None:
|
|
result = context.cov_result
|
|
assert result is not None, "get_by_name returned None"
|
|
assert result.identity.plan_id == context.cov_plan.identity.plan_id
|
|
|
|
|
|
@when('I retrieve a plan by name "{name}" from the repository')
|
|
def step_get_by_name_unknown(context: Context, name: str) -> None:
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = LifecyclePlanRepository(session_factory=lambda: session)
|
|
context.cov_result = repo.get_by_name(name)
|
|
session.close()
|
|
|
|
|
|
@then("the plan-by-name result should be None")
|
|
def step_verify_get_by_name_none(context: Context) -> None:
|
|
assert context.cov_result is None, f"Expected None, got {context.cov_result}"
|
|
|
|
|
|
# ── Update plan with children ──
|
|
|
|
|
|
@when("I update the plan description and phase through the repository")
|
|
def step_update_plan(context: Context) -> None:
|
|
plan = context.cov_plan
|
|
plan.description = "Updated description for coverage"
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = LifecyclePlanRepository(session_factory=lambda: session)
|
|
repo.update(plan)
|
|
session.commit()
|
|
session.close()
|
|
|
|
|
|
@then("the updated plan should have the new description in the DB")
|
|
def step_verify_updated_desc(context: Context) -> None:
|
|
plan_id = context.cov_plan.identity.plan_id
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = LifecyclePlanRepository(session_factory=lambda: session)
|
|
loaded = repo.get(plan_id)
|
|
session.close()
|
|
assert loaded is not None
|
|
assert loaded.description == "Updated description for coverage"
|
|
|
|
|
|
@then("the updated plan should retain project links")
|
|
def step_verify_project_links(context: Context) -> None:
|
|
plan_id = context.cov_plan.identity.plan_id
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = LifecyclePlanRepository(session_factory=lambda: session)
|
|
loaded = repo.get(plan_id)
|
|
session.close()
|
|
assert loaded is not None
|
|
links = getattr(loaded, "project_links", []) or []
|
|
assert len(links) >= 2, f"Expected >= 2 project links, got {len(links)}"
|
|
|
|
|
|
@then("the updated plan should retain arguments")
|
|
def step_verify_arguments(context: Context) -> None:
|
|
plan_id = context.cov_plan.identity.plan_id
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = LifecyclePlanRepository(session_factory=lambda: session)
|
|
loaded = repo.get(plan_id)
|
|
session.close()
|
|
assert loaded is not None
|
|
# Arguments dict may be empty, but it should be present
|
|
args = getattr(loaded, "arguments", None)
|
|
assert args is not None, "arguments should not be None"
|
|
|
|
|
|
@then("the updated plan should retain invariants")
|
|
def step_verify_invariants(context: Context) -> None:
|
|
plan_id = context.cov_plan.identity.plan_id
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = LifecyclePlanRepository(session_factory=lambda: session)
|
|
loaded = repo.get(plan_id)
|
|
session.close()
|
|
assert loaded is not None
|
|
invs = getattr(loaded, "invariants", []) or []
|
|
# Should have action invariants + plan invariants
|
|
assert len(invs) >= 1, f"Expected >= 1 invariants, got {len(invs)}"
|
|
|
|
|
|
# ── Delete plan ──
|
|
|
|
|
|
@when("I delete the plan from the repository")
|
|
def step_delete_plan(context: Context) -> None:
|
|
plan_id = context.cov_plan.identity.plan_id
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = LifecyclePlanRepository(session_factory=lambda: session)
|
|
context.cov_result = repo.delete(plan_id)
|
|
session.commit()
|
|
session.close()
|
|
|
|
|
|
@then("the plan should no longer be retrievable from the DB")
|
|
def step_verify_deleted(context: Context) -> None:
|
|
assert context.cov_result is True, "delete should return True"
|
|
plan_id = context.cov_plan.identity.plan_id
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = LifecyclePlanRepository(session_factory=lambda: session)
|
|
loaded = repo.get(plan_id)
|
|
session.close()
|
|
assert loaded is None, "Plan should be deleted"
|
|
|
|
|
|
@when('I delete a non-existent plan "{plan_id}" from the repository')
|
|
def step_delete_nonexistent(context: Context, plan_id: str) -> None:
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = LifecyclePlanRepository(session_factory=lambda: session)
|
|
context.cov_result = repo.delete(plan_id)
|
|
session.close()
|
|
|
|
|
|
@then("the delete result should be false")
|
|
def step_verify_delete_false(context: Context) -> None:
|
|
assert context.cov_result is False, f"Expected False, got {context.cov_result}"
|
|
|
|
|
|
# ── List plans with filters ──
|
|
|
|
|
|
@when('I list plans filtered by phase "{phase}"')
|
|
def step_list_by_phase(context: Context, phase: str) -> None:
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = LifecyclePlanRepository(session_factory=lambda: session)
|
|
context.cov_result = repo.list_plans(phase=phase)
|
|
session.close()
|
|
|
|
|
|
@when('I list plans filtered by namespace "{namespace}"')
|
|
def step_list_by_namespace(context: Context, namespace: str) -> None:
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = LifecyclePlanRepository(session_factory=lambda: session)
|
|
context.cov_result = repo.list_plans(namespace=namespace)
|
|
session.close()
|
|
|
|
|
|
@when("I list plans filtered by action_name")
|
|
def step_list_by_action_name(context: Context) -> None:
|
|
action_name = context.cov_action_name or str(context.cov_plan.action_name)
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = LifecyclePlanRepository(session_factory=lambda: session)
|
|
context.cov_result = repo.list_plans(action_name=action_name)
|
|
session.close()
|
|
|
|
|
|
@then("the listed plans should include the coverage plan")
|
|
def step_verify_listed(context: Context) -> None:
|
|
plans = context.cov_result
|
|
assert len(plans) >= 1, f"Expected >= 1 plans, got {len(plans)}"
|
|
plan_ids = [p.identity.plan_id for p in plans]
|
|
assert context.cov_plan.identity.plan_id in plan_ids
|
|
|
|
|
|
# ── Count plans ──
|
|
|
|
|
|
@when("I count plans with no filters")
|
|
def step_count_no_filter(context: Context) -> None:
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = LifecyclePlanRepository(session_factory=lambda: session)
|
|
context.cov_result = repo.count()
|
|
session.close()
|
|
|
|
|
|
@then("the count should be at least 1")
|
|
def step_verify_count(context: Context) -> None:
|
|
assert context.cov_result >= 1, f"Expected >= 1, got {context.cov_result}"
|
|
|
|
|
|
@when('I count plans filtered by phase "{phase}"')
|
|
def step_count_by_phase(context: Context, phase: str) -> None:
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = LifecyclePlanRepository(session_factory=lambda: session)
|
|
context.cov_result = repo.count(phase=phase)
|
|
session.close()
|
|
|
|
|
|
@when('I count plans filtered by processing_state "{state}"')
|
|
def step_count_by_state(context: Context, state: str) -> None:
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = LifecyclePlanRepository(session_factory=lambda: session)
|
|
context.cov_result = repo.count(processing_state=state)
|
|
session.close()
|
|
|
|
|
|
@then("the filtered count should be at least 1")
|
|
def step_verify_filtered_count(context: Context) -> None:
|
|
assert context.cov_result >= 1, f"Expected >= 1, got {context.cov_result}"
|
|
|
|
|
|
# ── Duplicate plan error ──
|
|
|
|
|
|
@when("I try to create a duplicate plan with the same ID")
|
|
def step_create_duplicate_plan(context: Context) -> None:
|
|
plan = context.cov_plan
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = LifecyclePlanRepository(session_factory=lambda: session)
|
|
try:
|
|
repo.create(plan)
|
|
session.commit()
|
|
context.cov_error = None
|
|
except DuplicatePlanError as exc:
|
|
context.cov_error = exc
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
@then("a DuplicatePlanError should be raised")
|
|
def step_verify_duplicate_error(context: Context) -> None:
|
|
assert context.cov_error is not None, "Expected DuplicatePlanError"
|
|
assert isinstance(context.cov_error, DuplicatePlanError)
|
|
|
|
|
|
# ── Update non-existent plan error ──
|
|
|
|
|
|
@when("I try to update a plan that does not exist in the DB")
|
|
def step_update_nonexistent_plan(context: Context) -> None:
|
|
from datetime import datetime
|
|
|
|
from cleveragents.domain.models.core.plan import (
|
|
NamespacedName,
|
|
Plan,
|
|
PlanIdentity,
|
|
PlanTimestamps,
|
|
)
|
|
|
|
fake_plan = Plan(
|
|
identity=PlanIdentity(plan_id="01ZZZZZZZZZZZZZZZZZZZZZZZZ"),
|
|
namespaced_name=NamespacedName(
|
|
server=None, namespace="local", name="fake-plan"
|
|
),
|
|
action_name="local/fake-action",
|
|
description="Fake plan",
|
|
definition_of_done="Never",
|
|
phase=PlanPhase.STRATEGIZE,
|
|
processing_state=ProcessingState.QUEUED,
|
|
strategy_actor="openai/gpt-4",
|
|
execution_actor="openai/gpt-4",
|
|
timestamps=PlanTimestamps(
|
|
created_at=datetime.now(),
|
|
updated_at=datetime.now(),
|
|
),
|
|
reusable=True,
|
|
read_only=False,
|
|
)
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = LifecyclePlanRepository(session_factory=lambda: session)
|
|
try:
|
|
repo.update(fake_plan)
|
|
context.cov_error = None
|
|
except PlanNotFoundError as exc:
|
|
context.cov_error = exc
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
@then("a PlanNotFoundError should be raised")
|
|
def step_verify_not_found_error(context: Context) -> None:
|
|
assert context.cov_error is not None, "Expected PlanNotFoundError"
|
|
assert isinstance(context.cov_error, PlanNotFoundError)
|
|
|
|
|
|
# ── Archive action (triggers ActionRepository.update) ──
|
|
|
|
|
|
@when('I archive the coverage action "{name}"')
|
|
def step_archive_action(context: Context, name: str) -> None:
|
|
svc: PlanLifecycleService = context.cov_service
|
|
context.cov_result = svc.archive_action(name)
|
|
|
|
|
|
@then('the action "{name}" should be archived in the DB')
|
|
def step_verify_archived(context: Context, name: str) -> None:
|
|
from cleveragents.infrastructure.database.repositories import ActionRepository
|
|
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = ActionRepository(session_factory=lambda: session)
|
|
action = repo.get_by_name(name)
|
|
session.close()
|
|
assert action is not None, f"Action {name} not found"
|
|
assert action.state.value == "archived" or str(action.state) == "archived"
|
|
|
|
|
|
# ── Action update with inputs_schema/arguments/invariants ──
|
|
|
|
|
|
@when("I update the coverage action with new description")
|
|
def step_update_action(context: Context) -> None:
|
|
svc: PlanLifecycleService = context.cov_service
|
|
action = svc.get_action("local/cov-inputs-act")
|
|
action.description = "Updated description"
|
|
# Persist via archive (which calls update in the repository)
|
|
# Instead, let's call update directly via UoW to hit the exact lines
|
|
with context.cov_uow.transaction() as ctx:
|
|
ctx.actions.update(action)
|
|
|
|
|
|
@then("the action should retain its inputs_schema in the DB")
|
|
def step_verify_inputs_schema(context: Context) -> None:
|
|
from cleveragents.infrastructure.database.repositories import ActionRepository
|
|
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = ActionRepository(session_factory=lambda: session)
|
|
action = repo.get_by_name("local/cov-inputs-act")
|
|
session.close()
|
|
assert action is not None
|
|
assert action.inputs_schema is not None, "inputs_schema should be set"
|
|
|
|
|
|
@then("the action should retain its arguments with defaults in the DB")
|
|
def step_verify_action_args(context: Context) -> None:
|
|
from cleveragents.infrastructure.database.repositories import ActionRepository
|
|
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = ActionRepository(session_factory=lambda: session)
|
|
action = repo.get_by_name("local/cov-inputs-act")
|
|
session.close()
|
|
assert action is not None
|
|
assert len(action.arguments) >= 2, (
|
|
f"Expected >= 2 args, got {len(action.arguments)}"
|
|
)
|
|
# Check default values were preserved
|
|
defaults = [
|
|
a.default_value for a in action.arguments if a.default_value is not None
|
|
]
|
|
assert len(defaults) >= 1, "Should have at least 1 argument with default"
|
|
|
|
|
|
@then("the action should retain its invariants in the DB")
|
|
def step_verify_action_invariants(context: Context) -> None:
|
|
from cleveragents.infrastructure.database.repositories import ActionRepository
|
|
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = ActionRepository(session_factory=lambda: session)
|
|
action = repo.get_by_name("local/cov-inputs-act")
|
|
session.close()
|
|
assert action is not None
|
|
assert len(action.invariants) >= 2, (
|
|
f"Expected >= 2 invariants, got {len(action.invariants)}"
|
|
)
|
|
|
|
|
|
# ── Service get_plan from persistence fallback ──
|
|
|
|
|
|
@when("I clear the in-memory plan cache")
|
|
def step_clear_cache(context: Context) -> None:
|
|
svc: PlanLifecycleService = context.cov_service
|
|
# Clear in-memory plans dict to force fallback to persistence
|
|
svc._plans.clear()
|
|
|
|
|
|
@when("I retrieve the plan by ID through the service")
|
|
def step_get_plan_by_id(context: Context) -> None:
|
|
svc: PlanLifecycleService = context.cov_service
|
|
plan_id = context.cov_plan.identity.plan_id
|
|
context.cov_result = svc.get_plan(plan_id)
|
|
|
|
|
|
@then("the plan should be loaded from the persistence layer")
|
|
def step_verify_from_persistence(context: Context) -> None:
|
|
result = context.cov_result
|
|
assert result is not None, "Plan not loaded from persistence"
|
|
assert result.identity.plan_id == context.cov_plan.identity.plan_id
|
|
|
|
|
|
# ── Constrain apply ──
|
|
|
|
|
|
@when('I constrain the apply with reason "{reason}"')
|
|
def step_constrain_apply(context: Context, reason: str) -> None:
|
|
svc: PlanLifecycleService = context.cov_service
|
|
plan_id = context.cov_plan.identity.plan_id
|
|
# start the apply phase first
|
|
svc.start_apply(plan_id)
|
|
context.cov_plan = svc.constrain_apply(plan_id, reason)
|
|
|
|
|
|
@then('the coverage plan processing state should be "{state}"')
|
|
def step_cov_check_proc_state(context: Context, state: str) -> None:
|
|
assert context.cov_plan.processing_state.value == state, (
|
|
f"Expected {state}, got {context.cov_plan.processing_state.value}"
|
|
)
|
|
|
|
|
|
@then('the coverage plan error message should be "{message}"')
|
|
def step_cov_check_error_msg(context: Context, message: str) -> None:
|
|
assert context.cov_plan.error_message == message, (
|
|
f"Expected '{message}', got '{context.cov_plan.error_message}'"
|
|
)
|
|
|
|
|
|
# ── Auto progress no-op ──
|
|
|
|
|
|
@when("I call auto_progress on the plan")
|
|
def step_auto_progress(context: Context) -> None:
|
|
svc: PlanLifecycleService = context.cov_service
|
|
plan_id = context.cov_plan.identity.plan_id
|
|
context.cov_result = svc.auto_progress(plan_id)
|
|
|
|
|
|
@then("the plan should be returned unchanged")
|
|
def step_verify_unchanged(context: Context) -> None:
|
|
result = context.cov_result
|
|
assert result is not None
|
|
assert result.identity.plan_id == context.cov_plan.identity.plan_id
|
|
# Phase should still be strategize/queued
|
|
assert result.phase == PlanPhase.STRATEGIZE
|
|
|
|
|
|
# ── UoW lifecycle_plans property ──
|
|
|
|
|
|
@when("I open a UoW transaction and access lifecycle_plans")
|
|
def step_uow_lifecycle_plans(context: Context) -> None:
|
|
uow = context.cov_uow
|
|
with uow.transaction() as ctx:
|
|
context.cov_result = ctx.lifecycle_plans
|
|
|
|
|
|
@then("the lifecycle_plans property should return a LifecyclePlanRepository")
|
|
def step_verify_lifecycle_plans_type(context: Context) -> None:
|
|
assert isinstance(context.cov_result, LifecyclePlanRepository)
|
|
|
|
|
|
# ── UoW add/flush ──
|
|
|
|
|
|
@when("I use UoW context to add a raw model entity and flush")
|
|
def step_uow_add_flush(context: Context) -> None:
|
|
from datetime import datetime
|
|
|
|
# First, create an action (to satisfy FK constraint on v3_plans.action_name)
|
|
svc: PlanLifecycleService = context.cov_service
|
|
with contextlib.suppress(Exception):
|
|
svc.create_action(
|
|
name="local/uow-flush-test",
|
|
description="UoW flush test action",
|
|
definition_of_done="Done",
|
|
strategy_actor="openai/gpt-4",
|
|
execution_actor="openai/gpt-4",
|
|
reusable=True,
|
|
)
|
|
|
|
uow = context.cov_uow
|
|
with uow.transaction() as ctx:
|
|
# Create a raw model and add it via the context
|
|
model = LifecyclePlanModel()
|
|
model.plan_id = "01TESTADD00000000000000000" # type: ignore[assignment]
|
|
model.namespaced_name = "local/uow-add-test" # type: ignore[assignment]
|
|
model.namespace = "local" # type: ignore[assignment]
|
|
model.name = "uow-add-test" # type: ignore[assignment]
|
|
model.action_name = "local/uow-flush-test" # type: ignore[assignment]
|
|
model.description = "UoW add test" # type: ignore[assignment]
|
|
model.definition_of_done = "Pass" # type: ignore[assignment]
|
|
model.phase = "strategize" # type: ignore[assignment]
|
|
model.processing_state = "queued" # type: ignore[assignment]
|
|
model.attempt = 1 # type: ignore[assignment]
|
|
model.strategy_actor = "openai/gpt-4" # type: ignore[assignment]
|
|
model.execution_actor = "openai/gpt-4" # type: ignore[assignment]
|
|
model.reusable = True # type: ignore[assignment]
|
|
model.read_only = False # type: ignore[assignment]
|
|
model.tags_json = "[]" # type: ignore[assignment]
|
|
model.sandbox_refs_json = "[]" # type: ignore[assignment]
|
|
now = datetime.now().isoformat()
|
|
model.created_at = now # type: ignore[assignment]
|
|
model.updated_at = now # type: ignore[assignment]
|
|
ctx.add(model)
|
|
ctx.flush()
|
|
context.cov_result = model.plan_id
|
|
|
|
|
|
@then("the entity should be queryable in the same session")
|
|
def step_verify_entity_added(context: Context) -> None:
|
|
sf = context.cov_sf
|
|
session = sf()
|
|
repo = LifecyclePlanRepository(session_factory=lambda: session)
|
|
plan = repo.get("01TESTADD00000000000000000")
|
|
session.close()
|
|
assert plan is not None, "Added entity should be queryable"
|