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"