Files
cleveragents-core/features/steps/plan_resume_steps.py
T
brent.edwards eb46f0ff54
CI / push-validation (push) Successful in 40s
CI / helm (push) Successful in 44s
CI / build (push) Successful in 1m14s
CI / lint (push) Successful in 1m18s
CI / quality (push) Successful in 1m36s
CI / e2e_tests (push) Successful in 1m19s
CI / typecheck (push) Successful in 2m22s
CI / security (push) Successful in 2m23s
CI / benchmark-regression (push) Failing after 41s
CI / integration_tests (push) Successful in 4m46s
CI / unit_tests (push) Failing after 20m13s
CI / benchmark-publish (push) Failing after 28m28s
CI / coverage (push) Has been cancelled
CI / docker (push) Has been cancelled
CI / status-check (push) Has been cancelled
fix(plan): add tier hydration and improve architecture review output (#10938)
## Summary

This PR fixes issue #10878 where architecture reviews were truncated because the regex pattern for parsing file output would stop at the first ``` encountered in the Markdown report.

## Changes

- Change file delimiters from ``` to >>>>>>>/<<<<<<< to avoid Markdown conflicts
- Add tier hydration before strategize phase in plan_executor.py
- Increase max_tokens to 16384 in llm_actors.py for longer outputs
- Increase context_max_tokens_hot from 16000 to 32000 in settings.py
- Fix get_hot_view → get_hot_fragments in strategy_actor.py and plan_executor.py
- Add opencode to skip directories in context_tier_hydrator.py
- Change sandbox output location to plan-output/ directory in plan.py
- Add get_context_summary stub method to acms_service.py

## Testing

Run architecture review action and verify the output report is complete with all sections.

Reviewed-on: #10938
2026-05-19 12:43:34 +00:00

447 lines
17 KiB
Python

"""Step definitions for Plan Resume feature tests."""
from behave import given, then, when
from behave.runner import Context
from ulid import ULID
from cleveragents.application.services.plan_lifecycle_service import (
PlanLifecycleService,
)
from cleveragents.application.services.plan_resume_service import (
PlanResumeService,
)
from cleveragents.config.settings import Settings
from cleveragents.core.exceptions import PlanError, ValidationError
from cleveragents.domain.models.core.plan import (
PlanPhase,
ProcessingState,
ProjectLink,
)
from cleveragents.domain.models.core.resume import ResumeMetadata
# --- Background ---
@given("I have a plan lifecycle service for resume tests")
def step_create_resume_lifecycle_service(context: Context) -> None:
"""Create lifecycle service for resume tests."""
settings = Settings()
context.lifecycle_service = PlanLifecycleService(settings=settings)
context.error = None
context.plan = None
context.plan_id = None
context.eligibility = None
context.summary = None
@given("I have a plan resume service")
def step_create_resume_service(context: Context) -> None:
"""Create plan resume service."""
context.resume_service = PlanResumeService(
lifecycle_service=context.lifecycle_service,
)
# --- Helper to create plans in specific states ---
def _create_action(context: Context) -> str:
"""Create and return an action name for testing."""
action_name = f"local/resume-test-{ULID()!s}"[:30].lower()
context.lifecycle_service.create_action(
name=action_name,
description="Resume test action",
definition_of_done="Step 1\nStep 2\nStep 3",
strategy_actor="local/stub-strategy",
execution_actor="local/stub-execute",
)
return action_name
def _create_plan_in_state(
context: Context,
phase: PlanPhase,
state: ProcessingState,
) -> str:
"""Create a plan and move it to the given phase/state."""
action_name = _create_action(context)
plan = context.lifecycle_service.use_action(
action_name=action_name,
project_links=[ProjectLink(project_name="local/test-proj")],
)
plan_id = plan.identity.plan_id
if phase == PlanPhase.STRATEGIZE and state == ProcessingState.QUEUED:
pass
elif phase == PlanPhase.STRATEGIZE and state == ProcessingState.PROCESSING:
context.lifecycle_service.start_strategize(plan_id)
elif phase == PlanPhase.EXECUTE and state == ProcessingState.PROCESSING:
context.lifecycle_service.start_strategize(plan_id)
p = context.lifecycle_service.get_plan(plan_id)
p.decision_root_id = str(ULID())
context.lifecycle_service.commit_plan(p)
context.lifecycle_service.complete_strategize(plan_id)
context.lifecycle_service.execute_plan(plan_id)
context.lifecycle_service.start_execute(plan_id)
elif phase == PlanPhase.EXECUTE and state == ProcessingState.ERRORED:
context.lifecycle_service.start_strategize(plan_id)
p = context.lifecycle_service.get_plan(plan_id)
p.decision_root_id = str(ULID())
context.lifecycle_service.commit_plan(p)
context.lifecycle_service.complete_strategize(plan_id)
context.lifecycle_service.execute_plan(plan_id)
context.lifecycle_service.start_execute(plan_id)
context.lifecycle_service.fail_execute(plan_id, "Test error")
elif phase == PlanPhase.APPLY and state == ProcessingState.APPLIED:
context.lifecycle_service.start_strategize(plan_id)
p = context.lifecycle_service.get_plan(plan_id)
p.decision_root_id = str(ULID())
context.lifecycle_service.commit_plan(p)
context.lifecycle_service.complete_strategize(plan_id)
context.lifecycle_service.execute_plan(plan_id)
context.lifecycle_service.start_execute(plan_id)
context.lifecycle_service.complete_execute(plan_id)
context.lifecycle_service.apply_plan(plan_id)
context.lifecycle_service.start_apply(plan_id)
context.lifecycle_service.complete_apply(plan_id)
elif phase == PlanPhase.APPLY and state == ProcessingState.CONSTRAINED:
context.lifecycle_service.start_strategize(plan_id)
p = context.lifecycle_service.get_plan(plan_id)
p.decision_root_id = str(ULID())
context.lifecycle_service.commit_plan(p)
context.lifecycle_service.complete_strategize(plan_id)
context.lifecycle_service.execute_plan(plan_id)
context.lifecycle_service.start_execute(plan_id)
context.lifecycle_service.complete_execute(plan_id)
context.lifecycle_service.apply_plan(plan_id)
context.lifecycle_service.start_apply(plan_id)
p2 = context.lifecycle_service.get_plan(plan_id)
p2.processing_state = ProcessingState.PROCESSING
context.lifecycle_service.commit_plan(p2)
context.lifecycle_service.constrain_apply(plan_id, "constraints violated")
context.plan_id = plan_id
context.plan = context.lifecycle_service.get_plan(plan_id)
return plan_id
# --- Given steps ---
@given("I have a resume-test plan in execute phase with errored state")
def step_resume_plan_execute_errored(context: Context) -> None:
"""Create a plan in execute/errored state for resume tests."""
_create_plan_in_state(context, PlanPhase.EXECUTE, ProcessingState.ERRORED)
@given("I have a resume-test plan in execute phase with processing state")
def step_resume_plan_execute_processing(context: Context) -> None:
"""Create a plan in execute/processing state for resume tests."""
_create_plan_in_state(context, PlanPhase.EXECUTE, ProcessingState.PROCESSING)
@given("I have a resume-test plan in strategize phase with queued state")
def step_resume_plan_strategize_queued(context: Context) -> None:
"""Create a plan in strategize/queued state for resume tests."""
_create_plan_in_state(context, PlanPhase.STRATEGIZE, ProcessingState.QUEUED)
@given("I have a resume-test plan in applied terminal state")
def step_resume_plan_applied(context: Context) -> None:
"""Create a plan in applied terminal state for resume tests."""
_create_plan_in_state(context, PlanPhase.APPLY, ProcessingState.APPLIED)
@given("I have a resume-test plan in cancelled terminal state")
def step_resume_plan_cancelled(context: Context) -> None:
"""Create a plan and cancel it for resume tests."""
_create_plan_in_state(context, PlanPhase.EXECUTE, ProcessingState.PROCESSING)
context.lifecycle_service.cancel_plan(context.plan_id, "test cancel")
context.plan = context.lifecycle_service.get_plan(context.plan_id)
@given("I have a resume-test plan in constrained terminal state")
def step_resume_plan_constrained(context: Context) -> None:
"""Create a plan in constrained terminal state for resume tests."""
_create_plan_in_state(context, PlanPhase.APPLY, ProcessingState.CONSTRAINED)
@given("I have a resume-test plan in action phase")
def step_resume_plan_action_phase(context: Context) -> None:
"""Create a plan and set it to action phase for resume tests."""
_create_plan_in_state(context, PlanPhase.STRATEGIZE, ProcessingState.QUEUED)
plan = context.lifecycle_service.get_plan(context.plan_id)
plan.phase = PlanPhase.ACTION
context.lifecycle_service.commit_plan(plan)
context.plan = plan
@given('the resume-test plan has checkpoint at step {index:d} with decision "{dec_id}"')
def step_resume_plan_has_checkpoint(context: Context, index: int, dec_id: str) -> None:
"""Add a checkpoint to the plan at given step."""
context.resume_service.set_total_steps(context.plan_id, 5)
context.resume_service.record_step_checkpoint(
plan_id=context.plan_id,
step_index=index,
decision_id=dec_id,
step_text=f"Step {index}",
)
@given("I have resume metadata with last completed step {step:d}")
def step_resume_metadata_with_step(context: Context, step: int) -> None:
"""Create resume metadata with given step."""
context.resume_metadata = ResumeMetadata(
last_completed_step=step,
total_steps=5,
)
@given("I have fresh resume metadata")
def step_fresh_resume_metadata(context: Context) -> None:
"""Create fresh resume metadata with no progress."""
context.resume_metadata = ResumeMetadata()
@given("I have resume metadata with {total:d} total steps and last step {last:d}")
def step_resume_metadata_with_totals(context: Context, total: int, last: int) -> None:
"""Create resume metadata with total and last step."""
context.resume_metadata = ResumeMetadata(
last_completed_step=last,
total_steps=total,
)
# --- When steps ---
@when("I validate resume eligibility")
def step_validate_resume_eligibility(context: Context) -> None:
"""Validate resume eligibility for the current plan."""
context.eligibility = context.resume_service.validate_eligibility(
context.plan_id,
)
@when('I record a resume step checkpoint at index {index:d} with decision "{dec_id}"')
def step_record_resume_checkpoint(context: Context, index: int, dec_id: str) -> None:
"""Record a step checkpoint."""
context.resume_service.set_total_steps(context.plan_id, 5)
context.resume_service.record_step_checkpoint(
plan_id=context.plan_id,
step_index=index,
decision_id=dec_id,
step_text=f"Step {index}",
)
context.plan = context.lifecycle_service.get_plan(context.plan_id)
@when("I resume the plan with dry-run")
def step_resume_dry_run(context: Context) -> None:
"""Resume the plan in dry-run mode."""
context.summary = context.resume_service.resume_plan(context.plan_id, dry_run=True)
@when("I resume the plan without dry-run")
def step_resume_live(context: Context) -> None:
"""Resume the plan in live mode."""
context.summary = context.resume_service.resume_plan(context.plan_id, dry_run=False)
context.plan = context.lifecycle_service.get_plan(context.plan_id)
@when("I record a graceful shutdown for resume")
def step_record_resume_shutdown(context: Context) -> None:
"""Record a graceful shutdown."""
context.resume_service.record_shutdown(context.plan_id)
@when("I try to resume a plan with empty ID")
def step_resume_empty_id(context: Context) -> None:
"""Try to resume a plan with empty ID."""
try:
context.resume_service.resume_plan("")
except ValidationError as e:
context.error = e
@when("I try to validate eligibility with empty plan ID")
def step_validate_eligibility_empty_id(context: Context) -> None:
"""Try to validate eligibility with empty plan ID."""
try:
context.resume_service.validate_eligibility("")
except ValidationError as e:
context.error = e
@when("I try to resume the terminal plan for resume test")
def step_resume_terminal_plan(context: Context) -> None:
"""Try to resume a terminal plan."""
try:
context.resume_service.resume_plan(context.plan_id)
except PlanError as e:
context.error = e
@when("I set the resume total steps to {total:d}")
def step_set_resume_total_steps(context: Context, total: int) -> None:
"""Set total steps for the plan."""
context.resume_service.set_total_steps(context.plan_id, total)
@when("I try to set resume total steps with empty plan ID")
def step_set_resume_total_steps_empty(context: Context) -> None:
"""Try to set total steps with empty plan ID."""
try:
context.resume_service.set_total_steps("", 5)
except ValidationError as e:
context.error = e
# --- Then steps ---
@then("the plan should be eligible for resume")
def step_assert_eligible(context: Context) -> None:
"""Assert the plan is eligible for resume."""
assert context.eligibility is not None
assert context.eligibility.eligible is True, (
f"Expected eligible=True, got: {context.eligibility.reason}"
)
@then("the plan should not be eligible for resume")
def step_assert_not_eligible(context: Context) -> None:
"""Assert the plan is not eligible for resume."""
assert context.eligibility is not None
assert context.eligibility.eligible is False
@then('the ineligible reason should be "{reason}"')
def step_assert_ineligible_reason(context: Context, reason: str) -> None:
"""Assert the ineligible reason code."""
assert context.eligibility is not None
assert context.eligibility.ineligible_reason is not None
assert context.eligibility.ineligible_reason.value == reason
@then("the resume-test plan should have last completed step {step:d}")
def step_assert_resume_last_step(context: Context, step: int) -> None:
"""Assert the plan's last completed step."""
plan = context.lifecycle_service.get_plan(context.plan_id)
assert plan.last_completed_step == step
@then("the resume-test plan should have a checkpoint ID set")
def step_assert_resume_checkpoint_id(context: Context) -> None:
"""Assert the plan has a checkpoint ID."""
plan = context.lifecycle_service.get_plan(context.plan_id)
assert plan.last_checkpoint_id is not None
@then("the resume metadata should have {count:d} checkpoints")
def step_assert_resume_checkpoint_count(context: Context, count: int) -> None:
"""Assert the resume metadata checkpoint count."""
metadata = context.resume_service.get_resume_metadata(context.plan_id)
assert len(metadata.checkpoints) == count
@then("the resume metadata next step should be {step:d}")
def step_assert_resume_next_step(context: Context, step: int) -> None:
"""Assert the resume metadata next step index."""
metadata = context.resume_service.get_resume_metadata(context.plan_id)
assert metadata.next_step_index == step
@then("I should get a resume summary")
def step_assert_resume_summary_exists(context: Context) -> None:
"""Assert a resume summary was returned."""
assert context.summary is not None
@then("the resume summary next step should be {step:d}")
def step_assert_resume_summary_next_step(context: Context, step: int) -> None:
"""Assert the resume summary next step."""
assert context.summary is not None
assert context.summary.next_step_index == step
@then("the resume-test plan processing state should still be errored")
def step_assert_resume_still_errored(context: Context) -> None:
"""Assert the plan is still in errored state."""
plan = context.lifecycle_service.get_plan(context.plan_id)
assert plan.processing_state == ProcessingState.ERRORED
@then('the resume summary decision ID should be "{dec_id}"')
def step_assert_resume_summary_decision(context: Context, dec_id: str) -> None:
"""Assert the resume summary decision ID."""
assert context.summary is not None
assert context.summary.decision_id == dec_id
@then("the resume-test plan processing state should be processing")
def step_assert_resume_processing(context: Context) -> None:
"""Assert the plan is in processing state."""
plan = context.lifecycle_service.get_plan(context.plan_id)
assert plan.processing_state == ProcessingState.PROCESSING
@then("the resume-test plan error message should be cleared")
def step_assert_resume_error_cleared(context: Context) -> None:
"""Assert the plan error message is None."""
plan = context.lifecycle_service.get_plan(context.plan_id)
assert plan.error_message is None
@then("the resume metadata should be marked as interrupted")
def step_assert_resume_interrupted(context: Context) -> None:
"""Assert the resume metadata is marked interrupted."""
metadata = context.resume_service.get_resume_metadata(context.plan_id)
assert metadata.interrupted is True
assert metadata.interrupted_at is not None
@then("a resume validation error should be raised")
def step_assert_resume_validation_error(context: Context) -> None:
"""Assert a ValidationError was raised."""
assert context.error is not None
assert isinstance(context.error, ValidationError)
@then("a plan error should be raised for resume")
def step_assert_resume_plan_error(context: Context) -> None:
"""Assert a PlanError was raised."""
assert context.error is not None
assert isinstance(context.error, PlanError)
@then("the resume metadata total steps should be {total:d}")
def step_assert_resume_total_steps(context: Context, total: int) -> None:
"""Assert the resume metadata total steps."""
metadata = context.resume_service.get_resume_metadata(context.plan_id)
assert metadata.total_steps == total
@then("the resume metadata has_progress should be true")
def step_assert_resume_has_progress_true(context: Context) -> None:
"""Assert has_progress is True."""
assert context.resume_metadata.has_progress is True
@then("the resume metadata has_progress should be false")
def step_assert_resume_has_progress_false(context: Context) -> None:
"""Assert has_progress is False."""
assert context.resume_metadata.has_progress is False
@then("the resume metadata is_complete should be true")
def step_assert_resume_is_complete_true(context: Context) -> None:
"""Assert is_complete is True."""
assert context.resume_metadata.is_complete is True
@then("the resume metadata is_complete should be false")
def step_assert_resume_is_complete_false(context: Context) -> None:
"""Assert is_complete is False."""
assert context.resume_metadata.is_complete is False