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
## 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
447 lines
17 KiB
Python
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
|