Files
cleveragents-core/features/consolidated_plan_misc.feature
brent.edwards 84b0c10dbf fix(cli): plan correct active-plan resolution in isolated environments (#1184)
## Summary
- tighten `plan correct` active-plan fallback so it only runs for isolated `CLEVERAGENTS_HOME` mismatch cases and never when explicit DB env overrides are configured
- narrow fallback exception handling in `_resolve_active_plan_id()` to expected DB/path/service failures; unexpected errors now surface instead of being silently swallowed
- add BDD regression coverage for both safeguards in `features/consolidated_plan_misc.feature` + `features/steps/plan_cli_legacy_r2_steps.py`

## Validation
- `nox -e lint`: PASS
- `nox -e typecheck`: PASS
- `nox -e unit_tests`: PASS
- `nox -e integration_tests`: FAIL in current branch baseline (29 failing Robot integration tests in this environment)
- `nox -e e2e_tests`: FAIL in current branch baseline (45 failing E2E tests in this environment)
- `nox -e coverage_report`: PASS (97%)

Closes #1025

Reviewed-on: cleveragents/cleveragents-core#1184
Reviewed-by: Jeffrey Phillips Freeman <jeffrey.freeman@cleverthis.com>
Co-authored-by: Brent E. Edwards <brent.edwards@cleverthis.com>
Co-committed-by: Brent E. Edwards <brent.edwards@cleverthis.com>
2026-03-31 01:49:16 +00:00

1051 lines
43 KiB
Gherkin
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Feature: Consolidated Plan Misc
Combined scenarios from: plan_apply_service_coverage, plan_cli_legacy_r2, plan_cli_spec_print_r2, plan_execute_runtime
# ============================================================
# Originally from: plan_apply_service_coverage.feature
# Feature: PlanApplyService full coverage
# ============================================================
Scenario: operation label returns correct label for "create"
When pas_cov I call _operation_label with "create"
Then pas_cov the label should be "new file"
Scenario: operation label returns correct label for "modify"
When pas_cov I call _operation_label with "modify"
Then pas_cov the label should be "modified"
Scenario: operation label returns correct label for "delete"
When pas_cov I call _operation_label with "delete"
Then pas_cov the label should be "deleted"
Scenario: operation label returns correct label for "rename"
When pas_cov I call _operation_label with "rename"
Then pas_cov the label should be "renamed"
Scenario: operation label returns the raw op for unknown operations
When pas_cov I call _operation_label with "unknown_op"
Then pas_cov the label should be "unknown_op"
# ======================================================================
# Helper function coverage: _render_diff_plain
# ======================================================================
Scenario: render diff plain with empty changeset
Given pas_cov a SpecChangeSet with no entries
When pas_cov I render diff plain
Then pas_cov the plain diff should be "No changes in changeset."
Scenario: render diff plain with entries having both hashes
Given pas_cov a SpecChangeSet with a modify entry having both hashes
When pas_cov I render diff plain
Then pas_cov the plain diff should contain "ChangeSet:"
And pas_cov the plain diff should contain "Total changes: 1"
And pas_cov the plain diff should contain "--- a/src/app.py"
And pas_cov the plain diff should contain "+++ b/src/app.py"
And pas_cov the plain diff should contain "@@ modified @@"
And pas_cov the plain diff should contain "- hash: abcdef012345..."
And pas_cov the plain diff should contain "+ hash: 123456abcdef..."
Scenario: render diff plain with create entry missing before_hash
Given pas_cov a SpecChangeSet with a create entry having only after_hash
When pas_cov I render diff plain
Then pas_cov the plain diff should contain "@@ new file @@"
And pas_cov the plain diff should contain "+ hash:"
And pas_cov the plain diff should not contain "- hash:"
Scenario: render diff plain with delete entry missing after_hash
Given pas_cov a SpecChangeSet with a delete entry having only before_hash
When pas_cov I render diff plain
Then pas_cov the plain diff should contain "@@ deleted @@"
And pas_cov the plain diff should contain "- hash:"
And pas_cov the plain diff should not contain "+ hash:"
# ======================================================================
# Helper function coverage: _render_diff_rich
# ======================================================================
Scenario: render diff rich with empty changeset
Given pas_cov a SpecChangeSet with no entries
When pas_cov I render diff rich
Then pas_cov the rich diff should contain "[yellow]No changes in changeset.[/yellow]"
Scenario: render diff rich with known operation colors
Given pas_cov a SpecChangeSet with a modify entry having both hashes
When pas_cov I render diff rich
Then pas_cov the rich diff should contain "[yellow]modified[/yellow]"
And pas_cov the rich diff should contain "[bold]ChangeSet:[/bold]"
And pas_cov the rich diff should contain "[bold]Total changes:[/bold] 1"
And pas_cov the rich diff should contain "[red]- abcdef012345...[/red]"
And pas_cov the rich diff should contain "[green]+ 123456abcdef...[/green]"
Scenario: render diff rich with unknown operation uses white color
Given pas_cov a SpecChangeSet with an entry having operation "rename"
When pas_cov I render diff rich
Then pas_cov the rich diff should contain "[cyan]renamed[/cyan]"
Scenario: render diff rich with create entry hides before_hash
Given pas_cov a SpecChangeSet with a create entry having only after_hash
When pas_cov I render diff rich
Then pas_cov the rich diff should contain "[green]"
And pas_cov the rich diff should not contain "[red]- "
# ======================================================================
# Helper function coverage: _render_diff_json
# ======================================================================
Scenario: render diff json returns correct structure
Given pas_cov a SpecChangeSet with a modify entry having both hashes
When pas_cov I render diff json
Then pas_cov the json diff should have key "changeset_id"
And pas_cov the json diff should have key "plan_id"
And pas_cov the json diff should have key "total_changes" with value 1
And pas_cov the json diff should have key "summary"
And pas_cov the json diff should have key "entries"
And pas_cov the json diff entries should have length 1
Scenario: render diff json entry contains all expected fields
Given pas_cov a SpecChangeSet with a modify entry having both hashes
When pas_cov I render diff json
Then pas_cov the json diff first entry should have "path" equal to "src/app.py"
And pas_cov the json diff first entry should have "operation" equal to "modify"
And pas_cov the json diff first entry should have key "timestamp"
And pas_cov the json diff first entry should have key "resource_id"
And pas_cov the json diff first entry should have key "tool_name"
# ======================================================================
# Helper function coverage: _build_artifacts_dict
# ======================================================================
Scenario: build artifacts dict with changeset present
Given pas_cov a mock plan with changeset_id "CS001"
And pas_cov a SpecChangeSet with a modify entry having both hashes
When pas_cov I build artifacts dict
Then pas_cov the artifacts dict should have key "plan_id"
And pas_cov the artifacts dict should have key "changeset_summary"
And pas_cov the artifacts dict files_changed list should have length 1
Scenario: build artifacts dict with no changeset
Given pas_cov a mock plan with changeset_id "CS001"
When pas_cov I build artifacts dict with no changeset
Then pas_cov the artifacts dict changeset_summary should be null
And pas_cov the artifacts dict files_changed list should have length 0
Scenario: build artifacts dict with validation summary
Given pas_cov a mock plan with changeset_id "CS001" and validation summary
When pas_cov I build artifacts dict with no changeset
Then pas_cov the artifacts dict should have key "validation_summary"
Scenario: build artifacts dict with apply summary in error_details
Given pas_cov a mock plan with apply summary in error_details
When pas_cov I build artifacts dict with no changeset
Then pas_cov the artifacts dict should have key "apply_summary"
And pas_cov the artifacts dict apply_summary files_changed should be "5"
# ======================================================================
# Service initialization
# ======================================================================
Scenario: PlanApplyService raises ValidationError when lifecycle is None
When pas_cov I create PlanApplyService with None lifecycle service
Then pas_cov a ValidationError should be raised
Scenario: PlanApplyService initializes with valid lifecycle and no store
Given pas_cov a mock lifecycle service
When pas_cov I create PlanApplyService with valid lifecycle and no store
Then pas_cov the service should be created successfully
Scenario: PlanApplyService initializes with valid lifecycle and a store
Given pas_cov a mock lifecycle service
And pas_cov a mock changeset store
When pas_cov I create PlanApplyService with valid lifecycle and store
Then pas_cov the service should be created successfully
# ======================================================================
# diff method
# ======================================================================
Scenario: diff raises PlanError when plan has no changeset
Given pas_cov a service with a plan that has no changeset_id
When pas_cov I call diff on the service
Then pas_cov a PlanError should be raised with message containing "no ChangeSet"
Scenario: diff returns rich format by default
Given pas_cov a service with a plan that has a changeset with entries
When pas_cov I call diff with format "rich"
Then pas_cov the diff result should contain "[bold]ChangeSet:[/bold]"
Scenario: diff returns plain format
Given pas_cov a service with a plan that has a changeset with entries
When pas_cov I call diff with format "plain"
Then pas_cov the diff result should contain "ChangeSet:"
And pas_cov the diff result should not contain "[bold]"
Scenario: diff returns json format
Given pas_cov a service with a plan that has a changeset with entries
When pas_cov I call diff with format "json"
Then pas_cov the diff result should be valid JSON
Scenario: diff returns yaml format
Given pas_cov a service with a plan that has a changeset with entries
When pas_cov I call diff with format "yaml"
Then pas_cov the diff result should contain "changeset_id:"
# ======================================================================
# artifacts method
# ======================================================================
Scenario: artifacts returns json format output
Given pas_cov a service with a plan that has a changeset with entries
When pas_cov I call artifacts with format "json"
Then pas_cov the artifacts result should contain "plan_id"
Scenario: artifacts returns plain format output
Given pas_cov a service with a plan that has a changeset with entries
When pas_cov I call artifacts with format "plain"
Then pas_cov the artifacts result should be a non-empty string
Scenario: artifacts falls back to json for rich format
Given pas_cov a service with a plan that has a changeset with entries
When pas_cov I call artifacts with format "rich"
Then pas_cov the artifacts result should contain "plan_id"
# ======================================================================
# persist_apply_summary
# ======================================================================
Scenario: persist apply summary stores metadata on plan with no prior error_details
Given pas_cov a service with a plan that has no error_details
When pas_cov I persist apply summary with 3 files and 2 validations
Then pas_cov the plan error_details should contain "apply_files_changed" with value "3"
And pas_cov the plan error_details should contain "apply_validations_run" with value "2"
And pas_cov the plan error_details should contain key "apply_completed_at"
And pas_cov the lifecycle _commit_plan should have been called
Scenario: persist apply summary preserves existing error_details
Given pas_cov a service with a plan that has existing error_details
When pas_cov I persist apply summary with 1 files and 0 validations
Then pas_cov the plan error_details should contain "existing_key" with value "existing_value"
And pas_cov the plan error_details should contain "apply_files_changed" with value "1"
# ======================================================================
# handle_merge_failure
# ======================================================================
Scenario: handle merge failure stores conflict details with no prior error_details
Given pas_cov a service with a plan that has no error_details
When pas_cov I handle merge failure with conflict "file.py has conflicts"
Then pas_cov the lifecycle fail_apply should have been called
And pas_cov the returned plan should reflect errored state
Scenario: handle merge failure preserves existing error_details
Given pas_cov a service with a plan that has existing error_details
When pas_cov I handle merge failure with conflict "merge.txt conflict"
Then pas_cov the committed plan error_details should contain "existing_key"
And pas_cov the committed plan error_details should contain "merge_conflict"
And pas_cov the committed plan error_details should contain "sandbox_rollback"
# ======================================================================
# guard_empty_changeset
# ======================================================================
Scenario: guard passes when changeset has entries
Given pas_cov a service with a plan that has a changeset with entries
When pas_cov I call guard_empty_changeset
Then pas_cov the guard should return true
Scenario: guard passes when changeset is empty but allow_empty is true
Given pas_cov a service with a plan that has an empty changeset
When pas_cov I call guard_empty_changeset with allow_empty true
Then pas_cov the guard should return true
Scenario: guard raises PlanError when changeset is empty
Given pas_cov a service with a plan that has an empty changeset
When pas_cov I call guard_empty_changeset
Then pas_cov a PlanError should be raised with message containing "empty ChangeSet"
Scenario: guard raises PlanError when changeset is None
Given pas_cov a service with a plan that has no changeset_id
When pas_cov I call guard_empty_changeset
Then pas_cov a PlanError should be raised with message containing "empty ChangeSet"
# ======================================================================
# _extract_validation_counts
# ======================================================================
Scenario: extract validation counts from None returns zeros
When pas_cov I extract validation counts from None
Then pas_cov the counts should be 0 passed 0 failed 0 total
Scenario: extract validation counts from full summary
When pas_cov I extract validation counts from full summary 3 passed 1 failed 6 total
Then pas_cov the counts should be 3 passed 1 failed 6 total
Scenario: extract validation counts computes total when missing
When pas_cov I extract validation counts from partial summary 2 passed 1 failed
Then pas_cov the counts should be 2 passed 1 failed 3 total
# ======================================================================
# _resolve_changeset
# ======================================================================
Scenario: resolve changeset returns None when plan has no changeset_id
Given pas_cov a service with a plan that has no changeset_id
When pas_cov I call _resolve_changeset
Then pas_cov the resolved changeset should be None
Scenario: resolve changeset returns from store when available
Given pas_cov a service with a plan that has changeset_id and store returns a changeset
When pas_cov I call _resolve_changeset
Then pas_cov the resolved changeset should have entries
Scenario: resolve changeset falls back to empty stub when store returns None
Given pas_cov a service with a plan that has changeset_id but store returns None
When pas_cov I call _resolve_changeset
Then pas_cov the resolved changeset should have no entries
And pas_cov the resolved changeset should have the plan changeset_id
Scenario: resolve changeset falls back to empty stub when no store configured
Given pas_cov a service with a plan that has changeset_id and no store
When pas_cov I call _resolve_changeset
Then pas_cov the resolved changeset should have no entries
# ======================================================================
# cleanup_changeset
# ======================================================================
Scenario: cleanup changeset raises ValidationError for empty plan_id
Given pas_cov a service with a mock lifecycle
When pas_cov I call cleanup_changeset with empty plan_id
Then pas_cov a ValidationError should be raised
Scenario: cleanup changeset delegates to store delete_for_plan
Given pas_cov a service with a store that has delete_for_plan returning 5
When pas_cov I call cleanup_changeset with plan_id "01PLANTEST000000000000001"
Then pas_cov cleanup should return 5
Scenario: cleanup changeset returns 0 when store is None
Given pas_cov a service with no changeset store
When pas_cov I call cleanup_changeset with plan_id "01PLANTEST000000000000001"
Then pas_cov cleanup should return 0
Scenario: cleanup changeset returns 0 when store has no delete_for_plan
Given pas_cov a service with a store that lacks delete_for_plan
When pas_cov I call cleanup_changeset with plan_id "01PLANTEST000000000000001"
Then pas_cov cleanup should return 0
# ======================================================================
# apply_with_validation_gate: constrain_apply raises exception branch
# ======================================================================
Scenario: apply with validation gate handles constrain_apply exception
Given pas_cov a service where constrain_apply raises PlanError
And pas_cov the plan has failed validations
When pas_cov I call apply_with_validation_gate
Then pas_cov the result outcome should be "constrained"
And pas_cov the result message should contain "Apply refused"
# ======================================================================
# apply_with_validation_gate: complete_apply raises exception branch
# ======================================================================
Scenario: apply with validation gate handles complete_apply exception
Given pas_cov a service where complete_apply raises PlanError
And pas_cov the plan has passing validations and entries
When pas_cov I call apply_with_validation_gate
Then pas_cov the result outcome should be "applied"
And pas_cov the result message should contain "applied successfully"
# ======================================================================
# ApplyOutcome and ApplyResult model
# ======================================================================
Scenario: ApplyOutcome enum has all expected values
Then pas_cov ApplyOutcome should have value "applied"
And pas_cov ApplyOutcome should have value "constrained"
And pas_cov ApplyOutcome should have value "already_applied"
And pas_cov ApplyOutcome should have value "blocked_empty"
Scenario: ApplyResult model validates and strips whitespace
When pas_cov I create an ApplyResult with whitespace in message
Then pas_cov the ApplyResult message should be stripped
Scenario: OP_COLORS dict has expected keys
Then pas_cov OP_COLORS should map "create" to "green"
And pas_cov OP_COLORS should map "modify" to "yellow"
And pas_cov OP_COLORS should map "delete" to "red"
And pas_cov OP_COLORS should map "rename" to "cyan"
# ============================================================
# Originally from: plan_cli_legacy_r2.feature
# Feature: Plan CLI legacy wrappers and resolve branch coverage (round 2)
# ============================================================
Scenario: r2plan tell_command raises when no project
When r2plan-I call tell_command with no project
Then r2plan-a CleverAgentsError should be raised with message "No project found"
Scenario: r2plan build_command raises when no project
When r2plan-I call build_command with no project
Then r2plan-a CleverAgentsError should be raised with message "No project found"
Scenario: r2plan apply_command raises when no project
When r2plan-I call apply_command with no project
Then r2plan-a CleverAgentsError should be raised with message "No project found"
Scenario: r2plan new_command raises when no project
When r2plan-I call new_command with no project
Then r2plan-a CleverAgentsError should be raised with message "No project found"
Scenario: r2plan current_command raises when no project
When r2plan-I call current_command with no project
Then r2plan-a CleverAgentsError should be raised with message "No project found"
Scenario: r2plan list_command raises when no project
When r2plan-I call list_command with no project
Then r2plan-a CleverAgentsError should be raised with message "No project found"
Scenario: r2plan cd_command raises when no project
When r2plan-I call cd_command with no project
Then r2plan-a CleverAgentsError should be raised with message "No project found"
Scenario: r2plan continue_command raises when no project
When r2plan-I call continue_command with no project
Then r2plan-a CleverAgentsError should be raised with message "No project found"
# ===================================================================
# continue_command with prompt vs no-prompt + no current plan
# ===================================================================
Scenario: r2plan continue_command with prompt calls continue_plan
When r2plan-I call continue_command with prompt "add tests"
Then r2plan-the continue_plan service method should be called
Scenario: r2plan continue_command no prompt and no current plan raises
When r2plan-I call continue_command with no prompt and no current plan
Then r2plan-a CleverAgentsError should be raised with message "No current plan"
# ===================================================================
# build_command returns empty list when changes is None
# ===================================================================
Scenario: r2plan build_command returns empty list when None
When r2plan-I call build_command with build returning None
Then r2plan-the build result should be an empty list
# ===================================================================
# list_command returns empty list when plans is None
# ===================================================================
Scenario: r2plan list_command returns empty list when None
When r2plan-I call list_command with list returning None
Then r2plan-the list result should be an empty list
# ===================================================================
# _resolve_active_plan_id no active plans / service error
# ===================================================================
Scenario: r2plan resolve_active_plan_id aborts when no active plans
When r2plan-I call _resolve_active_plan_id with no active plans
Then r2plan-a typer Abort should be raised
Scenario: r2plan resolve_active_plan_id aborts on service error
When r2plan-I call _resolve_active_plan_id with service error
Then r2plan-a typer Abort should be raised
Scenario: r2plan resolve_active_plan_id skips home fallback when DB env override is set
When r2plan-I call _resolve_active_plan_id with explicit DB env override and no active plans
Then r2plan-a typer Abort should be raised
And r2plan-the home DB fallback should not be attempted
Scenario: r2plan resolve_active_plan_id surfaces unexpected fallback errors
When r2plan-I call _resolve_active_plan_id with unexpected fallback error
Then r2plan-a RuntimeError should be raised
# ============================================================
# Originally from: plan_cli_spec_print_r2.feature
# Feature: Plan CLI spec dict and print branch coverage (round 2)
# ============================================================
Scenario: r2plan spec dict includes alias in project link
Given r2plan-a v3 Plan with a project link that has alias "backend"
When r2plan-I call _plan_spec_dict
Then r2plan-the spec dict project_links should include alias "backend"
Scenario: r2plan spec dict includes read_only in project link
Given r2plan-a v3 Plan with a project link that is read_only
When r2plan-I call _plan_spec_dict
Then r2plan-the spec dict project_links should include read_only true
Scenario: r2plan spec dict omits alias and read_only when unset
Given r2plan-a v3 Plan with a plain project link
When r2plan-I call _plan_spec_dict
Then r2plan-the spec dict project_links should not include alias
And r2plan-the spec dict project_links should not include read_only
# ===================================================================
# _plan_spec_dict automation_profile truthy / falsy
# ===================================================================
Scenario: r2plan spec dict includes automation_profile when set
Given r2plan-a v3 Plan with automation_profile "review"
When r2plan-I call _plan_spec_dict
Then r2plan-the spec dict automation_profile should be "review"
Scenario: r2plan spec dict has null automation_profile when unset
Given r2plan-a v3 Plan without automation_profile
When r2plan-I call _plan_spec_dict
Then r2plan-the spec dict automation_profile should be null
# ===================================================================
# _plan_spec_dict invariants, validation_summary/dod
# ===================================================================
Scenario: r2plan spec dict includes invariants when present
Given r2plan-a v3 Plan with invariants
When r2plan-I call _plan_spec_dict
Then r2plan-the spec dict should contain key "invariants"
And r2plan-the spec dict invariants count should be 2
Scenario: r2plan spec dict omits invariants when empty
Given r2plan-a v3 Plan without invariants
When r2plan-I call _plan_spec_dict
Then r2plan-the spec dict should not contain key "invariants"
Scenario: r2plan spec dict includes dod_evaluation when validated
Given r2plan-a v3 Plan with dod validation summary
When r2plan-I call _plan_spec_dict
Then r2plan-the spec dict should contain key "dod_evaluation"
And r2plan-the spec dict dod_evaluation all_passed should be true
Scenario: r2plan spec dict omits dod_evaluation when no validation
Given r2plan-a v3 Plan without validation_summary
When r2plan-I call _plan_spec_dict
Then r2plan-the spec dict should not contain key "dod_evaluation"
# ===================================================================
# _plan_spec_dict last_completed_step / last_checkpoint_id
# ===================================================================
Scenario: r2plan spec dict includes last_completed_step when >= 0
Given r2plan-a v3 Plan with last_completed_step 3
When r2plan-I call _plan_spec_dict
Then r2plan-the spec dict should contain key "last_completed_step"
And r2plan-the spec dict last_completed_step should be 3
Scenario: r2plan spec dict omits last_completed_step when -1
Given r2plan-a v3 Plan with last_completed_step default
When r2plan-I call _plan_spec_dict
Then r2plan-the spec dict should not contain key "last_completed_step"
Scenario: r2plan spec dict includes last_checkpoint_id when set
Given r2plan-a v3 Plan with last_checkpoint_id "01CHKPT001"
When r2plan-I call _plan_spec_dict
Then r2plan-the spec dict should contain key "last_checkpoint_id"
Scenario: r2plan spec dict omits last_checkpoint_id when None
Given r2plan-a v3 Plan without last_checkpoint_id
When r2plan-I call _plan_spec_dict
Then r2plan-the spec dict should not contain key "last_checkpoint_id"
# ===================================================================
# _print_lifecycle_plan definition_of_done branches
# ===================================================================
Scenario: r2plan print shows definition_of_done when short
Given r2plan-a v3 Plan with definition_of_done "All tests pass"
When r2plan-I call _print_lifecycle_plan
Then r2plan-the printed output should contain "Definition of Done"
And r2plan-the printed output should contain "All tests pass"
Scenario: r2plan print truncates long definition_of_done
Given r2plan-a v3 Plan with definition_of_done longer than 200 chars
When r2plan-I call _print_lifecycle_plan
Then r2plan-the printed output should contain "Definition of Done"
And r2plan-the printed output should contain "..."
Scenario: r2plan print omits definition_of_done when None
Given r2plan-a v3 Plan without definition_of_done
When r2plan-I call _print_lifecycle_plan
Then r2plan-the printed output should not contain "Definition of Done"
# ===================================================================
# _print_lifecycle_plan validation_summary dod pass / fail
# ===================================================================
Scenario: r2plan print shows DoD PASSED evaluation
Given r2plan-a v3 Plan with dod evaluated as passed
When r2plan-I call _print_lifecycle_plan
Then r2plan-the printed output should contain "DoD Evaluation"
And r2plan-the printed output should contain "PASSED"
Scenario: r2plan print shows DoD FAILED evaluation with failures
Given r2plan-a v3 Plan with dod evaluated as failed
When r2plan-I call _print_lifecycle_plan
Then r2plan-the printed output should contain "DoD Evaluation"
And r2plan-the printed output should contain "FAILED"
And r2plan-the printed output should contain "failed"
# ===================================================================
# _print_lifecycle_plan arguments with/without order
# ===================================================================
Scenario: r2plan print shows arguments using arguments_order
Given r2plan-a v3 Plan with arguments and arguments_order
When r2plan-I call _print_lifecycle_plan
Then r2plan-the printed output should contain "Arguments"
And r2plan-the printed output should contain "target_coverage = 80"
Scenario: r2plan print shows arguments sorted when no order
Given r2plan-a v3 Plan with arguments but no arguments_order
When r2plan-I call _print_lifecycle_plan
Then r2plan-the printed output should contain "Arguments"
# ===================================================================
# _print_lifecycle_plan long description > 200 chars
# ===================================================================
Scenario: r2plan print truncates long description
Given r2plan-a v3 Plan with description longer than 200 chars
When r2plan-I call _print_lifecycle_plan
Then r2plan-the printed output should contain "..."
# ===================================================================
# _print_lifecycle_plan automation_profile
# ===================================================================
Scenario: r2plan print shows automation profile
Given r2plan-a v3 Plan with automation_profile "review"
When r2plan-I call _print_lifecycle_plan
Then r2plan-the printed output should contain "Automation Profile"
And r2plan-the printed output should contain "review"
# ===================================================================
# _print_lifecycle_plan invariants display
# ===================================================================
Scenario: r2plan print shows invariants
Given r2plan-a v3 Plan with invariants
When r2plan-I call _print_lifecycle_plan
Then r2plan-the printed output should contain "Invariants"
# ===================================================================
# _print_lifecycle_plan resume metadata
# ===================================================================
Scenario: r2plan print shows resume metadata
Given r2plan-a v3 Plan with resume metadata
When r2plan-I call _print_lifecycle_plan
Then r2plan-the printed output should contain "Last Completed Step"
And r2plan-the printed output should contain "Last Checkpoint"
# ===================================================================
# _print_lifecycle_plan project link alias and read_only
# ===================================================================
Scenario: r2plan print shows project link with alias and read_only
Given r2plan-a v3 Plan with project link alias and read_only
When r2plan-I call _print_lifecycle_plan
Then r2plan-the printed output should contain "alias:"
And r2plan-the printed output should contain "local/ref-data"
# ============================================================
# Originally from: plan_execute_runtime.feature
# Feature: Plan Execute Runtime Integration
# ============================================================
@context
Scenario: Create PlanExecutionContext with required plan_id
Given a fresh in-memory changeset store
And a valid plan ID
When I create a PlanExecutionContext with a valid plan_id
Then the context plan_id should match the provided value
@context
Scenario: Create PlanExecutionContext with all optional fields
Given a fresh in-memory changeset store
And a valid plan ID
When I create a PlanExecutionContext with all optional fields
Then the context should have the correct decision_root_id
And the context should have the correct sandbox_root
And the context should have the correct automation_profile
@context @error
Scenario: PlanExecutionContext rejects empty plan_id
Given a fresh in-memory changeset store
And a valid plan ID
When I attempt to create a PlanExecutionContext with empty plan_id
Then the plan execution context should raise a ValidationError containing "plan_id"
@context @error
Scenario: PlanExecutionContext rejects None plan_id
Given a fresh in-memory changeset store
And a valid plan ID
When I attempt to create a PlanExecutionContext with None plan_id
Then the plan execution context should raise a ValidationError containing "plan_id"
@context
Scenario: PlanExecutionContext defaults to InMemoryChangeSetStore
Given a fresh in-memory changeset store
And a valid plan ID
When I create a PlanExecutionContext without specifying a changeset store
Then the context changeset_store should be an InMemoryChangeSetStore
@context
Scenario: PlanExecutionContext accepts custom changeset store
Given a fresh in-memory changeset store
And a valid plan ID
When I create a PlanExecutionContext with a custom changeset store
Then the context changeset_store should be the provided store
@context
Scenario: PlanExecutionContext defaults project_resources to empty dict
Given a fresh in-memory changeset store
And a valid plan ID
When I create a PlanExecutionContext with a valid plan_id
Then the context project_resources should be an empty dict
@context
Scenario: PlanExecutionContext defaults resource_bindings to empty dict
Given a fresh in-memory changeset store
And a valid plan ID
When I create a PlanExecutionContext with a valid plan_id
Then the context resource_bindings should be an empty dict
# -- Changeset operations --
@changeset
Scenario: start_changeset creates a changeset with ULID
Given a fresh in-memory changeset store
And a valid plan ID
Given a PlanExecutionContext
When I call start_changeset
Then the returned changeset_id should be a valid ULID string
And the active_changeset_ids should contain the new ID
@changeset
Scenario: record_change persists an entry into the active changeset
Given a fresh in-memory changeset store
And a valid plan ID
Given a PlanExecutionContext with an active changeset
When I record a ChangeEntry into the context
Then the changeset should contain the recorded entry
@changeset
Scenario: record_change without active changeset raises PlanError
Given a fresh in-memory changeset store
And a valid plan ID
Given a PlanExecutionContext without an active changeset
When I attempt to record a ChangeEntry
Then a PlanError should be raised about no active changeset
@changeset
Scenario: Multiple changes recorded into same changeset
Given a fresh in-memory changeset store
And a valid plan ID
Given a PlanExecutionContext with an active changeset
When I record three ChangeEntry instances
Then the changeset should contain exactly three entries
@changeset
Scenario: get_changeset retrieves existing changeset
Given a fresh in-memory changeset store
And a valid plan ID
Given a PlanExecutionContext with an active changeset and one recorded entry
When I call get_changeset with the active changeset_id
Then the returned changeset should not be None
And the changeset plan_id should match
@changeset
Scenario: get_changeset returns None for unknown ID
Given a fresh in-memory changeset store
And a valid plan ID
Given a PlanExecutionContext
When I call get_changeset with a nonexistent ID
Then the returned changeset should be None
# -- Summarize --
@summarize
Scenario: summarize includes all expected fields
Given a fresh in-memory changeset store
And a valid plan ID
Given a PlanExecutionContext with an active changeset and one recorded entry
When I call summarize on the context
Then the summary should contain plan_id
And the summary should contain resource_binding_count of 0
And the summary should contain decision_root_id
@summarize
Scenario: summarize with no changesets shows zero resource counts
Given a fresh in-memory changeset store
And a valid plan ID
Given a PlanExecutionContext
When I call summarize on the context
Then the summary resource_binding_count should be 0
# -- RuntimeExecuteResult model --
@model
Scenario: RuntimeExecuteResult validates with valid data
Given a fresh in-memory changeset store
And a valid plan ID
When I create a RuntimeExecuteResult with valid fields
Then the runtime execute result should have a correct changeset_id
And the runtime execute result tool_call_count should be non-negative
@model @error
Scenario: RuntimeExecuteResult rejects negative tool_call_count
Given a fresh in-memory changeset store
And a valid plan ID
When I attempt to create a RuntimeExecuteResult with negative tool_call_count
Then the runtime result should fail validation
@model @error
Scenario: RuntimeExecuteResult rejects empty changeset_id
Given a fresh in-memory changeset store
And a valid plan ID
When I attempt to create a RuntimeExecuteResult with empty changeset_id
Then the runtime result should fail validation
# -- RuntimeExecuteActor --
@actor
Scenario: RuntimeExecuteActor executes with stub decisions
Given a fresh in-memory changeset store
And a valid plan ID
Given a PlanExecutionContext
And a ToolRunner with an empty registry
And a RuntimeExecuteActor
When I execute with a list of two stub decisions
Then the runtime execute result should be a RuntimeExecuteResult
And the runtime execute result changeset_id should be a valid ULID
And the runtime execute result decision_ids_processed should have two entries
@actor
Scenario: RuntimeExecuteActor with streaming callback
Given a fresh in-memory changeset store
And a valid plan ID
Given a PlanExecutionContext
And a ToolRunner with an empty registry
And a RuntimeExecuteActor
And a stream callback collector
When I execute with one stub decision and the stream callback
Then the callback should have received runtime_execute_started event
And the callback should have received runtime_execute_step event
And the callback should have received runtime_execute_complete event
@actor
Scenario: RuntimeExecuteActor records sandbox_refs when sandbox_root is set
Given a fresh in-memory changeset store
And a valid plan ID
Given a PlanExecutionContext with sandbox_root "/tmp/sandbox"
And a ToolRunner with an empty registry
And a RuntimeExecuteActor
When I execute with one stub decision
Then the runtime execute result sandbox_refs should contain "/tmp/sandbox"
@actor @error
Scenario: RuntimeExecuteActor rejects None tool_runner
Given a fresh in-memory changeset store
And a valid plan ID
Given a PlanExecutionContext
When I attempt to create a RuntimeExecuteActor with None tool_runner
Then the plan execution context should raise a ValidationError
@actor @error
Scenario: RuntimeExecuteActor rejects None execution_context
Given a fresh in-memory changeset store
And a valid plan ID
Given a ToolRunner with an empty registry
When I attempt to create a RuntimeExecuteActor with None execution_context
Then the plan execution context should raise a ValidationError
# -- PlanExecutor integration --
@executor
Scenario: PlanExecutor has_runtime is True with tool_calling_runtime
Given a fresh in-memory changeset store
And a valid plan ID
Given a mock lifecycle service
And a PlanExecutionContext
And a ToolRunner with an empty registry
When I create a PlanExecutor with a mock tool_calling_runtime
Then has_runtime should be True
@executor
Scenario: PlanExecutor has_runtime is False without execution_context
Given a fresh in-memory changeset store
And a valid plan ID
Given a mock lifecycle service
When I create a PlanExecutor without execution_context
Then has_runtime should be False
@executor
Scenario: PlanExecutor changeset_store returns store from context
Given a fresh in-memory changeset store
And a valid plan ID
Given a mock lifecycle service
And a PlanExecutionContext with a known changeset store
When I create a PlanExecutor with the execution_context
Then changeset_store should be the same object as the context store
@executor
Scenario: PlanExecutor changeset_store returns None without context
Given a fresh in-memory changeset store
And a valid plan ID
Given a mock lifecycle service
When I create a PlanExecutor without execution_context
Then changeset_store should be None
@executor
Scenario: PlanExecutor execution_context property returns context
Given a fresh in-memory changeset store
And a valid plan ID
Given a mock lifecycle service
And a PlanExecutionContext
When I create a PlanExecutor with the execution_context
Then execution_context property should return the context
# -- Error handling --
@error @executor
Scenario: PlanExecutor rejects None lifecycle_service
Given a fresh in-memory changeset store
And a valid plan ID
When I attempt to create a PlanExecutor with None lifecycle_service
Then the plan execution context should raise a ValidationError containing "lifecycle_service"
@error @executor
Scenario: PlanExecutor run_execute rejects empty plan_id
Given a fresh in-memory changeset store
And a valid plan ID
Given a mock lifecycle service
And a PlanExecutor without execution_context
When I attempt to run execute with an empty plan_id
Then the plan execution context should raise a ValidationError containing "plan_id"