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
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
304 lines
21 KiB
Gherkin
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
|