9e316b1a3e
CI / build (pull_request) Successful in 22s
CI / typecheck (pull_request) Successful in 47s
CI / lint (pull_request) Successful in 3m17s
CI / quality (pull_request) Successful in 3m33s
CI / security (pull_request) Successful in 3m59s
CI / unit_tests (pull_request) Successful in 5m31s
CI / docker (pull_request) Successful in 57s
CI / coverage (pull_request) Successful in 12m32s
CI / status-check (pull_request) Successful in 1s
CI / integration_tests (pull_request) Successful in 2m25s
CI / e2e_tests (pull_request) Successful in 7m9s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (push) Successful in 26s
CI / lint (push) Successful in 3m18s
CI / typecheck (push) Successful in 3m54s
CI / security (push) Successful in 4m2s
CI / quality (push) Successful in 3m33s
CI / integration_tests (push) Successful in 5m47s
CI / unit_tests (push) Successful in 7m6s
CI / docker (push) Successful in 39s
CI / benchmark-regression (pull_request) Failing after 21m11s
CI / coverage (push) Failing after 19m3s
CI / benchmark-regression (push) Has been skipped
CI / e2e_tests (push) Failing after 21m14s
CI / status-check (push) Failing after 1s
CI / benchmark-publish (push) Failing after 18m51s
Aligned the plan lifecycle model with the specification: 1. ERRORED is now treated as terminal in is_terminal property, matching the spec table where errored is marked "Terminal? Yes" for all processing phases. 2. Added per-phase state validation via model_validator: APPLIED and CONSTRAINED are only valid in APPLY phase; COMPLETE is only valid in STRATEGIZE or EXECUTE phases. Invalid combinations now raise ValueError at construction time. 3. Updated ProcessingState.COMPLETE docstring to clarify phase-level terminality semantics. 4. Fixed assignment ordering in execute_plan() to set processing_state before phase, consistent with the state-first pattern used in apply_plan() and _perform_reversion(). 5. Added defensive coercion in LifecyclePlanModel.to_domain() to handle legacy DB rows with invalid phase/state combinations (e.g. APPLY/COMPLETE -> APPLY/APPLIED) with warning-level logging for observability. 6. Updated module docstrings: ERRORED description now reflects terminal semantics, terminal outcomes location clarified for all phases, can_revert_to docstring notes ERRORED/CONSTRAINED are terminal but revertable, is_terminal docstring explains the distinction between terminal and permanently irrecoverable and documents why COMPLETE is not plan-terminal despite the spec marking it "Terminal? Yes" (phase-level vs plan-level). 7. Updated PlanResumeService.validate_eligibility() docstring to reflect that ERRORED is now terminal but still eligible for resume. 8. Added CHANGELOG entry. ISSUES CLOSED: #918
579 lines
20 KiB
Python
579 lines
20 KiB
Python
"""Step definitions for plan persistence feature.
|
|
|
|
Tests LifecyclePlanRepository CRUD, phase/state transitions,
|
|
list filters, plan tree links, and cross-restart durability.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import tempfile
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
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.domain.models.core.action import Action, ActionState
|
|
from cleveragents.domain.models.core.plan import (
|
|
InvariantSource,
|
|
NamespacedName,
|
|
Plan,
|
|
PlanIdentity,
|
|
PlanInvariant,
|
|
PlanPhase,
|
|
PlanTimestamps,
|
|
ProcessingState,
|
|
ProjectLink,
|
|
)
|
|
from cleveragents.infrastructure.database.models import (
|
|
Base,
|
|
)
|
|
from cleveragents.infrastructure.database.repositories import (
|
|
ActionRepository,
|
|
LifecyclePlanRepository,
|
|
)
|
|
|
|
_CB32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
_counter = 0
|
|
|
|
|
|
def _next_ulid() -> str:
|
|
"""Generate a monotonically-increasing ULID for test isolation."""
|
|
global _counter
|
|
_counter += 1
|
|
n = _counter
|
|
suffix = ""
|
|
for _ in range(8):
|
|
suffix = _CB32[n % 32] + suffix
|
|
n //= 32
|
|
return f"01HGZ6FE0AQDYTR4BX{suffix}"
|
|
|
|
|
|
def _make_plan(
|
|
plan_id: str,
|
|
action_name: str = "local/persist-action",
|
|
phase: PlanPhase = PlanPhase.ACTION,
|
|
processing_state: ProcessingState = ProcessingState.QUEUED,
|
|
parent_plan_id: str | None = None,
|
|
root_plan_id: str | None = None,
|
|
project_links: list[ProjectLink] | None = None,
|
|
arguments: dict[str, Any] | None = None,
|
|
arguments_order: list[str] | None = None,
|
|
invariants: list[PlanInvariant] | None = None,
|
|
) -> Plan:
|
|
"""Build a Plan domain object for testing."""
|
|
now = datetime(2026, 3, 1, tzinfo=UTC)
|
|
return Plan(
|
|
identity=PlanIdentity(
|
|
plan_id=plan_id,
|
|
parent_plan_id=parent_plan_id,
|
|
root_plan_id=root_plan_id,
|
|
attempt=1,
|
|
),
|
|
namespaced_name=NamespacedName(namespace="local", name="persist-plan"),
|
|
action_name=action_name,
|
|
description="Persistence test plan",
|
|
definition_of_done="All assertions pass",
|
|
phase=phase,
|
|
processing_state=processing_state,
|
|
strategy_actor="local/s",
|
|
execution_actor="local/e",
|
|
project_links=project_links or [],
|
|
invariants=invariants or [],
|
|
arguments=arguments or {},
|
|
arguments_order=arguments_order or [],
|
|
timestamps=PlanTimestamps(created_at=now, updated_at=now),
|
|
created_by="test-persist",
|
|
tags=[],
|
|
reusable=True,
|
|
read_only=False,
|
|
)
|
|
|
|
|
|
def _setup_db(context: Context, *, file_based: bool = False) -> None:
|
|
"""Create a SQLite DB and attach repos to context.
|
|
|
|
By default uses in-memory SQLite (fast). Pass ``file_based=True`` for
|
|
cross-restart scenarios that need to close and reopen the same database
|
|
file.
|
|
"""
|
|
if file_based:
|
|
tmp = tempfile.mktemp(suffix=".db")
|
|
db_url = f"sqlite:///{tmp}"
|
|
context._pp_db_path = tmp
|
|
else:
|
|
db_url = "sqlite://"
|
|
context._pp_db_path = None
|
|
engine = create_engine(db_url, echo=False)
|
|
Base.metadata.create_all(engine)
|
|
sm = sessionmaker(bind=engine)
|
|
session = sm()
|
|
context._pp_db_url = db_url
|
|
context._pp_engine = engine
|
|
context._pp_session = session
|
|
context._pp_session_factory = lambda: session
|
|
context._pp_plan_repo = LifecyclePlanRepository(
|
|
session_factory=context._pp_session_factory,
|
|
)
|
|
context._pp_action_repo = ActionRepository(
|
|
session_factory=context._pp_session_factory,
|
|
)
|
|
|
|
|
|
def _teardown_db(context: Context) -> None:
|
|
"""Close the session and clean up temp DB file."""
|
|
if hasattr(context, "_pp_session"):
|
|
context._pp_session.close()
|
|
if hasattr(context, "_pp_engine"):
|
|
context._pp_engine.dispose()
|
|
if getattr(context, "_pp_db_path", None):
|
|
Path(context._pp_db_path).unlink(missing_ok=True)
|
|
|
|
|
|
def _create_action(context: Context, action_name: str = "local/persist-action") -> None:
|
|
"""Ensure an action exists for FK constraint."""
|
|
ns = NamespacedName.parse(action_name)
|
|
action = Action(
|
|
namespaced_name=ns,
|
|
description="Prerequisite action for plan persistence tests",
|
|
definition_of_done="Done",
|
|
strategy_actor="local/s",
|
|
execution_actor="local/e",
|
|
state=ActionState.AVAILABLE,
|
|
created_at=datetime(2026, 1, 1, tzinfo=UTC),
|
|
updated_at=datetime(2026, 1, 1, tzinfo=UTC),
|
|
)
|
|
context._pp_action_repo.create(action)
|
|
context._pp_session.commit()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Background
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a fresh in-memory plan persistence database")
|
|
def step_fresh_plan_persistence_db(context: Context) -> None:
|
|
"""Set up a clean in-memory SQLite database for plan persistence tests."""
|
|
_setup_db(context)
|
|
if not hasattr(context, "_cleanup_handlers"):
|
|
context._cleanup_handlers = []
|
|
context._cleanup_handlers.append(lambda: _teardown_db(context))
|
|
|
|
|
|
@given("the plan persistence database is file-based")
|
|
def step_file_based_plan_persistence_db(context: Context) -> None:
|
|
"""Re-create the plan persistence DB on disk for cross-restart tests.
|
|
|
|
The Background already creates an in-memory DB. This step replaces it
|
|
with a file-backed DB so the "close and reopen" step can reopen the same
|
|
file after engine disposal.
|
|
"""
|
|
_teardown_db(context)
|
|
_setup_db(context, file_based=True)
|
|
if not hasattr(context, "_cleanup_handlers"):
|
|
context._cleanup_handlers = []
|
|
context._cleanup_handlers.append(lambda: _teardown_db(context))
|
|
|
|
|
|
@given('a prerequisite action "{action_name}" exists in the database')
|
|
def step_prerequisite_action_exists(context: Context, action_name: str) -> None:
|
|
"""Create a prerequisite action for FK constraints."""
|
|
_create_action(context, action_name)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Create scenarios
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given('a new lifecycle plan with ID "{plan_id}"')
|
|
def step_new_lifecycle_plan(context: Context, plan_id: str) -> None:
|
|
"""Build a new Plan domain object."""
|
|
context._pp_plan = _make_plan(plan_id)
|
|
|
|
|
|
@given(
|
|
'a new lifecycle plan with parent tree ID "{plan_id}" parent "{parent_id}" root "{root_id}"'
|
|
)
|
|
def step_new_lifecycle_plan_with_tree(
|
|
context: Context, plan_id: str, parent_id: str, root_id: str
|
|
) -> None:
|
|
"""Build a new Plan with parent/root IDs."""
|
|
# Ensure parent plan exists first
|
|
parent = _make_plan(parent_id)
|
|
context._pp_plan_repo.create(parent)
|
|
context._pp_session.commit()
|
|
context._pp_plan = _make_plan(
|
|
plan_id, parent_plan_id=parent_id, root_plan_id=root_id
|
|
)
|
|
|
|
|
|
@when("I persist the plan via the plan repository")
|
|
def step_persist_plan(context: Context) -> None:
|
|
"""Persist the plan through the repository."""
|
|
context._pp_plan_repo.create(context._pp_plan)
|
|
context._pp_session.commit()
|
|
|
|
|
|
@then('I can retrieve the plan by ID "{plan_id}"')
|
|
def step_retrieve_plan_by_id(context: Context, plan_id: str) -> None:
|
|
"""Retrieve the plan and store for further assertions."""
|
|
plan = context._pp_plan_repo.get(plan_id)
|
|
assert plan is not None, f"Plan {plan_id} not found"
|
|
context._pp_retrieved = plan
|
|
|
|
|
|
@then('the persisted plan description should be "{description}"')
|
|
def step_plan_description(context: Context, description: str) -> None:
|
|
"""Verify the plan's description."""
|
|
assert context._pp_retrieved.description == description
|
|
|
|
|
|
@then('the plan parent_plan_id should be "{parent_id}"')
|
|
def step_plan_parent_id(context: Context, parent_id: str) -> None:
|
|
"""Verify the plan's parent_plan_id."""
|
|
assert context._pp_retrieved.identity.parent_plan_id == parent_id
|
|
|
|
|
|
@then('the plan root_plan_id should be "{root_id}"')
|
|
def step_plan_root_id(context: Context, root_id: str) -> None:
|
|
"""Verify the plan's root_plan_id."""
|
|
assert context._pp_retrieved.identity.root_plan_id == root_id
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Phase/state transition scenarios
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given('a persisted plan in phase "{phase}" with state "{state}"')
|
|
def step_persisted_plan_phase_state(context: Context, phase: str, state: str) -> None:
|
|
"""Create and persist a plan in the given phase/state."""
|
|
plan_id = _next_ulid()
|
|
plan = _make_plan(
|
|
plan_id,
|
|
phase=PlanPhase(phase),
|
|
processing_state=ProcessingState(state),
|
|
)
|
|
context._pp_plan_repo.create(plan)
|
|
context._pp_session.commit()
|
|
context._pp_current_plan_id = plan_id
|
|
|
|
|
|
@when('I update the plan phase to "{phase}" with state "{state}"')
|
|
def step_update_plan_phase_state(context: Context, phase: str, state: str) -> None:
|
|
"""Update the plan's phase and processing state.
|
|
|
|
Sets processing_state before phase so that the per-phase state
|
|
validator sees a universally valid state (e.g. QUEUED) when the
|
|
phase assignment triggers re-validation.
|
|
"""
|
|
plan = context._pp_plan_repo.get(context._pp_current_plan_id)
|
|
assert plan is not None
|
|
plan.processing_state = ProcessingState(state)
|
|
plan.phase = PlanPhase(phase)
|
|
plan.timestamps.updated_at = datetime.now(tz=UTC)
|
|
context._pp_plan_repo.update(plan)
|
|
context._pp_session.commit()
|
|
|
|
|
|
@then('the plan persistence phase should be "{phase}"')
|
|
def step_verify_plan_phase(context: Context, phase: str) -> None:
|
|
"""Verify the persisted plan's phase."""
|
|
plan = context._pp_plan_repo.get(context._pp_current_plan_id)
|
|
assert plan is not None
|
|
assert plan.phase == PlanPhase(phase), f"Expected {phase}, got {plan.phase}"
|
|
|
|
|
|
@then('the persisted plan processing_state should be "{state}"')
|
|
def step_verify_plan_state(context: Context, state: str) -> None:
|
|
"""Verify the persisted plan's processing state."""
|
|
plan = context._pp_plan_repo.get(context._pp_current_plan_id)
|
|
assert plan is not None
|
|
assert plan.processing_state == ProcessingState(state), (
|
|
f"Expected {state}, got {plan.processing_state}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# List filter scenarios
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given('{count:d} persisted plans in phase "{phase}"')
|
|
def step_n_plans_in_phase(context: Context, count: int, phase: str) -> None:
|
|
"""Create N plans in the given phase."""
|
|
for _ in range(count):
|
|
plan_id = _next_ulid()
|
|
plan = _make_plan(plan_id, phase=PlanPhase(phase))
|
|
context._pp_plan_repo.create(plan)
|
|
context._pp_session.commit()
|
|
|
|
|
|
@given('{count:d} persisted plans with processing state "{state}"')
|
|
def step_n_plans_in_state(context: Context, count: int, state: str) -> None:
|
|
"""Create N plans with the given processing state."""
|
|
for _ in range(count):
|
|
plan_id = _next_ulid()
|
|
plan = _make_plan(plan_id, processing_state=ProcessingState(state))
|
|
context._pp_plan_repo.create(plan)
|
|
context._pp_session.commit()
|
|
|
|
|
|
@given('{count:d} persisted plan with processing state "{state}"')
|
|
def step_one_plan_in_state(context: Context, count: int, state: str) -> None:
|
|
"""Create 1 plan with the given processing state (singular)."""
|
|
for _ in range(count):
|
|
plan_id = _next_ulid()
|
|
plan = _make_plan(plan_id, processing_state=ProcessingState(state))
|
|
context._pp_plan_repo.create(plan)
|
|
context._pp_session.commit()
|
|
|
|
|
|
@given('{count:d} persisted plans linked to action "{action_name}"')
|
|
def step_n_plans_linked_to_action(
|
|
context: Context, count: int, action_name: str
|
|
) -> None:
|
|
"""Create N plans linked to the given action."""
|
|
for _ in range(count):
|
|
plan_id = _next_ulid()
|
|
plan = _make_plan(plan_id, action_name=action_name)
|
|
context._pp_plan_repo.create(plan)
|
|
context._pp_session.commit()
|
|
|
|
|
|
@when('I list persisted plans filtered by phase "{phase}"')
|
|
def step_list_plans_by_phase(context: Context, phase: str) -> None:
|
|
"""List plans filtered by phase."""
|
|
context._pp_listed = context._pp_plan_repo.list_plans(phase=phase)
|
|
|
|
|
|
@when('I list plans filtered by processing_state "{state}"')
|
|
def step_list_plans_by_state(context: Context, state: str) -> None:
|
|
"""List plans filtered by processing state."""
|
|
context._pp_listed = context._pp_plan_repo.list_plans(processing_state=state)
|
|
|
|
|
|
@when('I list plans filtered by action_name "{action_name}"')
|
|
def step_list_plans_by_action(context: Context, action_name: str) -> None:
|
|
"""List plans filtered by action name."""
|
|
context._pp_listed = context._pp_plan_repo.list_plans(action_name=action_name)
|
|
|
|
|
|
@then("I should get {count:d} plans in the list")
|
|
def step_verify_plan_count(context: Context, count: int) -> None:
|
|
"""Verify the number of plans returned."""
|
|
assert len(context._pp_listed) == count, (
|
|
f"Expected {count}, got {len(context._pp_listed)}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Plan tree link scenarios
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given('a parent plan with ID "{plan_id}"')
|
|
def step_parent_plan(context: Context, plan_id: str) -> None:
|
|
"""Create and persist a parent plan."""
|
|
plan = _make_plan(plan_id, phase=PlanPhase.EXECUTE)
|
|
context._pp_plan_repo.create(plan)
|
|
context._pp_session.commit()
|
|
|
|
|
|
@given('a child plan with ID "{child_id}" under parent "{parent_id}"')
|
|
def step_child_plan(context: Context, child_id: str, parent_id: str) -> None:
|
|
"""Create and persist a child plan referencing the parent."""
|
|
plan = _make_plan(
|
|
child_id,
|
|
parent_plan_id=parent_id,
|
|
root_plan_id=parent_id,
|
|
phase=PlanPhase.STRATEGIZE,
|
|
)
|
|
context._pp_plan_repo.create(plan)
|
|
context._pp_session.commit()
|
|
|
|
|
|
@when('I retrieve the child plan "{plan_id}"')
|
|
def step_retrieve_child(context: Context, plan_id: str) -> None:
|
|
"""Retrieve a child plan from the repo."""
|
|
context._pp_child = context._pp_plan_repo.get(plan_id)
|
|
assert context._pp_child is not None
|
|
|
|
|
|
@then('the child plan should reference parent "{parent_id}"')
|
|
def step_child_references_parent(context: Context, parent_id: str) -> None:
|
|
"""Verify the child plan's parent reference."""
|
|
assert context._pp_child.identity.parent_plan_id == parent_id
|
|
|
|
|
|
@then('the child plan should reference root "{root_id}"')
|
|
def step_child_references_root(context: Context, root_id: str) -> None:
|
|
"""Verify the child plan's root reference."""
|
|
assert context._pp_child.identity.root_plan_id == root_id
|
|
|
|
|
|
@given('a root plan with ID "{plan_id}"')
|
|
def step_root_plan(context: Context, plan_id: str) -> None:
|
|
"""Create and persist a root plan."""
|
|
plan = _make_plan(plan_id, phase=PlanPhase.EXECUTE)
|
|
context._pp_plan_repo.create(plan)
|
|
context._pp_session.commit()
|
|
|
|
|
|
@given('a mid plan with ID "{mid_id}" under root "{root_id}"')
|
|
def step_mid_plan(context: Context, mid_id: str, root_id: str) -> None:
|
|
"""Create and persist a mid-level plan."""
|
|
plan = _make_plan(
|
|
mid_id,
|
|
parent_plan_id=root_id,
|
|
root_plan_id=root_id,
|
|
phase=PlanPhase.STRATEGIZE,
|
|
)
|
|
context._pp_plan_repo.create(plan)
|
|
context._pp_session.commit()
|
|
|
|
|
|
@given(
|
|
'a leaf plan with ID "{leaf_id}" under parent "{parent_id}" and root "{root_id}"'
|
|
)
|
|
def step_leaf_plan(
|
|
context: Context, leaf_id: str, parent_id: str, root_id: str
|
|
) -> None:
|
|
"""Create and persist a leaf plan."""
|
|
plan = _make_plan(
|
|
leaf_id,
|
|
parent_plan_id=parent_id,
|
|
root_plan_id=root_id,
|
|
phase=PlanPhase.STRATEGIZE,
|
|
)
|
|
context._pp_plan_repo.create(plan)
|
|
context._pp_session.commit()
|
|
|
|
|
|
@when('I retrieve the leaf plan "{plan_id}"')
|
|
def step_retrieve_leaf(context: Context, plan_id: str) -> None:
|
|
"""Retrieve a leaf plan from the repo."""
|
|
context._pp_leaf = context._pp_plan_repo.get(plan_id)
|
|
assert context._pp_leaf is not None
|
|
|
|
|
|
@then('the leaf plan parent should be "{parent_id}"')
|
|
def step_leaf_parent(context: Context, parent_id: str) -> None:
|
|
"""Verify the leaf plan's parent reference."""
|
|
assert context._pp_leaf.identity.parent_plan_id == parent_id
|
|
|
|
|
|
@then('the leaf plan root should be "{root_id}"')
|
|
def step_leaf_root(context: Context, root_id: str) -> None:
|
|
"""Verify the leaf plan's root reference."""
|
|
assert context._pp_leaf.identity.root_plan_id == root_id
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Cross-restart scenarios
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("I close and reopen the persistence database")
|
|
def step_reopen_database(context: Context) -> None:
|
|
"""Close the session and engine, then reopen from the same file."""
|
|
db_path = context._pp_db_path
|
|
context._pp_session.close()
|
|
context._pp_engine.dispose()
|
|
|
|
engine = create_engine(f"sqlite:///{db_path}", echo=False)
|
|
sm = sessionmaker(bind=engine)
|
|
session = sm()
|
|
context._pp_engine = engine
|
|
context._pp_session = session
|
|
context._pp_session_factory = lambda: session
|
|
context._pp_plan_repo = LifecyclePlanRepository(
|
|
session_factory=context._pp_session_factory,
|
|
)
|
|
context._pp_action_repo = ActionRepository(
|
|
session_factory=context._pp_session_factory,
|
|
)
|
|
|
|
|
|
@then('the plan should still be in phase "{phase}" with state "{state}"')
|
|
def step_plan_phase_after_restart(context: Context, phase: str, state: str) -> None:
|
|
"""Verify plan phase/state after reconnection."""
|
|
plan = context._pp_plan_repo.get(context._pp_current_plan_id)
|
|
assert plan is not None, "Plan not found after reconnect"
|
|
assert plan.phase == PlanPhase(phase)
|
|
assert plan.processing_state == ProcessingState(state)
|
|
|
|
|
|
@given('a persisted plan with project links "{link1}" and "{link2}"')
|
|
def step_plan_with_project_links(context: Context, link1: str, link2: str) -> None:
|
|
"""Create a plan with project links."""
|
|
plan_id = _next_ulid()
|
|
plan = _make_plan(
|
|
plan_id,
|
|
project_links=[
|
|
ProjectLink(project_name=link1),
|
|
ProjectLink(project_name=link2),
|
|
],
|
|
)
|
|
context._pp_plan_repo.create(plan)
|
|
context._pp_session.commit()
|
|
context._pp_current_plan_id = plan_id
|
|
|
|
|
|
@then("the plan should still have {count:d} project links")
|
|
def step_plan_project_links_count(context: Context, count: int) -> None:
|
|
"""Verify the plan still has the expected number of project links."""
|
|
plan = context._pp_plan_repo.get(context._pp_current_plan_id)
|
|
assert plan is not None
|
|
assert len(plan.project_links) == count
|
|
|
|
|
|
@given('a persisted plan with arguments "{arg1}" and "{arg2}"')
|
|
def step_plan_with_arguments(context: Context, arg1: str, arg2: str) -> None:
|
|
"""Create a plan with arguments."""
|
|
plan_id = _next_ulid()
|
|
plan = _make_plan(
|
|
plan_id,
|
|
arguments={arg1: "value1", arg2: "value2"},
|
|
arguments_order=[arg1, arg2],
|
|
)
|
|
context._pp_plan_repo.create(plan)
|
|
context._pp_session.commit()
|
|
context._pp_current_plan_id = plan_id
|
|
|
|
|
|
@then("the plan should still have {count:d} arguments in order")
|
|
def step_plan_arguments_count(context: Context, count: int) -> None:
|
|
"""Verify the plan still has the expected number of arguments."""
|
|
plan = context._pp_plan_repo.get(context._pp_current_plan_id)
|
|
assert plan is not None
|
|
assert len(plan.arguments) == count
|
|
assert len(plan.arguments_order) == count
|
|
|
|
|
|
@given('a persisted plan with invariant "{text}"')
|
|
def step_plan_with_invariant(context: Context, text: str) -> None:
|
|
"""Create a plan with an invariant."""
|
|
plan_id = _next_ulid()
|
|
plan = _make_plan(
|
|
plan_id,
|
|
invariants=[PlanInvariant(text=text, source=InvariantSource.PLAN)],
|
|
)
|
|
context._pp_plan_repo.create(plan)
|
|
context._pp_session.commit()
|
|
context._pp_current_plan_id = plan_id
|
|
|
|
|
|
@then('the plan should still have invariant "{text}"')
|
|
def step_plan_has_invariant(context: Context, text: str) -> None:
|
|
"""Verify the plan still has the invariant."""
|
|
plan = context._pp_plan_repo.get(context._pp_current_plan_id)
|
|
assert plan is not None
|
|
invariant_texts = [inv.text for inv in plan.invariants]
|
|
assert text in invariant_texts, f"Expected '{text}' in {invariant_texts}"
|