forked from cleveragents/cleveragents-core
84b0c10dbf
## 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>
1051 lines
43 KiB
Gherkin
1051 lines
43 KiB
Gherkin
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"
|