beceb183d0
CheckpointManager.create_checkpoint() computed sandbox_path from
sandbox.context.sandbox_path but never stored it in the metadata dict.
This caused rollback_to() to always find metadata.get('sandbox_path')
returning None, silently skip the rollback, and return False.
The fix adds sandbox_path to the metadata dict immediately after it is
resolved from the sandbox context, before the SandboxCheckpoint is
constructed. rollback_to() can now retrieve the path and correctly
restore the sandbox filesystem state.
Added two new BDD scenarios to checkpoint_manager_coverage.feature:
- Verifies sandbox_path is automatically stored in metadata on create
- Verifies rollback succeeds without manually supplying sandbox_path
ISSUES CLOSED: #7488
213 lines
11 KiB
Gherkin
213 lines
11 KiB
Gherkin
Feature: CheckpointManager full coverage
|
||
As a developer
|
||
I want thorough tests for every method, branch, and edge case in checkpoint.py
|
||
So that line-rate and branch-rate reach 1.0
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# SandboxCheckpoint Pydantic model
|
||
# ---------------------------------------------------------------------------
|
||
|
||
Scenario: SandboxCheckpoint model can be constructed with all fields
|
||
Given I build a SandboxCheckpoint with valid fields
|
||
Then the constructed checkpoint model should expose all field values
|
||
And the constructed checkpoint model should be frozen
|
||
|
||
Scenario: SandboxCheckpoint model uses empty dict as default metadata
|
||
Given I build a SandboxCheckpoint without explicit metadata
|
||
Then the constructed checkpoint metadata should be an empty dict
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Checkpointable protocol
|
||
# ---------------------------------------------------------------------------
|
||
|
||
Scenario: An object satisfying Checkpointable passes isinstance check
|
||
Given a mock object with sandbox_id and context properties
|
||
Then the mock object should satisfy the Checkpointable protocol
|
||
|
||
Scenario: An object missing sandbox_id fails Checkpointable isinstance check
|
||
Given a mock object without a sandbox_id property
|
||
Then the mock object should not satisfy the Checkpointable protocol
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# CheckpointManager.__init__
|
||
# ---------------------------------------------------------------------------
|
||
|
||
Scenario: CheckpointManager initialises with empty state
|
||
Given a freshly created CheckpointManager
|
||
Then the manager internal checkpoints dict should be empty
|
||
And the manager should have a threading lock
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# create_checkpoint – context is None (metadata-only)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
Scenario: Creating a checkpoint when sandbox context is None produces a metadata-only snapshot
|
||
Given a freshly created CheckpointManager
|
||
And a mock sandbox whose context is None
|
||
When I invoke create_checkpoint with plan "plan-m1" phase "pre_execute" and no metadata
|
||
Then the returned checkpoint should have a non-empty snapshot_path directory
|
||
And the returned checkpoint sandbox_id should equal the mock sandbox id
|
||
|
||
Scenario: Creating a checkpoint with explicit metadata converts values to strings
|
||
Given a freshly created CheckpointManager
|
||
And a mock sandbox whose context is None
|
||
When I invoke create_checkpoint with plan "plan-m2" phase "post_execute" and numeric metadata
|
||
Then the returned checkpoint metadata values should all be strings
|
||
|
||
Scenario: Creating a checkpoint with None metadata defaults to empty dict
|
||
Given a freshly created CheckpointManager
|
||
And a mock sandbox whose context is None
|
||
When I invoke create_checkpoint with plan "plan-m3" phase "pre_apply" and None metadata
|
||
Then the returned checkpoint metadata should be an empty dict
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# create_checkpoint – context with sandbox_path
|
||
# ---------------------------------------------------------------------------
|
||
|
||
Scenario: Creating a checkpoint with a real sandbox path snapshots the directory
|
||
Given a freshly created CheckpointManager
|
||
And a temporary sandbox directory with sample files
|
||
And a mock sandbox whose context points to the temporary directory
|
||
When I invoke create_checkpoint with plan "plan-s1" phase "pre_execute" and no metadata
|
||
Then the snapshot directory should contain copies of the sample files
|
||
|
||
Scenario: Creating a checkpoint with a real sandbox path stores sandbox_path in metadata
|
||
Given a freshly created CheckpointManager
|
||
And a temporary sandbox directory with sample files
|
||
And a mock sandbox whose context points to the temporary directory
|
||
When I invoke create_checkpoint with plan "plan-s2" phase "pre_execute" and no metadata
|
||
Then the returned checkpoint metadata should contain the sandbox path
|
||
|
||
Scenario: Rollback succeeds without manually supplying sandbox_path in metadata
|
||
Given a freshly created CheckpointManager
|
||
And a temporary sandbox directory with sample files
|
||
And a mock sandbox whose context points to the temporary directory
|
||
And a checkpoint was created for the temporary sandbox without explicit sandbox_path metadata
|
||
And the temporary sandbox files are then modified
|
||
When I invoke rollback_to on the created checkpoint
|
||
Then the rollback result should be true
|
||
And the temporary sandbox should contain the original files
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# _snapshot_directory – branches
|
||
# ---------------------------------------------------------------------------
|
||
|
||
Scenario: _snapshot_directory with None path creates an empty snapshot dir
|
||
Given a freshly created CheckpointManager
|
||
When I call _snapshot_directory with a None sandbox_path
|
||
Then the returned snapshot should be an existing empty directory
|
||
|
||
Scenario: _snapshot_directory with non-existent path creates an empty snapshot dir
|
||
Given a freshly created CheckpointManager
|
||
When I call _snapshot_directory with a non-existent sandbox_path
|
||
Then the returned snapshot should be an existing empty directory
|
||
|
||
Scenario: _snapshot_directory with a valid path copies the tree
|
||
Given a freshly created CheckpointManager
|
||
And a temporary sandbox directory with sample files
|
||
When I call _snapshot_directory with the temporary sandbox_path
|
||
Then the returned snapshot should contain the sample files
|
||
|
||
Scenario: _snapshot_directory raises SandboxError when copytree fails
|
||
Given a freshly created CheckpointManager
|
||
And a temporary sandbox directory with sample files
|
||
When I call _snapshot_directory with a path that triggers OSError
|
||
Then a SandboxError should have been raised
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# _cleanup_snapshot – branches
|
||
# ---------------------------------------------------------------------------
|
||
|
||
Scenario: _cleanup_snapshot with empty string is a no-op
|
||
Given a freshly created CheckpointManager
|
||
When I call _cleanup_snapshot with an empty string
|
||
Then no error should occur from cleanup_snapshot
|
||
|
||
Scenario: _cleanup_snapshot removes parent directory when it exists
|
||
Given a freshly created CheckpointManager
|
||
And a temporary snapshot directory tree for cleanup
|
||
When I call _cleanup_snapshot with the temporary snapshot path
|
||
Then the temporary snapshot parent should no longer exist
|
||
|
||
Scenario: _cleanup_snapshot with non-existent parent is a no-op
|
||
Given a freshly created CheckpointManager
|
||
When I call _cleanup_snapshot with a non-existent path
|
||
Then no error should occur from cleanup_snapshot
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# rollback_to – all branches
|
||
# ---------------------------------------------------------------------------
|
||
|
||
Scenario: Rollback returns false when snapshot_path is empty string
|
||
Given a freshly created CheckpointManager
|
||
And a SandboxCheckpoint with empty snapshot_path
|
||
When I invoke rollback_to on the crafted checkpoint
|
||
Then the rollback result should be false
|
||
|
||
Scenario: Rollback returns false when snapshot_path does not exist on disk
|
||
Given a freshly created CheckpointManager
|
||
And a SandboxCheckpoint with a non-existent snapshot_path
|
||
When I invoke rollback_to on the crafted checkpoint
|
||
Then the rollback result should be false
|
||
|
||
Scenario: Rollback returns false when sandbox_path is missing from metadata
|
||
Given a freshly created CheckpointManager
|
||
And a SandboxCheckpoint whose snapshot exists but metadata has no sandbox_path
|
||
When I invoke rollback_to on the crafted checkpoint
|
||
Then the rollback result should be false
|
||
|
||
Scenario: Rollback returns false when sandbox_path directory does not exist
|
||
Given a freshly created CheckpointManager
|
||
And a SandboxCheckpoint whose snapshot exists but sandbox_path points nowhere
|
||
When I invoke rollback_to on the crafted checkpoint
|
||
Then the rollback result should be false
|
||
|
||
Scenario: Rollback succeeds and restores files and subdirectories
|
||
Given a freshly created CheckpointManager
|
||
And a temporary sandbox directory with sample files
|
||
And a mock sandbox whose context points to the temporary directory
|
||
And a checkpoint was created for the temporary sandbox
|
||
And the temporary sandbox files are then modified
|
||
When I invoke rollback_to on the created checkpoint
|
||
Then the rollback result should be true
|
||
And the temporary sandbox should contain the original files
|
||
|
||
Scenario: Rollback handles OSError during restore gracefully
|
||
Given a freshly created CheckpointManager
|
||
And a SandboxCheckpoint whose snapshot and sandbox exist but restore will fail
|
||
When I invoke rollback_to on the crafted checkpoint
|
||
Then the rollback result should be false
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# list_checkpoints – branches
|
||
# ---------------------------------------------------------------------------
|
||
|
||
Scenario: Listing checkpoints for unknown sandbox returns empty list
|
||
Given a freshly created CheckpointManager
|
||
When I invoke list_checkpoints for sandbox "unknown-sb-99"
|
||
Then the checkpoint list should be empty
|
||
|
||
Scenario: Listing checkpoints returns all created checkpoints in order
|
||
Given a freshly created CheckpointManager
|
||
And a mock sandbox whose context is None
|
||
And I create two checkpoints for the mock sandbox
|
||
When I invoke list_checkpoints for the mock sandbox
|
||
Then the checkpoint list should have 2 entries in creation order
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# delete_checkpoint – branches
|
||
# ---------------------------------------------------------------------------
|
||
|
||
Scenario: Deleting a known checkpoint returns true and removes it
|
||
Given a freshly created CheckpointManager
|
||
And a mock sandbox whose context is None
|
||
And a single checkpoint is created for the mock sandbox
|
||
When I invoke delete_checkpoint with the created checkpoint id
|
||
Then the checkpoint delete result should be true
|
||
And listing checkpoints for the mock sandbox should return 0 entries
|
||
|
||
Scenario: Deleting an unknown checkpoint id returns false
|
||
Given a freshly created CheckpointManager
|
||
When I invoke delete_checkpoint with id "no-such-cp-id"
|
||
Then the checkpoint delete result should be false
|