Files
cleveragents-core/features/cross_plan_correction.feature
freemo 4ca4874c4d
CI / lint (pull_request) Successful in 14s
CI / typecheck (pull_request) Successful in 34s
CI / quality (pull_request) Successful in 15s
CI / security (pull_request) Successful in 35s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 16s
CI / integration_tests (pull_request) Successful in 2m54s
CI / unit_tests (pull_request) Successful in 4m9s
CI / coverage (pull_request) Successful in 4m48s
CI / docker (pull_request) Successful in 1m46s
CI / lint (push) Successful in 13s
CI / typecheck (push) Successful in 35s
CI / quality (push) Successful in 16s
CI / security (push) Successful in 31s
CI / build (push) Successful in 14s
CI / unit_tests (push) Successful in 2m11s
CI / integration_tests (push) Successful in 3m9s
CI / benchmark-regression (pull_request) Successful in 30m12s
CI / benchmark-regression (push) Has been skipped
CI / docker (push) Successful in 40s
CI / coverage (push) Successful in 4m25s
CI / benchmark-publish (push) Successful in 17m34s
feat(correction): implement cross-plan correction cascading with child plan state handling
Add CrossPlanCorrectionService that implements the four child-plan-state-
dependent behaviours from the specification when a correction's affected
subtree includes child plans:

- Not yet started → cancel the child plan
- In progress → cancel + rollback sandbox to pre-child-plan state
- Completed but not applied → cancel + rollback sandbox
- Already applied → reject the correction (CorrectionRejection)

Key additions:
- ChildPlanState enum classifying child plans into 4 states
- CorrectionRejection result type with reason and affected applied plan IDs
- CascadeAction/CascadeResult models for cascade operation tracking
- CorrectionStatus.REJECTED for rejected corrections
- Atomic cascade-or-rollback: all child plan actions succeed or the
  entire cascade is rolled back
- Protocol-based dependency injection (ChildPlanLookup, ChildPlanCanceller,
  SandboxRollbacker) for testability
- execute_correction_with_cascade() integrates with CorrectionService flow

Testing:
- 24 Behave BDD scenarios in cross_plan_correction.feature
- 8 Robot Framework end-to-end smoke tests
- ASV benchmarks for cascade performance with varying child plan counts

ISSUES CLOSED: #547
2026-03-04 21:20:47 +00:00

304 lines
21 KiB
Gherkin

Feature: Cross-plan correction cascading
When a correction's affected subtree includes child plans, the child
plan's state determines the cascade behaviour: cancel, cancel with
sandbox rollback, or reject the correction entirely.
#
# Not-yet-started child plans cancel only
#
Scenario: Correction cascades to not-started child plan — cancel
Given a cross-plan correction service
And the child plan "CP1" has state "not_started"
When I execute a cascade correction for correction "C1" with child plans "CP1"
Then the cascade should succeed
And the cancelled child plans should contain "CP1"
And the rolled back child plans should be empty
# ─────────────────────────────────────────────────────────────────
# In-progress child plans → cancel + sandbox rollback
# ─────────────────────────────────────────────────────────────────
Scenario: Correction cascades to in-progress child plan — cancel and rollback
Given a cross-plan correction service
And the child plan "CP1" has state "in_progress"
When I execute a cascade correction for correction "C1" with child plans "CP1"
Then the cascade should succeed
And the cancelled child plans should contain "CP1"
And the rolled back child plans should contain "CP1"
# ─────────────────────────────────────────────────────────────────
# Completed-unapplied child plans → cancel + rollback
# ─────────────────────────────────────────────────────────────────
Scenario: Correction cascades to completed-unapplied child plan — cancel and rollback
Given a cross-plan correction service
And the child plan "CP1" has state "completed_unapplied"
When I execute a cascade correction for correction "C1" with child plans "CP1"
Then the cascade should succeed
And the cancelled child plans should contain "CP1"
And the rolled back child plans should contain "CP1"
# ─────────────────────────────────────────────────────────────────
# Already-applied child plans → rejection
# ─────────────────────────────────────────────────────────────────
Scenario: Correction cascades to already-applied child plan — rejection with CorrectionRejection
Given a cross-plan correction service
And the child plan "CP1" has state "applied"
When I execute a cascade correction for correction "C1" with child plans "CP1"
Then the cascade should be rejected
And the rejection reason should mention "already applied"
And the rejection should list applied child plan "CP1"
# ─────────────────────────────────────────────────────────────────
# Mixed child plan states
# ─────────────────────────────────────────────────────────────────
Scenario: Mixed child plan states — reject if any applied even if others are cancellable
Given a cross-plan correction service
And the child plan "CP1" has state "not_started"
And the child plan "CP2" has state "in_progress"
And the child plan "CP3" has state "applied"
When I execute a cascade correction for correction "C1" with child plans "CP1,CP2,CP3"
Then the cascade should be rejected
And the rejection should list applied child plan "CP3"
And the cancelled child plans should be empty
# ─────────────────────────────────────────────────────────────────
# Atomic cascade: failure during one cancel rolls back all previous
# ─────────────────────────────────────────────────────────────────
Scenario: Atomic cascade — failure during one cancel rolls back all previous cancels
Given a cross-plan correction service with failing canceller on "CP2"
And the child plan "CP1" has state "not_started"
And the child plan "CP2" has state "not_started"
When I try to execute a cascade correction for correction "C1" with child plans "CP1,CP2"
Then the cascade should raise an error
And the cascade error should mention "CP2"
# ─────────────────────────────────────────────────────────────────
# Evaluation (dry-run) scenarios
# ─────────────────────────────────────────────────────────────────
Scenario: Evaluate cascade without executing — no child plans
Given a cross-plan correction service
When I evaluate a cascade for correction "C1" with no child plans
Then the cascade evaluation should succeed
And the cascade actions should be empty
Scenario: Evaluate cascade — mixed states preview
Given a cross-plan correction service
And the child plan "CP1" has state "not_started"
And the child plan "CP2" has state "in_progress"
When I evaluate a cascade for correction "C1" with child plans "CP1,CP2"
Then the cascade evaluation should succeed
And the cascade action for "CP1" should be "cancel"
And the cascade action for "CP2" should be "cancel_and_rollback"
# ─────────────────────────────────────────────────────────────────
# Validation
# ─────────────────────────────────────────────────────────────────
Scenario: Empty correction_id raises validation error
Given a cross-plan correction service
When I try to evaluate a cascade with empty correction_id
Then a cross-plan validation error should be raised
# ─────────────────────────────────────────────────────────────────
# Multiple not-started child plans — all cancelled
# ─────────────────────────────────────────────────────────────────
Scenario: Multiple not-started child plans all cancelled
Given a cross-plan correction service
And the child plan "CP1" has state "not_started"
And the child plan "CP2" has state "not_started"
And the child plan "CP3" has state "not_started"
When I execute a cascade correction for correction "C1" with child plans "CP1,CP2,CP3"
Then the cascade should succeed
And the cancelled child plans should contain "CP1"
And the cancelled child plans should contain "CP2"
And the cancelled child plans should contain "CP3"
And the rolled back child plans should be empty
# ─────────────────────────────────────────────────────────────────
# Integration with CorrectionImpact — execute_correction_with_cascade
# ─────────────────────────────────────────────────────────────────
Scenario: Execute correction with cascade — no child plans returns result
Given a cross-plan correction service
When I execute a correction with cascade for correction "C1" with no child plans in revert mode
Then the correction with cascade result should be applied
Scenario: Execute correction with cascade — applied child plan returns rejection
Given a cross-plan correction service
And the child plan "CP1" has state "applied"
When I execute a correction with cascade for correction "C1" with child plan "CP1" in revert mode
Then the correction with cascade result should be a rejection
Scenario: Execute correction with cascade — cancellable child plan succeeds
Given a cross-plan correction service
And the child plan "CP1" has state "not_started"
When I execute a correction with cascade for correction "C1" with child plan "CP1" in revert mode
Then the correction with cascade result should be applied
# ─────────────────────────────────────────────────────────────────
# classify_cascade_action unit-level checks
# ─────────────────────────────────────────────────────────────────
Scenario: classify_cascade_action for not_started returns cancel
When I classify cascade action for state "not_started"
Then the classified action should be "cancel"
Scenario: classify_cascade_action for in_progress returns cancel_and_rollback
When I classify cascade action for state "in_progress"
Then the classified action should be "cancel_and_rollback"
Scenario: classify_cascade_action for completed_unapplied returns cancel_and_rollback
When I classify cascade action for state "completed_unapplied"
Then the classified action should be "cancel_and_rollback"
Scenario: classify_cascade_action for applied returns reject
When I classify cascade action for state "applied"
Then the classified action should be "reject"
# ─────────────────────────────────────────────────────────────────
# Constructor validation
# ─────────────────────────────────────────────────────────────────
Scenario: Constructor rejects None plan_lookup
When I try to create a cross-plan service with None plan_lookup
Then a cross-plan validation error should be raised
Scenario: Constructor rejects None plan_canceller
When I try to create a cross-plan service with None plan_canceller
Then a cross-plan validation error should be raised
Scenario: Constructor rejects None sandbox_rollbacker
When I try to create a cross-plan service with None sandbox_rollbacker
Then a cross-plan validation error should be raised
# ─────────────────────────────────────────────────────────────────
# CorrectionRejection model validation
# ─────────────────────────────────────────────────────────────────
Scenario: CorrectionRejection model has correct fields
When I create a CorrectionRejection with correction_id "C1" and reason "test" and plans "CP1,CP2"
Then the rejection correction_id should be "C1"
And the rejection reason should be "test"
And the rejection affected plans should contain "CP1"
And the rejection affected plans should contain "CP2"
# ─────────────────────────────────────────────────────────────────
# CascadeAction model validation
# ─────────────────────────────────────────────────────────────────
Scenario: CascadeAction with invalid action raises error
When I try to create a CascadeAction with invalid action "destroy"
Then a cross-plan pydantic validation error should be raised
# ─────────────────────────────────────────────────────────────────
# CorrectionStatus REJECTED
# ─────────────────────────────────────────────────────────────────
Scenario: CorrectionStatus includes REJECTED
Then the CorrectionStatus enum should include "rejected"
# ─────────────────────────────────────────────────────────────────
# ChildPlanState enum completeness
# ─────────────────────────────────────────────────────────────────
Scenario: ChildPlanState enum has all four states
Then the ChildPlanState enum should have 4 members
And the ChildPlanState enum should include "not_started"
And the ChildPlanState enum should include "in_progress"
And the ChildPlanState enum should include "completed_unapplied"
And the ChildPlanState enum should include "applied"
# ─────────────────────────────────────────────────────────────────
# Validation — execute_cascade with empty correction_id
# ─────────────────────────────────────────────────────────────────
Scenario: execute_cascade with empty correction_id raises validation error
Given a cross-plan correction service
When I try to execute a cascade with empty correction_id
Then a cross-plan validation error should be raised
# ─────────────────────────────────────────────────────────────────
# Validation — execute_correction_with_cascade with empty id
# ─────────────────────────────────────────────────────────────────
Scenario: execute_correction_with_cascade with empty correction_id raises validation error
Given a cross-plan correction service
When I try to execute a correction with cascade with empty correction_id
Then a cross-plan validation error should be raised
# ─────────────────────────────────────────────────────────────────
# Append mode — no child plans
# ─────────────────────────────────────────────────────────────────
Scenario: Execute correction with cascade in append mode — no child plans returns result with empty lists
Given a cross-plan correction service
When I execute a correction with cascade for correction "C1" with no child plans in append mode
Then the correction with cascade result should be applied
And the correction result reverted decisions should be empty
And the correction result archived artifacts should be empty
# ─────────────────────────────────────────────────────────────────
# Append mode — cancellable child plan
# ─────────────────────────────────────────────────────────────────
Scenario: Execute correction with cascade in append mode — cancellable child plan succeeds with empty lists
Given a cross-plan correction service
And the child plan "CP1" has state "not_started"
When I execute a correction with cascade for correction "C1" with child plan "CP1" in append mode
Then the correction with cascade result should be applied
And the correction result reverted decisions should be empty
And the correction result archived artifacts should be empty
# ─────────────────────────────────────────────────────────────────
# CorrectionImpact model validation — invalid rollback_tier
# ─────────────────────────────────────────────────────────────────
Scenario: CorrectionImpact rejects invalid rollback_tier
When I try to create a CorrectionImpact with rollback_tier "invalid_tier"
Then a cross-plan pydantic validation error should be raised
# ─────────────────────────────────────────────────────────────────
# CorrectionImpact model validation — invalid risk_level
# ─────────────────────────────────────────────────────────────────
Scenario: CorrectionImpact rejects invalid risk_level
When I try to create a CorrectionImpact with risk_level "catastrophic"
Then a cross-plan pydantic validation error should be raised
# ─────────────────────────────────────────────────────────────────
# Protocol runtime checks
# ─────────────────────────────────────────────────────────────────
Scenario: Mock plan lookup satisfies ChildPlanLookup protocol
Given a cross-plan correction service
Then the plan lookup should satisfy the ChildPlanLookup protocol
Scenario: Mock plan canceller satisfies ChildPlanCanceller protocol
Given a cross-plan correction service
Then the plan canceller should satisfy the ChildPlanCanceller protocol
Scenario: Mock sandbox rollbacker satisfies SandboxRollbacker protocol
Given a cross-plan correction service
Then the sandbox rollbacker should satisfy the SandboxRollbacker protocol
# ─────────────────────────────────────────────────────────────────
# Whitespace-only correction_id treated as empty
# ─────────────────────────────────────────────────────────────────
Scenario: execute_cascade with whitespace-only correction_id raises validation error
Given a cross-plan correction service
When I try to execute a cascade with whitespace-only correction_id
Then a cross-plan validation error should be raised
Scenario: execute_correction_with_cascade with whitespace-only correction_id raises validation error
Given a cross-plan correction service
When I try to execute a correction with cascade with whitespace-only correction_id
Then a cross-plan validation error should be raised