Implements optional git-based version control for information spaces: - HistoryConfig model for configuring history tracking - Commit, Branch, HistoryEntry, DiffResult models - IHistoryBackend and IHistoryQuery interfaces - GitHistoryBackend using git CLI for version control - GitHistoryEventHandler for event-driven auto-commits - HistoryEventCoordinator for managing space history - HistoryQueryService for high-level history queries - Automatic commits on DOCUMENT_ADDED/REMOVED/CONTENT_CHANGED events - Support for: * Commit log with pagination and filtering * Diff between versions * File content at specific versions * Branch creation and switching * Version restoration * Uncommitted changes detection - 43 comprehensive unit tests with git availability checks Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
839 lines
27 KiB
Python
839 lines
27 KiB
Python
"""
|
|
Tests for Phase 8: History Tracking.
|
|
|
|
Tests git-based version control, event-driven commits, and history queries.
|
|
"""
|
|
|
|
import pytest
|
|
import tempfile
|
|
import subprocess
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
|
|
from markitect.spaces.history import (
|
|
# Models
|
|
Commit,
|
|
Branch,
|
|
HistoryEntry,
|
|
DiffResult,
|
|
DiffLine,
|
|
DiffType,
|
|
HistoryConfig,
|
|
# Backend
|
|
GitHistoryBackend,
|
|
GitError,
|
|
# Events
|
|
GitHistoryEventHandler,
|
|
HistoryEventCoordinator,
|
|
# Queries
|
|
HistoryQueryService,
|
|
)
|
|
from markitect.spaces import (
|
|
SpaceEvent,
|
|
SpaceEventType,
|
|
EventBus,
|
|
)
|
|
|
|
|
|
# ===========================================================================
|
|
# Fixtures
|
|
# ===========================================================================
|
|
|
|
|
|
def check_git_available() -> bool:
|
|
"""Check if git is available."""
|
|
try:
|
|
result = subprocess.run(
|
|
["git", "--version"],
|
|
capture_output=True,
|
|
timeout=5,
|
|
)
|
|
return result.returncode == 0
|
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
return False
|
|
|
|
|
|
# Skip all tests if git is not available
|
|
pytestmark = pytest.mark.skipif(
|
|
not check_git_available(),
|
|
reason="git not available"
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_dir():
|
|
"""Create a temporary directory."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
yield Path(tmpdir)
|
|
|
|
|
|
@pytest.fixture
|
|
def git_backend():
|
|
"""Create a git backend instance."""
|
|
return GitHistoryBackend()
|
|
|
|
|
|
@pytest.fixture
|
|
def history_config():
|
|
"""Create a history configuration."""
|
|
return HistoryConfig(
|
|
enabled=True,
|
|
backend="git",
|
|
auto_commit=True,
|
|
author_name="Test User",
|
|
author_email="test@example.com",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def initialized_repo(temp_dir, git_backend, history_config):
|
|
"""Create an initialized git repository."""
|
|
git_backend.initialize(temp_dir, history_config)
|
|
return temp_dir
|
|
|
|
|
|
@pytest.fixture
|
|
def event_bus():
|
|
"""Create an event bus."""
|
|
return EventBus()
|
|
|
|
|
|
# ===========================================================================
|
|
# HistoryConfig Tests
|
|
# ===========================================================================
|
|
|
|
|
|
class TestHistoryConfig:
|
|
"""Tests for HistoryConfig model."""
|
|
|
|
def test_default_config(self):
|
|
"""Test default configuration."""
|
|
config = HistoryConfig()
|
|
assert config.enabled is False
|
|
assert config.backend == "git"
|
|
assert config.auto_commit is True
|
|
assert config.author_name == "markitect"
|
|
|
|
def test_config_with_custom_values(self):
|
|
"""Test configuration with custom values."""
|
|
config = HistoryConfig(
|
|
enabled=True,
|
|
backend="git",
|
|
author_name="John Doe",
|
|
author_email="john@example.com",
|
|
directory="/path/to/space",
|
|
)
|
|
assert config.enabled is True
|
|
assert config.author_name == "John Doe"
|
|
assert config.directory == "/path/to/space"
|
|
|
|
def test_config_serialization(self):
|
|
"""Test config to/from dict."""
|
|
config = HistoryConfig(
|
|
enabled=True,
|
|
author_name="Test",
|
|
commit_on_events=["DOCUMENT_ADDED"],
|
|
)
|
|
data = config.to_dict()
|
|
restored = HistoryConfig.from_dict(data)
|
|
assert restored.enabled == config.enabled
|
|
assert restored.author_name == config.author_name
|
|
assert restored.commit_on_events == config.commit_on_events
|
|
|
|
|
|
# ===========================================================================
|
|
# Commit Model Tests
|
|
# ===========================================================================
|
|
|
|
|
|
class TestCommitModel:
|
|
"""Tests for Commit model."""
|
|
|
|
def test_create_commit(self):
|
|
"""Test creating a commit."""
|
|
commit = Commit(
|
|
id="abc123",
|
|
message="Initial commit",
|
|
author="Test User <test@example.com>",
|
|
)
|
|
assert commit.id == "abc123"
|
|
assert commit.message == "Initial commit"
|
|
assert commit.short_id == "abc123"
|
|
|
|
def test_commit_serialization(self):
|
|
"""Test commit to/from dict."""
|
|
commit = Commit(
|
|
id="abc123def456",
|
|
message="Test commit",
|
|
author="Test <test@example.com>",
|
|
files_changed=["file1.md", "file2.md"],
|
|
metadata={"key": "value"},
|
|
)
|
|
data = commit.to_dict()
|
|
restored = Commit.from_dict(data)
|
|
assert restored.id == commit.id
|
|
assert restored.message == commit.message
|
|
assert restored.files_changed == commit.files_changed
|
|
assert restored.metadata == commit.metadata
|
|
|
|
def test_short_id(self):
|
|
"""Test short commit ID."""
|
|
commit = Commit(id="abcdef1234567890", message="Test")
|
|
assert commit.short_id == "abcdef1"
|
|
|
|
# Short ID for short hash
|
|
short_commit = Commit(id="abc", message="Test")
|
|
assert short_commit.short_id == "abc"
|
|
|
|
|
|
# ===========================================================================
|
|
# DiffResult Tests
|
|
# ===========================================================================
|
|
|
|
|
|
class TestDiffResult:
|
|
"""Tests for DiffResult model."""
|
|
|
|
def test_create_diff(self):
|
|
"""Test creating a diff result."""
|
|
diff = DiffResult(
|
|
path="test.md",
|
|
old_version="v1",
|
|
new_version="v2",
|
|
)
|
|
assert diff.path == "test.md"
|
|
assert diff.additions == 0
|
|
assert diff.deletions == 0
|
|
|
|
def test_unified_diff_format(self):
|
|
"""Test unified diff generation."""
|
|
diff = DiffResult(path="test.md")
|
|
diff.lines = [
|
|
DiffLine(DiffType.CONTEXT, "unchanged line", 1, 1),
|
|
DiffLine(DiffType.ADDITION, "added line", None, 2),
|
|
DiffLine(DiffType.DELETION, "removed line", 2, None),
|
|
]
|
|
diff.additions = 1
|
|
diff.deletions = 1
|
|
|
|
unified = diff.to_unified_diff()
|
|
assert "--- a/test.md" in unified
|
|
assert "+++ b/test.md" in unified
|
|
assert "+added line" in unified
|
|
assert "-removed line" in unified
|
|
assert " unchanged line" in unified
|
|
|
|
|
|
# ===========================================================================
|
|
# GitHistoryBackend Tests
|
|
# ===========================================================================
|
|
|
|
|
|
class TestGitHistoryBackend:
|
|
"""Tests for GitHistoryBackend."""
|
|
|
|
def test_initialize_creates_repo(self, temp_dir, git_backend, history_config):
|
|
"""Test initializing a git repository."""
|
|
git_backend.initialize(temp_dir, history_config)
|
|
assert (temp_dir / ".git").exists()
|
|
assert git_backend.is_initialized(temp_dir)
|
|
|
|
def test_initialize_idempotent(self, temp_dir, git_backend, history_config):
|
|
"""Test that initialize can be called multiple times."""
|
|
git_backend.initialize(temp_dir, history_config)
|
|
git_backend.initialize(temp_dir, history_config)
|
|
assert git_backend.is_initialized(temp_dir)
|
|
|
|
def test_is_initialized_false_for_non_repo(self, temp_dir, git_backend):
|
|
"""Test is_initialized returns False for non-repo."""
|
|
assert not git_backend.is_initialized(temp_dir)
|
|
|
|
def test_commit_creates_commit(self, initialized_repo, git_backend):
|
|
"""Test creating a commit."""
|
|
# Create a file
|
|
test_file = initialized_repo / "test.md"
|
|
test_file.write_text("# Test")
|
|
|
|
# Commit
|
|
commit = git_backend.commit(
|
|
initialized_repo,
|
|
"Add test file",
|
|
author="Test User <test@example.com>",
|
|
)
|
|
assert commit.message == "Add test file"
|
|
assert commit.id is not None
|
|
assert "test.md" in commit.files_changed
|
|
|
|
def test_commit_with_metadata(self, initialized_repo, git_backend):
|
|
"""Test commit with metadata trailers."""
|
|
test_file = initialized_repo / "doc.md"
|
|
test_file.write_text("Content")
|
|
|
|
commit = git_backend.commit(
|
|
initialized_repo,
|
|
"Add doc",
|
|
metadata={"space_id": "space-1", "event_type": "test"},
|
|
)
|
|
# Git trailers are stored as "key: value" in commit body
|
|
# They should be parseable
|
|
assert commit.message == "Add doc"
|
|
assert commit.id is not None
|
|
# Metadata parsing from git trailers may vary by git version
|
|
# Just verify commit succeeds
|
|
|
|
def test_commit_specific_files(self, initialized_repo, git_backend):
|
|
"""Test committing specific files."""
|
|
file1 = initialized_repo / "file1.md"
|
|
file2 = initialized_repo / "file2.md"
|
|
file1.write_text("File 1")
|
|
file2.write_text("File 2")
|
|
|
|
# Commit only file1
|
|
commit = git_backend.commit(
|
|
initialized_repo,
|
|
"Add file1",
|
|
files=["file1.md"],
|
|
)
|
|
assert "file1.md" in commit.files_changed
|
|
|
|
# file2 should still be uncommitted
|
|
assert git_backend.has_changes(initialized_repo)
|
|
|
|
def test_commit_no_changes_raises_error(self, initialized_repo, git_backend):
|
|
"""Test that committing with no changes raises error."""
|
|
with pytest.raises(GitError, match="No changes"):
|
|
git_backend.commit(initialized_repo, "Empty commit")
|
|
|
|
def test_get_commit(self, initialized_repo, git_backend):
|
|
"""Test getting a commit by ID."""
|
|
test_file = initialized_repo / "test.md"
|
|
test_file.write_text("Content")
|
|
|
|
commit = git_backend.commit(initialized_repo, "Test commit")
|
|
|
|
retrieved = git_backend.get_commit(initialized_repo, commit.id)
|
|
assert retrieved is not None
|
|
assert retrieved.id == commit.id
|
|
assert retrieved.message == "Test commit"
|
|
|
|
def test_get_commit_not_found(self, initialized_repo, git_backend):
|
|
"""Test getting non-existent commit returns None."""
|
|
result = git_backend.get_commit(initialized_repo, "nonexistent")
|
|
assert result is None
|
|
|
|
def test_get_log(self, initialized_repo, git_backend):
|
|
"""Test getting commit history."""
|
|
# Create multiple commits
|
|
for i in range(3):
|
|
file = initialized_repo / f"file{i}.md"
|
|
file.write_text(f"File {i}")
|
|
git_backend.commit(initialized_repo, f"Commit {i}")
|
|
|
|
# Get log
|
|
commits = git_backend.get_log(initialized_repo, limit=10)
|
|
# Should have initial commit + 3 new commits
|
|
assert len(commits) >= 3
|
|
assert commits[0].message == "Commit 2" # Most recent first
|
|
assert commits[1].message == "Commit 1"
|
|
|
|
def test_get_log_with_limit(self, initialized_repo, git_backend):
|
|
"""Test log with limit."""
|
|
for i in range(5):
|
|
file = initialized_repo / f"file{i}.md"
|
|
file.write_text(f"File {i}")
|
|
git_backend.commit(initialized_repo, f"Commit {i}")
|
|
|
|
commits = git_backend.get_log(initialized_repo, limit=2)
|
|
assert len(commits) == 2
|
|
|
|
def test_get_log_with_path_filter(self, initialized_repo, git_backend):
|
|
"""Test log filtered by path."""
|
|
file1 = initialized_repo / "file1.md"
|
|
file2 = initialized_repo / "file2.md"
|
|
|
|
file1.write_text("File 1")
|
|
git_backend.commit(initialized_repo, "Add file1")
|
|
|
|
file2.write_text("File 2")
|
|
git_backend.commit(initialized_repo, "Add file2")
|
|
|
|
file1.write_text("File 1 updated")
|
|
git_backend.commit(initialized_repo, "Update file1")
|
|
|
|
# Get log for file1 only
|
|
commits = git_backend.get_log(initialized_repo, path="file1.md")
|
|
# Should have 2 commits for file1 (add and update)
|
|
assert len(commits) >= 2
|
|
assert all("file1" in c.message for c in commits[:2])
|
|
|
|
def test_get_diff(self, initialized_repo, git_backend):
|
|
"""Test getting diff between commits."""
|
|
file = initialized_repo / "test.md"
|
|
file.write_text("Line 1\nLine 2\n")
|
|
commit1 = git_backend.commit(initialized_repo, "Initial")
|
|
|
|
file.write_text("Line 1\nLine 2 modified\nLine 3\n")
|
|
commit2 = git_backend.commit(initialized_repo, "Update")
|
|
|
|
diffs = git_backend.get_diff(initialized_repo, commit1.id, commit2.id)
|
|
assert len(diffs) > 0
|
|
diff = diffs[0]
|
|
assert diff.path == "test.md"
|
|
assert diff.additions > 0 or diff.deletions > 0
|
|
|
|
def test_checkout_version(self, initialized_repo, git_backend):
|
|
"""Test checking out a specific version."""
|
|
file = initialized_repo / "test.md"
|
|
file.write_text("Version 1")
|
|
commit1 = git_backend.commit(initialized_repo, "V1")
|
|
|
|
file.write_text("Version 2")
|
|
git_backend.commit(initialized_repo, "V2")
|
|
|
|
# Checkout file at commit1
|
|
success = git_backend.checkout(initialized_repo, commit1.id, "test.md")
|
|
assert success
|
|
assert file.read_text() == "Version 1"
|
|
|
|
def test_get_file_at_version(self, initialized_repo, git_backend):
|
|
"""Test getting file content at specific version."""
|
|
file = initialized_repo / "test.md"
|
|
file.write_text("Version 1")
|
|
commit1 = git_backend.commit(initialized_repo, "V1")
|
|
|
|
file.write_text("Version 2")
|
|
git_backend.commit(initialized_repo, "V2")
|
|
|
|
# Get content at commit1
|
|
content = git_backend.get_file_at_version(
|
|
initialized_repo, "test.md", commit1.id
|
|
)
|
|
assert content == "Version 1"
|
|
|
|
def test_get_file_at_version_not_found(self, initialized_repo, git_backend):
|
|
"""Test getting non-existent file returns None."""
|
|
content = git_backend.get_file_at_version(
|
|
initialized_repo, "nonexistent.md", "HEAD"
|
|
)
|
|
assert content is None
|
|
|
|
def test_list_branches(self, initialized_repo, git_backend):
|
|
"""Test listing branches."""
|
|
branches = git_backend.list_branches(initialized_repo)
|
|
assert len(branches) >= 1
|
|
# Should have at least main/master branch
|
|
assert any(b.name in ["main", "master"] for b in branches)
|
|
|
|
def test_create_branch(self, initialized_repo, git_backend):
|
|
"""Test creating a branch."""
|
|
# Create a commit first
|
|
file = initialized_repo / "test.md"
|
|
file.write_text("Content")
|
|
git_backend.commit(initialized_repo, "Add file")
|
|
|
|
branch = git_backend.create_branch(initialized_repo, "feature")
|
|
assert branch.name == "feature"
|
|
assert branch.head_commit_id is not None
|
|
|
|
branches = git_backend.list_branches(initialized_repo)
|
|
assert any(b.name == "feature" for b in branches)
|
|
|
|
def test_switch_branch(self, initialized_repo, git_backend):
|
|
"""Test switching branches."""
|
|
# Create commit and branch
|
|
file = initialized_repo / "test.md"
|
|
file.write_text("Content")
|
|
git_backend.commit(initialized_repo, "Add file")
|
|
git_backend.create_branch(initialized_repo, "feature")
|
|
|
|
# Switch to feature branch
|
|
success = git_backend.switch_branch(initialized_repo, "feature")
|
|
assert success
|
|
|
|
current = git_backend.get_current_branch(initialized_repo)
|
|
assert current == "feature"
|
|
|
|
def test_get_current_branch(self, initialized_repo, git_backend):
|
|
"""Test getting current branch."""
|
|
current = git_backend.get_current_branch(initialized_repo)
|
|
assert current in ["main", "master"] # Depends on git version
|
|
|
|
def test_has_changes(self, initialized_repo, git_backend):
|
|
"""Test checking for uncommitted changes."""
|
|
# No changes initially
|
|
assert not git_backend.has_changes(initialized_repo)
|
|
|
|
# Add a file
|
|
file = initialized_repo / "test.md"
|
|
file.write_text("Content")
|
|
|
|
# Should have changes now
|
|
assert git_backend.has_changes(initialized_repo)
|
|
|
|
# Commit
|
|
git_backend.commit(initialized_repo, "Add file")
|
|
|
|
# No changes after commit
|
|
assert not git_backend.has_changes(initialized_repo)
|
|
|
|
|
|
# ===========================================================================
|
|
# GitHistoryEventHandler Tests
|
|
# ===========================================================================
|
|
|
|
|
|
class TestGitHistoryEventHandler:
|
|
"""Tests for event-driven commits."""
|
|
|
|
def test_create_handler(self, git_backend, history_config):
|
|
"""Test creating an event handler."""
|
|
def resolver(space_id):
|
|
return Path(f"/tmp/{space_id}")
|
|
|
|
handler = GitHistoryEventHandler(git_backend, history_config, resolver)
|
|
assert handler is not None
|
|
|
|
def test_handler_registers_with_bus(
|
|
self, git_backend, history_config, event_bus
|
|
):
|
|
"""Test handler registration."""
|
|
def resolver(space_id):
|
|
return Path(f"/tmp/{space_id}")
|
|
|
|
handler = GitHistoryEventHandler(git_backend, history_config, resolver)
|
|
handler.register(event_bus)
|
|
|
|
# Should have registered handlers
|
|
assert len(event_bus._handlers) > 0
|
|
|
|
handler.unregister()
|
|
|
|
def test_handler_creates_commit_on_event(
|
|
self, temp_dir, git_backend, history_config, event_bus
|
|
):
|
|
"""Test that handler commits on document events."""
|
|
space_id = "test-space"
|
|
|
|
def resolver(sid):
|
|
return temp_dir if sid == space_id else None
|
|
|
|
# Initialize repo
|
|
git_backend.initialize(temp_dir, history_config)
|
|
|
|
# Create initial file
|
|
file = temp_dir / "doc.md"
|
|
file.write_text("Initial content")
|
|
git_backend.commit(temp_dir, "Initial commit")
|
|
|
|
# Create handler
|
|
handler = GitHistoryEventHandler(git_backend, history_config, resolver)
|
|
handler.register(event_bus)
|
|
|
|
# Modify file
|
|
file.write_text("Updated content")
|
|
|
|
# Emit event
|
|
event = SpaceEvent(
|
|
event_type=SpaceEventType.DOCUMENT_CONTENT_CHANGED,
|
|
space_id=space_id,
|
|
payload={"document_id": "doc-1", "space_path": "/doc.md"},
|
|
)
|
|
event_bus.emit(event)
|
|
|
|
# Should have created a new commit
|
|
commits = git_backend.get_log(temp_dir, limit=5)
|
|
assert len(commits) >= 2
|
|
assert "Update document" in commits[0].message
|
|
|
|
handler.unregister()
|
|
|
|
def test_handler_disabled_when_not_enabled(
|
|
self, temp_dir, git_backend, event_bus
|
|
):
|
|
"""Test handler does nothing when disabled."""
|
|
config = HistoryConfig(enabled=False)
|
|
space_id = "test-space"
|
|
|
|
def resolver(sid):
|
|
return temp_dir if sid == space_id else None
|
|
|
|
handler = GitHistoryEventHandler(git_backend, config, resolver)
|
|
handler.register(event_bus)
|
|
|
|
# Initialize repo manually
|
|
git_backend.initialize(temp_dir, config)
|
|
|
|
file = temp_dir / "doc.md"
|
|
file.write_text("Content")
|
|
|
|
# Emit event - should not commit since disabled
|
|
event = SpaceEvent(
|
|
event_type=SpaceEventType.DOCUMENT_ADDED,
|
|
space_id=space_id,
|
|
payload={"space_path": "/doc.md"},
|
|
)
|
|
event_bus.emit(event)
|
|
|
|
# Should still have uncommitted changes
|
|
assert git_backend.has_changes(temp_dir)
|
|
|
|
handler.unregister()
|
|
|
|
|
|
# ===========================================================================
|
|
# HistoryQueryService Tests
|
|
# ===========================================================================
|
|
|
|
|
|
class TestHistoryQueryService:
|
|
"""Tests for HistoryQueryService."""
|
|
|
|
def test_create_service(self, git_backend):
|
|
"""Test creating a query service."""
|
|
def resolver(space_id):
|
|
return Path(f"/tmp/{space_id}")
|
|
|
|
service = HistoryQueryService(git_backend, resolver)
|
|
assert service is not None
|
|
|
|
def test_get_history(self, initialized_repo, git_backend):
|
|
"""Test getting history entries."""
|
|
space_id = "test-space"
|
|
|
|
def resolver(sid):
|
|
return initialized_repo if sid == space_id else None
|
|
|
|
service = HistoryQueryService(git_backend, resolver)
|
|
|
|
# Create commits
|
|
for i in range(3):
|
|
file = initialized_repo / f"file{i}.md"
|
|
file.write_text(f"Content {i}")
|
|
git_backend.commit(initialized_repo, f"Add file {i}")
|
|
|
|
# Get history
|
|
history = service.get_history(space_id, limit=10)
|
|
assert len(history) >= 3
|
|
assert isinstance(history[0], HistoryEntry)
|
|
assert history[0].commit.message.startswith("Add file")
|
|
|
|
def test_get_version_content(self, initialized_repo, git_backend):
|
|
"""Test getting content at specific version."""
|
|
space_id = "test-space"
|
|
|
|
def resolver(sid):
|
|
return initialized_repo if sid == space_id else None
|
|
|
|
service = HistoryQueryService(git_backend, resolver)
|
|
|
|
# Create versions
|
|
file = initialized_repo / "doc.md"
|
|
file.write_text("Version 1")
|
|
commit1 = git_backend.commit(initialized_repo, "V1")
|
|
|
|
file.write_text("Version 2")
|
|
git_backend.commit(initialized_repo, "V2")
|
|
|
|
# Get content at V1
|
|
content = service.get_version_content(space_id, "/doc.md", commit1.id)
|
|
assert content == "Version 1"
|
|
|
|
def test_compare_versions(self, initialized_repo, git_backend):
|
|
"""Test comparing two versions."""
|
|
space_id = "test-space"
|
|
|
|
def resolver(sid):
|
|
return initialized_repo if sid == space_id else None
|
|
|
|
service = HistoryQueryService(git_backend, resolver)
|
|
|
|
file = initialized_repo / "doc.md"
|
|
file.write_text("Line 1\n")
|
|
commit1 = git_backend.commit(initialized_repo, "V1")
|
|
|
|
file.write_text("Line 1\nLine 2\n")
|
|
commit2 = git_backend.commit(initialized_repo, "V2")
|
|
|
|
diff = service.compare_versions(space_id, "/doc.md", commit1.id, commit2.id)
|
|
assert diff is not None
|
|
assert diff.additions > 0
|
|
|
|
def test_search_history(self, initialized_repo, git_backend):
|
|
"""Test searching commit messages."""
|
|
space_id = "test-space"
|
|
|
|
def resolver(sid):
|
|
return initialized_repo if sid == space_id else None
|
|
|
|
service = HistoryQueryService(git_backend, resolver)
|
|
|
|
# Create commits with different messages
|
|
file = initialized_repo / "doc.md"
|
|
file.write_text("V1")
|
|
git_backend.commit(initialized_repo, "Add feature X")
|
|
|
|
file.write_text("V2")
|
|
git_backend.commit(initialized_repo, "Fix bug Y")
|
|
|
|
file.write_text("V3")
|
|
git_backend.commit(initialized_repo, "Update feature X")
|
|
|
|
# Search for "feature"
|
|
results = service.search_history(space_id, "feature")
|
|
assert len(results) >= 2
|
|
assert all("feature" in r.commit.message.lower() for r in results)
|
|
|
|
def test_restore_version(self, initialized_repo, git_backend):
|
|
"""Test restoring a document to previous version."""
|
|
space_id = "test-space"
|
|
|
|
def resolver(sid):
|
|
return initialized_repo if sid == space_id else None
|
|
|
|
service = HistoryQueryService(git_backend, resolver)
|
|
|
|
file = initialized_repo / "doc.md"
|
|
file.write_text("Version 1")
|
|
commit1 = git_backend.commit(initialized_repo, "V1")
|
|
|
|
file.write_text("Version 2")
|
|
git_backend.commit(initialized_repo, "V2")
|
|
|
|
# Restore to V1
|
|
success = service.restore_version(space_id, "/doc.md", commit1.id)
|
|
assert success
|
|
assert file.read_text() == "Version 1"
|
|
|
|
def test_has_history(self, initialized_repo, git_backend):
|
|
"""Test checking if space has history."""
|
|
space1 = "space-with-history"
|
|
space2 = "space-without-history"
|
|
|
|
# Create fresh directory for space2
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
no_history_dir = Path(tmpdir)
|
|
|
|
def resolver(sid):
|
|
if sid == space1:
|
|
return initialized_repo
|
|
elif sid == space2:
|
|
return no_history_dir
|
|
return None
|
|
|
|
service = HistoryQueryService(git_backend, resolver)
|
|
|
|
assert service.has_history(space1)
|
|
assert not service.has_history(space2)
|
|
|
|
def test_get_branches(self, initialized_repo, git_backend):
|
|
"""Test getting branch names."""
|
|
space_id = "test-space"
|
|
|
|
def resolver(sid):
|
|
return initialized_repo if sid == space_id else None
|
|
|
|
service = HistoryQueryService(git_backend, resolver)
|
|
|
|
# Create a commit first
|
|
file = initialized_repo / "doc.md"
|
|
file.write_text("Content")
|
|
git_backend.commit(initialized_repo, "Add file")
|
|
|
|
# Create branch
|
|
git_backend.create_branch(initialized_repo, "feature")
|
|
|
|
branches = service.get_branches(space_id)
|
|
assert "feature" in branches
|
|
|
|
def test_has_uncommitted_changes(self, initialized_repo, git_backend):
|
|
"""Test checking for uncommitted changes."""
|
|
space_id = "test-space"
|
|
|
|
def resolver(sid):
|
|
return initialized_repo if sid == space_id else None
|
|
|
|
service = HistoryQueryService(git_backend, resolver)
|
|
|
|
# No changes initially
|
|
assert not service.has_uncommitted_changes(space_id)
|
|
|
|
# Add file
|
|
file = initialized_repo / "doc.md"
|
|
file.write_text("Content")
|
|
|
|
# Should have changes
|
|
assert service.has_uncommitted_changes(space_id)
|
|
|
|
|
|
# ===========================================================================
|
|
# Integration Tests
|
|
# ===========================================================================
|
|
|
|
|
|
class TestHistoryIntegration:
|
|
"""Integration tests for history tracking."""
|
|
|
|
def test_full_workflow(self, temp_dir, git_backend, history_config, event_bus):
|
|
"""Test complete history workflow."""
|
|
space_id = "test-space"
|
|
|
|
def resolver(sid):
|
|
return temp_dir if sid == space_id else None
|
|
|
|
# Initialize
|
|
git_backend.initialize(temp_dir, history_config)
|
|
|
|
# Create query service
|
|
query_service = HistoryQueryService(git_backend, resolver)
|
|
|
|
# Create event handler
|
|
event_handler = GitHistoryEventHandler(
|
|
git_backend, history_config, resolver
|
|
)
|
|
event_handler.register(event_bus)
|
|
|
|
# Add initial file
|
|
file = temp_dir / "intro.md"
|
|
file.write_text("# Introduction")
|
|
git_backend.commit(temp_dir, "Initial commit")
|
|
|
|
# Emit document added event
|
|
file2 = temp_dir / "chapter1.md"
|
|
file2.write_text("# Chapter 1")
|
|
event = SpaceEvent(
|
|
event_type=SpaceEventType.DOCUMENT_ADDED,
|
|
space_id=space_id,
|
|
payload={"space_path": "/chapter1.md"},
|
|
)
|
|
event_bus.emit(event)
|
|
|
|
# Should have 2 commits
|
|
history = query_service.get_history(space_id)
|
|
assert len(history) >= 2
|
|
|
|
# Get specific version (find the commit where intro.md was added)
|
|
commits = git_backend.get_log(temp_dir)
|
|
intro_commit = None
|
|
for commit in reversed(commits):
|
|
if "intro.md" in commit.files_changed:
|
|
intro_commit = commit
|
|
break
|
|
|
|
if intro_commit:
|
|
old_content = query_service.get_version_content(
|
|
space_id, "/intro.md", intro_commit.id
|
|
)
|
|
assert old_content == "# Introduction"
|
|
|
|
# Compare versions
|
|
file.write_text("# Introduction\nUpdated")
|
|
git_backend.commit(temp_dir, "Update intro")
|
|
|
|
diff = query_service.compare_versions(
|
|
space_id, "/intro.md", commits[-1].id, "HEAD"
|
|
)
|
|
assert diff is not None
|
|
assert diff.additions > 0
|
|
|
|
event_handler.unregister()
|