197 lines
6.3 KiB
Python
197 lines
6.3 KiB
Python
"""Tests for CLI rollback command."""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from datetime import datetime, timezone
|
|
|
|
import pytest
|
|
from click.testing import CliRunner
|
|
|
|
from vlm.cli import main
|
|
from vlm.models import FileOperation, OperationResult, RollbackLog
|
|
|
|
|
|
@pytest.fixture
|
|
def cli_runner():
|
|
"""Create a Click CLI test runner."""
|
|
return CliRunner()
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_rollback_log(tmp_path):
|
|
"""Create a sample rollback log file for testing."""
|
|
# Create test files
|
|
source1 = tmp_path / "source1.txt"
|
|
source2 = tmp_path / "source2.txt"
|
|
dest1 = tmp_path / "dest1.txt"
|
|
dest2 = tmp_path / "dest2.txt"
|
|
|
|
source1.write_text("content1")
|
|
source2.write_text("content2")
|
|
|
|
# Move files to simulate execution
|
|
source1.rename(dest1)
|
|
source2.rename(dest2)
|
|
|
|
# Create rollback log
|
|
operations = [
|
|
OperationResult(
|
|
operation=FileOperation(
|
|
operation_type="move",
|
|
source_path=source1,
|
|
destination_path=dest1,
|
|
reason="test move 1",
|
|
has_conflict=False,
|
|
conflict_reason=None
|
|
),
|
|
success=True,
|
|
error_message=None,
|
|
executed_at=datetime.now(timezone.utc)
|
|
),
|
|
OperationResult(
|
|
operation=FileOperation(
|
|
operation_type="move",
|
|
source_path=source2,
|
|
destination_path=dest2,
|
|
reason="test move 2",
|
|
has_conflict=False,
|
|
conflict_reason=None
|
|
),
|
|
success=True,
|
|
error_message=None,
|
|
executed_at=datetime.now(timezone.utc)
|
|
)
|
|
]
|
|
|
|
rollback_log = RollbackLog(
|
|
log_id="test-log-id",
|
|
execution_plan_id="test-plan-id",
|
|
executed_at=datetime.now(timezone.utc),
|
|
operations=operations
|
|
)
|
|
|
|
# Save rollback log to file
|
|
log_path = tmp_path / "rollback_test.json"
|
|
log_data = {
|
|
"log_id": rollback_log.log_id,
|
|
"execution_plan_id": rollback_log.execution_plan_id,
|
|
"executed_at": rollback_log.executed_at.isoformat(),
|
|
"operations": [
|
|
{
|
|
"operation_type": op.operation.operation_type,
|
|
"source_path": str(op.operation.source_path),
|
|
"destination_path": str(op.operation.destination_path),
|
|
"reason": op.operation.reason,
|
|
"success": op.success,
|
|
"error_message": op.error_message,
|
|
"executed_at": op.executed_at.isoformat()
|
|
}
|
|
for op in rollback_log.operations
|
|
]
|
|
}
|
|
|
|
with open(log_path, 'w', encoding='utf-8') as f:
|
|
json.dump(log_data, f, indent=2)
|
|
|
|
return {
|
|
"log_path": log_path,
|
|
"source1": source1,
|
|
"source2": source2,
|
|
"dest1": dest1,
|
|
"dest2": dest2
|
|
}
|
|
|
|
|
|
class TestRollbackCommand:
|
|
"""Tests for the rollback CLI command."""
|
|
|
|
def test_rollback_help(self, cli_runner):
|
|
"""Test that rollback command shows help text."""
|
|
result = cli_runner.invoke(main, ['rollback', '--help'])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Rollback previous execution" in result.output
|
|
assert "--log" in result.output
|
|
assert "best-effort" in result.output
|
|
|
|
def test_rollback_with_log_file(self, cli_runner, sample_rollback_log):
|
|
"""Test rollback command with explicit log file."""
|
|
log_path = sample_rollback_log["log_path"]
|
|
dest1 = sample_rollback_log["dest1"]
|
|
dest2 = sample_rollback_log["dest2"]
|
|
source1 = sample_rollback_log["source1"]
|
|
source2 = sample_rollback_log["source2"]
|
|
|
|
# Verify files are at destination before rollback
|
|
assert dest1.exists()
|
|
assert dest2.exists()
|
|
assert not source1.exists()
|
|
assert not source2.exists()
|
|
|
|
# Run rollback command with auto-confirmation
|
|
result = cli_runner.invoke(
|
|
main,
|
|
['rollback', '--log', str(log_path)],
|
|
input='y\n' # Confirm rollback
|
|
)
|
|
|
|
# Check command succeeded
|
|
assert result.exit_code == 0
|
|
assert "Rollback log loaded" in result.output
|
|
assert "Rolling back" in result.output
|
|
assert "Rollback Summary" in result.output
|
|
|
|
# Verify files were moved back to source
|
|
assert source1.exists()
|
|
assert source2.exists()
|
|
assert not dest1.exists()
|
|
assert not dest2.exists()
|
|
|
|
def test_rollback_cancel_confirmation(self, cli_runner, sample_rollback_log):
|
|
"""Test that rollback can be cancelled at confirmation prompt."""
|
|
log_path = sample_rollback_log["log_path"]
|
|
dest1 = sample_rollback_log["dest1"]
|
|
dest2 = sample_rollback_log["dest2"]
|
|
|
|
# Run rollback command and cancel
|
|
result = cli_runner.invoke(
|
|
main,
|
|
['rollback', '--log', str(log_path)],
|
|
input='n\n' # Cancel rollback
|
|
)
|
|
|
|
# Check command was cancelled
|
|
assert result.exit_code == 0
|
|
assert "Rollback cancelled" in result.output
|
|
|
|
# Verify files were NOT moved (still at destination)
|
|
assert dest1.exists()
|
|
assert dest2.exists()
|
|
|
|
def test_rollback_missing_log_file(self, cli_runner, tmp_path):
|
|
"""Test rollback command with missing log file."""
|
|
missing_log = tmp_path / "nonexistent.json"
|
|
|
|
result = cli_runner.invoke(
|
|
main,
|
|
['rollback', '--log', str(missing_log)]
|
|
)
|
|
|
|
# Check command failed with appropriate error
|
|
# Click returns exit code 2 for file validation errors
|
|
assert result.exit_code == 2
|
|
assert "Error" in result.output or "does not exist" in result.output
|
|
|
|
def test_rollback_no_log_specified_no_logs_exist(self, cli_runner, tmp_path, monkeypatch):
|
|
"""Test rollback command without log file when no logs exist."""
|
|
# Mock home directory to use tmp_path
|
|
fake_home = tmp_path / "fake_home"
|
|
fake_home.mkdir()
|
|
monkeypatch.setattr(Path, 'home', lambda: fake_home)
|
|
|
|
result = cli_runner.invoke(main, ['rollback'])
|
|
|
|
# Check command failed with appropriate error
|
|
assert result.exit_code == 1
|
|
assert "No rollback logs found" in result.output
|