ec4c39aecf
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 17s
CI / build (pull_request) Successful in 28s
CI / quality (pull_request) Successful in 34s
CI / security (pull_request) Successful in 42s
CI / typecheck (pull_request) Successful in 45s
CI / unit_tests (pull_request) Successful in 3m11s
CI / integration_tests (pull_request) Successful in 3m37s
CI / e2e_tests (pull_request) Successful in 4m1s
CI / docker (pull_request) Successful in 56s
CI / coverage (pull_request) Successful in 6m37s
CI / lint (push) Successful in 11s
CI / build (push) Successful in 15s
CI / quality (push) Successful in 32s
CI / typecheck (push) Successful in 44s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 56s
CI / unit_tests (push) Successful in 2m55s
CI / integration_tests (push) Successful in 3m34s
CI / docker (push) Successful in 59s
CI / e2e_tests (push) Successful in 3m59s
CI / coverage (push) Successful in 6m55s
CI / benchmark-publish (push) Has been cancelled
CI / benchmark-regression (pull_request) Successful in 37m40s
Replace the simulated rollback in CheckpointService.rollback_to_checkpoint() with real git operations. The method now executes git reset --hard <sandbox_ref> followed by git clean -fd inside the sandbox working directory, reverting all tracked file changes and removing untracked files added after the checkpoint. Key changes: - CheckpointService.rollback_to_checkpoint() now calls _git_reset_hard() and _git_clean() via subprocess against the sandbox path, enforcing sandbox boundary by confining all git operations to cwd=sandbox_path. - Added _resolve_sandbox_path() to extract sandbox validation into a dedicated method, supporting both lifecycle-service and in-memory fallback paths. - Added _validate_sandbox() to verify the sandbox path is a directory containing a .git subdirectory before executing git operations. - Added _git_changed_paths() to compute the diff between HEAD and the target ref before reset, providing accurate restored file counts in the result. - Domain event emission (CHECKPOINT_RESTORED) added via the optional event_bus, following the same EventBus protocol pattern used by other services. - Updated existing Robot Framework helper (helper_checkpoint_rollback.py) to use a real temporary git workspace instead of a fake sandbox path, since _validate_sandbox() now enforces that the path is a real git repository. - Removed @tdd_expected_fail tags from TDD test files since the bug is now fixed. - Added new Behave scenarios and Robot integration tests for real file reversion, file removal after rollback, domain event emission, and sandbox boundary enforcement. ISSUES CLOSED: #822
334 lines
19 KiB
Gherkin
334 lines
19 KiB
Gherkin
@phase2 @checkpoint @rollback
|
|
Feature: Checkpoint and rollback
|
|
As an operator
|
|
I want to create checkpoints during plan execution and rollback to them
|
|
So that I can recover from mistakes without losing all progress
|
|
|
|
# ───────────────────────────────────────────────────────────
|
|
# Domain model scenarios
|
|
# ───────────────────────────────────────────────────────────
|
|
|
|
Scenario: Checkpoint model validates ULID fields
|
|
Given a valid checkpoint with plan_id "01ARZ3NDEKTSV4RRFFQ69G5FAV" and sandbox_ref "abc123"
|
|
Then the checkpoint plan_id should be "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
And the checkpoint sandbox_ref should be "abc123"
|
|
|
|
Scenario: Checkpoint metadata stores reason and source tool
|
|
Given a checkpoint with reason "pre-execution" and source_tool "lint-check" and phase "execute"
|
|
Then the checkpoint metadata reason should be "pre-execution"
|
|
And the checkpoint metadata source_tool should be "lint-check"
|
|
And the checkpoint metadata phase should be "execute"
|
|
|
|
Scenario: Checkpoint retention policy defaults
|
|
Given a default retention policy
|
|
Then the max_checkpoints should be 50
|
|
And auto_prune should be true
|
|
|
|
Scenario: Checkpoint retention policy custom values
|
|
Given a retention policy with max_checkpoints 5 and auto_prune false
|
|
Then the max_checkpoints should be 5
|
|
And auto_prune should be false
|
|
|
|
Scenario: RollbackResult model stores restored file count
|
|
Given a rollback result with 3 restored files and checkpoint "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
Then the restored_files_count should be 3
|
|
And the from_checkpoint_id should be "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
|
|
# ───────────────────────────────────────────────────────────
|
|
# CheckpointService scenarios
|
|
# ───────────────────────────────────────────────────────────
|
|
|
|
Scenario: Create a checkpoint
|
|
Given a checkpoint service
|
|
When I create a checkpoint for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with sandbox_ref "commit-abc"
|
|
Then the checkpoint should be created successfully
|
|
And the checkpoint plan_id should match "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
|
|
Scenario: List checkpoints for a plan
|
|
Given a checkpoint service
|
|
And I create a checkpoint for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with sandbox_ref "commit-1"
|
|
And I create a checkpoint for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with sandbox_ref "commit-2"
|
|
When I list checkpoints for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
Then I should see 2 checkpoints
|
|
|
|
Scenario: Rollback to checkpoint succeeds
|
|
Given a checkpoint service with sandbox for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
And a checkpoint is created from the sandbox HEAD for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
And a file is modified in the sandbox after the checkpoint
|
|
When I rollback plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" to the created checkpoint
|
|
Then the rollback should succeed
|
|
|
|
Scenario: Rollback rejected when plan is applied
|
|
Given a checkpoint service
|
|
And plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" is marked as applied
|
|
And I create a checkpoint for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with sandbox_ref "commit-abc"
|
|
When I attempt rollback plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" to the created checkpoint
|
|
Then the rollback should be rejected with "plan is already applied"
|
|
|
|
Scenario: Rollback rejected when sandbox is missing
|
|
Given a checkpoint service
|
|
And I create a checkpoint for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with sandbox_ref "commit-abc"
|
|
When I attempt rollback plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" to the created checkpoint
|
|
Then the rollback should be rejected with "sandbox is missing"
|
|
|
|
Scenario: Rollback with non-existent checkpoint
|
|
Given a checkpoint service with sandbox for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
When I attempt rollback plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" to checkpoint "01NONEXISTENT0000000000000"
|
|
Then the rollback should fail with not found error
|
|
|
|
Scenario: Delete a checkpoint
|
|
Given a checkpoint service
|
|
And I create a checkpoint for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with sandbox_ref "commit-abc"
|
|
When I delete the created checkpoint
|
|
Then the checkpoint should be deleted
|
|
|
|
Scenario: Prune enforces retention policy
|
|
Given a checkpoint service
|
|
And I create 5 checkpoints for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
When I prune checkpoints with max 3
|
|
Then 2 checkpoints should be pruned
|
|
And 3 checkpoint snapshots should remain
|
|
|
|
Scenario: Auto-prune on create with retention policy
|
|
Given a checkpoint service
|
|
And I create 10 checkpoints for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
When I create a checkpoint with retention policy max 5
|
|
Then 6 checkpoints should have been pruned on creation
|
|
And 5 checkpoint snapshots should remain
|
|
|
|
Scenario: Create checkpoint with empty plan_id raises error
|
|
Given a checkpoint service
|
|
When I attempt to create a checkpoint with empty plan_id
|
|
Then a checkpoint validation error should be raised
|
|
|
|
Scenario: Create checkpoint with empty sandbox_ref raises error
|
|
Given a checkpoint service
|
|
When I attempt to create a checkpoint with empty sandbox_ref
|
|
Then a checkpoint validation error should be raised
|
|
|
|
Scenario: Get checkpoint by ID
|
|
Given a checkpoint service
|
|
And I create a checkpoint for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with sandbox_ref "commit-xyz"
|
|
When I get the checkpoint by its ID
|
|
Then the checkpoint should be returned successfully
|
|
|
|
Scenario: Get non-existent checkpoint raises error
|
|
Given a checkpoint service
|
|
When I attempt to get checkpoint "01NONEXISTENT0000000000000"
|
|
Then a checkpoint not found error should be raised
|
|
|
|
# ───────────────────────────────────────────────────────────
|
|
# Correction service rollback integration
|
|
# ───────────────────────────────────────────────────────────
|
|
|
|
Scenario: Correction service accepts checkpoint_service parameter
|
|
Given a correction service with checkpoint support
|
|
Then the correction service should have checkpoint support
|
|
|
|
Scenario: Checkpoint metadata reason stored for auditability
|
|
Given a checkpoint service
|
|
When I create a checkpoint with reason "pre-tool-run" and source_tool "format-code" and phase "execute"
|
|
Then the stored checkpoint metadata should have reason "pre-tool-run"
|
|
And the stored checkpoint metadata should have source_tool "format-code"
|
|
And the stored checkpoint metadata should have phase "execute"
|
|
|
|
# ───────────────────────────────────────────────────────────
|
|
# Spec-aligned field scenarios (decision_id, checkpoint_type, etc.)
|
|
# ───────────────────────────────────────────────────────────
|
|
|
|
Scenario: Checkpoint stores decision_id when aligned to a decision
|
|
Given a checkpoint service
|
|
When I create a checkpoint aligned to decision "01DEC1S10N0000000000000000"
|
|
Then the stored checkpoint decision_id should be "01DEC1S10N0000000000000000"
|
|
|
|
Scenario: Checkpoint stores checkpoint_type
|
|
Given a checkpoint service
|
|
When I create a pre_write checkpoint for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
Then the stored checkpoint_type should be "pre_write"
|
|
|
|
Scenario: Checkpoint type validation rejects invalid values
|
|
Given a checkpoint service
|
|
When I attempt to create a checkpoint with type "invalid_type"
|
|
Then a checkpoint validation error should be raised
|
|
|
|
Scenario: Checkpoint stores resource_id
|
|
Given a checkpoint service
|
|
When I create a checkpoint with resource "01RES0URCE0000000000000000"
|
|
Then the stored checkpoint resource_id should be "01RES0URCE0000000000000000"
|
|
|
|
Scenario: Checkpoint stores filesystem_path and size_bytes
|
|
Given a checkpoint service
|
|
When I create a checkpoint with path "snapshots/cp001" and size 4096
|
|
Then the stored checkpoint filesystem_path should be "snapshots/cp001"
|
|
And the stored checkpoint size_bytes should be 4096
|
|
|
|
Scenario: Prune preserves first and most recent checkpoints
|
|
Given a checkpoint service
|
|
And I create 5 checkpoints for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
When I prune checkpoints with max 2
|
|
Then 3 checkpoints should be pruned
|
|
And 2 checkpoint snapshots should remain
|
|
And the first checkpoint should survive pruning
|
|
And the most recent checkpoint should survive pruning
|
|
|
|
# ───────────────────────────────────────────────────────────
|
|
# Persistent guard scenarios (via PlanLifecycleService)
|
|
# ───────────────────────────────────────────────────────────
|
|
|
|
Scenario: Rollback rejected via lifecycle service when plan is applied
|
|
Given a checkpoint service backed by a lifecycle service with an applied plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
And I create a checkpoint for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with sandbox_ref "commit-abc"
|
|
When I attempt rollback plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" to the created checkpoint
|
|
Then the rollback should be rejected with "plan is already applied"
|
|
|
|
Scenario: Rollback rejected via lifecycle service when sandbox refs are empty
|
|
Given a checkpoint service backed by a lifecycle service with no sandbox for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
And I create a checkpoint for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with sandbox_ref "commit-abc"
|
|
When I attempt rollback plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" to the created checkpoint
|
|
Then the rollback should be rejected with "sandbox is missing"
|
|
|
|
Scenario: Rollback succeeds via lifecycle service when plan has sandbox
|
|
Given a checkpoint service backed by a lifecycle service with real sandbox for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
And a checkpoint is created from the sandbox HEAD for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
And a file is modified in the sandbox after the checkpoint
|
|
When I rollback plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" to the created checkpoint
|
|
Then the rollback should succeed
|
|
|
|
# ───────────────────────────────────────────────────────────
|
|
# Default retention policy scenario
|
|
# ───────────────────────────────────────────────────────────
|
|
|
|
Scenario: Default retention policy applied when none specified
|
|
Given a checkpoint service
|
|
And I create 55 checkpoints for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
When I create a checkpoint for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with sandbox_ref "commit-final"
|
|
Then at most 50 checkpoint snapshots should remain for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
|
|
Scenario: Rollback rejected when checkpoint belongs to different plan
|
|
Given a checkpoint service with sandbox for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
And I create a checkpoint for plan "01KJGSHD2JKTFZGGBT6FRQD2W2" with sandbox_ref "commit-other"
|
|
When I attempt rollback plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" to the created checkpoint
|
|
Then the rollback should be rejected with "does not belong to plan"
|
|
|
|
Scenario: Unregister sandbox prevents rollback
|
|
Given a checkpoint service with sandbox for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
And I create a checkpoint for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with sandbox_ref "commit-abc"
|
|
And the sandbox for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" is unregistered
|
|
When I attempt rollback plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" to the created checkpoint
|
|
Then the rollback should be rejected with "sandbox is missing"
|
|
|
|
Scenario: Prune with auto_prune disabled returns empty
|
|
Given a checkpoint service
|
|
And I create 5 checkpoints for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
When I prune checkpoints with auto_prune disabled
|
|
Then 0 checkpoints should be pruned
|
|
|
|
# ───────────────────────────────────────────────────────────
|
|
# Repository-backed service scenarios
|
|
# ───────────────────────────────────────────────────────────
|
|
|
|
Scenario: Repository-backed create persists via repository
|
|
Given a checkpoint service backed by a stub repository
|
|
When I create a checkpoint for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with sandbox_ref "commit-repo"
|
|
Then the checkpoint should be created successfully
|
|
And the stub repository should have 1 checkpoint stored
|
|
|
|
Scenario: Repository-backed list delegates to repository
|
|
Given a checkpoint service backed by a stub repository
|
|
And I create a checkpoint for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with sandbox_ref "commit-r1"
|
|
And I create a checkpoint for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with sandbox_ref "commit-r2"
|
|
When I list checkpoints for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
Then I should see 2 checkpoints
|
|
|
|
Scenario: Repository-backed delete removes via repository
|
|
Given a checkpoint service backed by a stub repository
|
|
And I create a checkpoint for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with sandbox_ref "commit-del"
|
|
When I delete the created checkpoint
|
|
Then the checkpoint should be deleted
|
|
|
|
Scenario: Repository-backed delete raises error when not found
|
|
Given a checkpoint service backed by a stub repository
|
|
When I attempt to delete checkpoint "01NONEXISTENT0000000000000"
|
|
Then a checkpoint not found error should be raised
|
|
|
|
Scenario: Repository-backed prune delegates to repository
|
|
Given a checkpoint service backed by a stub repository
|
|
And I create 5 checkpoints for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
When I prune checkpoints with max 3
|
|
Then 2 checkpoints should be pruned
|
|
|
|
Scenario: Repository-backed get retrieves via repository
|
|
Given a checkpoint service backed by a stub repository
|
|
And I create a checkpoint for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with sandbox_ref "commit-get"
|
|
When I get the checkpoint by its ID
|
|
Then the checkpoint should be returned successfully
|
|
|
|
Scenario: Repository-backed get raises not found for missing checkpoint
|
|
Given a checkpoint service backed by a stub repository
|
|
When I attempt to get checkpoint "01NONEXISTENT0000000000000"
|
|
Then a checkpoint not found error should be raised
|
|
|
|
# ───────────────────────────────────────────────────────────
|
|
# In-memory edge case scenarios
|
|
# ───────────────────────────────────────────────────────────
|
|
|
|
Scenario: Delete non-existent checkpoint from in-memory store raises error
|
|
Given a checkpoint service
|
|
When I attempt to delete checkpoint "01NONEXISTENT0000000000000"
|
|
Then a checkpoint not found error should be raised
|
|
|
|
Scenario: List checkpoints tolerates orphaned index entries
|
|
Given a checkpoint service with an orphaned plan index entry
|
|
When I list checkpoints for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
Then I should see 1 checkpoints
|
|
|
|
Scenario: Delete checkpoint whose plan index is missing skips cleanup
|
|
Given a checkpoint service with a checkpoint missing from plan index
|
|
When I delete the created checkpoint
|
|
Then the checkpoint should be deleted
|
|
|
|
# ───────────────────────────────────────────────────────────
|
|
# Domain model validator edge cases
|
|
# ───────────────────────────────────────────────────────────
|
|
|
|
Scenario: Checkpoint ULID validator rejects empty string
|
|
When I invoke the checkpoint ULID validator with an empty string
|
|
Then the checkpoint validator should raise ValueError "ULID field must not be empty"
|
|
|
|
# ───────────────────────────────────────────────────────────
|
|
# Real git rollback verification scenarios (bug #822)
|
|
# ───────────────────────────────────────────────────────────
|
|
|
|
Scenario: Rollback reverts modified file content to checkpoint state
|
|
Given a checkpoint service with sandbox for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
And a checkpoint is created from the sandbox HEAD for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
And a tracked sandbox file is modified and committed after the checkpoint
|
|
When I rollback plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" to the created checkpoint
|
|
Then the sandbox file content should match the pre-checkpoint state
|
|
|
|
Scenario: Rollback removes files added after the checkpoint
|
|
Given a checkpoint service with sandbox for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
And a checkpoint is created from the sandbox HEAD for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
And a new file is added and committed in the sandbox after the checkpoint
|
|
When I rollback plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" to the created checkpoint
|
|
Then the file added after the checkpoint should not exist
|
|
|
|
Scenario: Rollback emits CheckpointRolledBack domain event
|
|
Given a checkpoint service with sandbox and event bus for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
And a checkpoint is created from the sandbox HEAD for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
And a file is modified in the sandbox after the checkpoint
|
|
When I rollback plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" to the created checkpoint
|
|
Then a CHECKPOINT_RESTORED domain event should have been emitted
|
|
|
|
Scenario: Rollback rejects sandbox path that does not exist
|
|
Given a checkpoint service with nonexistent sandbox path for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
And I create a checkpoint for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with sandbox_ref "abc123"
|
|
When I attempt rollback plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" to the created checkpoint
|
|
Then the rollback should be rejected with "sandbox path does not exist"
|
|
|
|
Scenario: Rollback rejects sandbox path that is not a git repo
|
|
Given a checkpoint service with non-git sandbox path for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
And I create a checkpoint for plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" with sandbox_ref "abc123"
|
|
When I attempt rollback plan "01ARZ3NDEKTSV4RRFFQ69G5FAV" to the created checkpoint
|
|
Then the rollback should be rejected with "not a git repository"
|