Files
markitect-main/tests/integration/spaces/test_space_service_integration.py
tegwick 9b12875681 feat(spaces): implement Phase 0-1 of Information Space Service
Phase 0 - Project Organization:
- Create docs/PROJECT_STRUCTURE.md documenting codebase layout
- Create markitect/core/ with parser, serializer, document_manager, workspace
- Create markitect/schema/ consolidating 6 schema_*.py modules
- Create markitect/storage/ with database module
- Maintain backward compatibility via re-exports from original locations
- Add docs/roadmap/information-space-service/ with README and WORKPLAN

Phase 1 - Foundation (Weeks 1-3):
- Week 1: Core domain models (InformationSpace, SpaceDocument, SpaceConfig,
  SpaceMetadata, SpaceVariable, TransclusionReference, SpaceStatus)
- Week 2: Repository layer with interfaces (ISpaceRepository,
  IDocumentAssociationRepository, IVariableRepository, IReferenceRepository)
  and SQLite implementations with foreign key cascade deletes
- Week 3: SpaceService orchestration layer with full CRUD, document,
  variable, and reference tracking operations

Test coverage: 124 tests (25 model + 63 repository + 36 integration)

Capabilities delivered:
- CAP-001: InformationSpace entity with lifecycle management
- CAP-002: SpaceRepository CRUD with SQLite backing
- CAP-003: Document-Space associations with path-based organization
- CAP-004: Space metadata and configuration schemas
- CAP-005: Database schema with migrations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 02:02:46 +01:00

571 lines
21 KiB
Python

"""
Integration tests for SpaceService.
Tests the full workflow of space operations including:
- Space creation and lifecycle management
- Document operations within spaces
- Variable management
- Reference tracking for cache invalidation
"""
import pytest
import tempfile
import os
from markitect.spaces import (
SpaceService,
InformationSpace,
SpaceDocument,
SpaceConfig,
SpaceMetadata,
SpaceStatus,
SqliteSpaceRepository,
SqliteDocumentRepository,
SqliteVariableRepository,
SqliteReferenceRepository,
)
@pytest.fixture
def temp_db():
"""Create a temporary database file for testing."""
fd, path = tempfile.mkstemp(suffix=".db")
os.close(fd)
yield path
if os.path.exists(path):
os.unlink(path)
@pytest.fixture
def space_service(temp_db):
"""Create a fully wired SpaceService for testing."""
return SpaceService(
space_repo=SqliteSpaceRepository(temp_db),
document_repo=SqliteDocumentRepository(temp_db),
variable_repo=SqliteVariableRepository(temp_db),
reference_repo=SqliteReferenceRepository(temp_db),
)
class TestSpaceLifecycle:
"""Tests for space lifecycle operations."""
def test_create_and_retrieve_space(self, space_service):
"""Test creating and retrieving a space."""
space = space_service.create_space(
name="my-docs",
description="My documentation",
)
assert space.name == "my-docs"
assert space.description == "My documentation"
assert space.status == SpaceStatus.DRAFT
# Retrieve by ID
retrieved = space_service.get_space(space.id)
assert retrieved is not None
assert retrieved.name == "my-docs"
# Retrieve by name
by_name = space_service.get_space_by_name("my-docs")
assert by_name is not None
assert by_name.id == space.id
def test_create_space_with_config_and_metadata(self, space_service):
"""Test creating a space with custom config and metadata."""
config = SpaceConfig(
theme="dark",
history_enabled=True,
enable_caching=False,
)
metadata = SpaceMetadata(
tags=["api", "v1"],
author="tester",
custom={"version": "1.0"},
)
space = space_service.create_space(
name="configured-space",
config=config,
metadata=metadata,
)
assert space.config.theme == "dark"
assert space.config.history_enabled is True
assert space.metadata.tags == ["api", "v1"]
assert space.metadata.author == "tester"
def test_update_space(self, space_service):
"""Test updating a space."""
space = space_service.create_space(name="original")
updated = space_service.update_space(
space.id,
name="updated",
description="New description",
)
assert updated.name == "updated"
assert updated.description == "New description"
# Verify persisted
retrieved = space_service.get_space(space.id)
assert retrieved.name == "updated"
def test_space_lifecycle_transitions(self, space_service):
"""Test space status transitions."""
space = space_service.create_space(name="lifecycle-test")
assert space.status == SpaceStatus.DRAFT
# Activate
activated = space_service.activate_space(space.id)
assert activated.status == SpaceStatus.ACTIVE
# Archive
archived = space_service.archive_space(space.id)
assert archived.status == SpaceStatus.ARCHIVED
def test_delete_space(self, space_service):
"""Test deleting a space."""
space = space_service.create_space(name="to-delete")
result = space_service.delete_space(space.id)
assert result is True
# Verify deleted
retrieved = space_service.get_space(space.id)
assert retrieved is None
def test_list_spaces_excludes_archived(self, space_service):
"""Test that list_spaces excludes archived by default."""
space1 = space_service.create_space(name="active")
space2 = space_service.create_space(name="archived")
space_service.archive_space(space2.id)
spaces = space_service.list_spaces()
assert len(spaces) == 1
assert spaces[0].name == "active"
# Include archived
all_spaces = space_service.list_spaces(include_archived=True)
assert len(all_spaces) == 2
class TestSpaceHierarchy:
"""Tests for space hierarchy operations."""
def test_create_child_space(self, space_service):
"""Test creating a child space."""
parent = space_service.create_space(name="parent")
child = space_service.create_space(
name="child",
parent_space_id=parent.id,
)
assert child.parent_space_id == parent.id
children = space_service.get_child_spaces(parent.id)
assert len(children) == 1
assert children[0].id == child.id
def test_create_nested_hierarchy(self, space_service):
"""Test creating a nested space hierarchy."""
root = space_service.create_space(name="root")
level1 = space_service.create_space(name="level1", parent_space_id=root.id)
level2 = space_service.create_space(name="level2", parent_space_id=level1.id)
# Verify hierarchy
root_children = space_service.get_child_spaces(root.id)
assert len(root_children) == 1
assert root_children[0].id == level1.id
level1_children = space_service.get_child_spaces(level1.id)
assert len(level1_children) == 1
assert level1_children[0].id == level2.id
def test_delete_space_with_children_cascade(self, space_service):
"""Test deleting a space cascades to children."""
parent = space_service.create_space(name="parent")
child = space_service.create_space(name="child", parent_space_id=parent.id)
space_service.delete_space(parent.id, cascade=True)
assert space_service.get_space(parent.id) is None
assert space_service.get_space(child.id) is None
def test_delete_space_with_children_no_cascade_raises(self, space_service):
"""Test deleting a space with children raises if cascade=False."""
parent = space_service.create_space(name="parent")
space_service.create_space(name="child", parent_space_id=parent.id)
with pytest.raises(ValueError, match="has 1 child"):
space_service.delete_space(parent.id, cascade=False)
class TestDocumentOperations:
"""Tests for document operations within spaces."""
def test_add_and_list_documents(self, space_service):
"""Test adding and listing documents."""
space = space_service.create_space(name="doc-space")
doc1 = space_service.add_document(
space.id,
space_path="/intro.md",
document_id="doc-1",
)
doc2 = space_service.add_document(
space.id,
space_path="/api/endpoints.md",
document_id="doc-2",
)
docs = space_service.list_documents(space.id)
assert len(docs) == 2
def test_get_document_by_path(self, space_service):
"""Test getting a document by its path."""
space = space_service.create_space(name="doc-space")
space_service.add_document(space.id, "/intro.md", document_id="doc-1")
doc = space_service.get_document_by_path(space.id, "/intro.md")
assert doc is not None
assert doc.document_id == "doc-1"
# Also works without leading slash
doc2 = space_service.get_document_by_path(space.id, "intro.md")
assert doc2 is not None
def test_move_document(self, space_service):
"""Test moving a document to a new path."""
space = space_service.create_space(name="doc-space")
doc = space_service.add_document(space.id, "/old-path.md")
moved = space_service.move_document(doc.id, "/new-path.md")
assert moved.space_path == "/new-path.md"
# Old path should not exist
old_doc = space_service.get_document_by_path(space.id, "/old-path.md")
assert old_doc is None
# New path should work
new_doc = space_service.get_document_by_path(space.id, "/new-path.md")
assert new_doc is not None
def test_remove_document(self, space_service):
"""Test removing a document."""
space = space_service.create_space(name="doc-space")
doc = space_service.add_document(space.id, "/to-remove.md")
result = space_service.remove_document(doc.id)
assert result is True
# Verify removed
retrieved = space_service.get_document(doc.id)
assert retrieved is None
def test_reorder_documents(self, space_service):
"""Test reordering documents."""
space = space_service.create_space(name="doc-space")
doc1 = space_service.add_document(space.id, "/a.md", order_index=0)
doc2 = space_service.add_document(space.id, "/b.md", order_index=1)
doc3 = space_service.add_document(space.id, "/c.md", order_index=2)
# Reorder: c, a, b
space_service.reorder_documents(space.id, [doc3.id, doc1.id, doc2.id])
docs = space_service.list_documents(space.id)
assert docs[0].id == doc3.id
assert docs[1].id == doc1.id
assert docs[2].id == doc2.id
def test_document_with_metadata(self, space_service):
"""Test document with custom metadata."""
space = space_service.create_space(name="doc-space")
doc = space_service.add_document(
space.id,
"/api.md",
metadata={"title": "API Reference", "order": 5},
)
retrieved = space_service.get_document(doc.id)
assert retrieved.metadata["title"] == "API Reference"
assert retrieved.metadata["order"] == 5
def test_update_document_hash(self, space_service):
"""Test updating document content hash."""
space = space_service.create_space(name="doc-space")
doc = space_service.add_document(space.id, "/content.md")
space_service.update_document_hash(doc.id, "hash123abc")
retrieved = space_service.get_document(doc.id)
assert retrieved.content_hash == "hash123abc"
class TestVariableOperations:
"""Tests for variable operations within spaces."""
def test_set_and_get_variable(self, space_service):
"""Test setting and getting a variable."""
space = space_service.create_space(name="var-space")
var = space_service.set_variable(space.id, "version", "1.0.0")
assert var.value == "1.0.0"
retrieved = space_service.get_variable(space.id, "version")
assert retrieved is not None
assert retrieved.value == "1.0.0"
def test_list_variables(self, space_service):
"""Test listing variables."""
space = space_service.create_space(name="var-space")
space_service.set_variable(space.id, "var1", "value1")
space_service.set_variable(space.id, "var2", "value2")
variables = space_service.list_variables(space.id)
assert len(variables) == 2
def test_list_variables_by_scope(self, space_service):
"""Test listing variables filtered by scope."""
space = space_service.create_space(name="var-space")
space_service.set_variable(space.id, "global", "g", scope="space")
space_service.set_variable(space.id, "local", "l", scope="document")
space_vars = space_service.list_variables(space.id, scope="space")
assert len(space_vars) == 1
assert space_vars[0].name == "global"
def test_delete_variable(self, space_service):
"""Test deleting a variable."""
space = space_service.create_space(name="var-space")
space_service.set_variable(space.id, "temp", "value")
result = space_service.delete_variable(space.id, "temp")
assert result is True
retrieved = space_service.get_variable(space.id, "temp")
assert retrieved is None
def test_get_variables_dict(self, space_service):
"""Test getting variables as a dictionary."""
space = space_service.create_space(name="var-space")
space_service.set_variable(space.id, "api_url", "https://api.example.com")
space_service.set_variable(space.id, "version", "2.0")
variables_dict = space_service.get_variables_dict(space.id)
assert variables_dict == {
"api_url": "https://api.example.com",
"version": "2.0",
}
def test_variable_with_complex_value(self, space_service):
"""Test variable with complex JSON value."""
space = space_service.create_space(name="var-space")
complex_value = {
"endpoints": ["/api/v1", "/api/v2"],
"config": {"timeout": 30},
}
space_service.set_variable(space.id, "api_config", complex_value)
retrieved = space_service.get_variable(space.id, "api_config")
assert retrieved.value == complex_value
class TestReferenceTracking:
"""Tests for transclusion reference tracking."""
def test_add_and_get_references(self, space_service):
"""Test adding and getting references."""
space = space_service.create_space(name="ref-space")
space_service.add_reference("doc-1", "shared-component", space.id)
space_service.add_reference("doc-2", "shared-component", space.id)
refs = space_service.get_references_to("shared-component", space.id)
assert len(refs) == 2
def test_get_references_from(self, space_service):
"""Test getting references from a source document."""
space = space_service.create_space(name="ref-space")
space_service.add_reference("doc-1", "component-a", space.id)
space_service.add_reference("doc-1", "component-b", space.id)
refs = space_service.get_references_from("doc-1", space.id)
assert len(refs) == 2
targets = [r.target_doc_id for r in refs]
assert "component-a" in targets
assert "component-b" in targets
def test_get_dependents(self, space_service):
"""Test getting dependent documents."""
space = space_service.create_space(name="ref-space")
space_service.add_reference("doc-1", "shared", space.id)
space_service.add_reference("doc-2", "shared", space.id)
space_service.add_reference("doc-3", "shared", space.id)
dependents = space_service.get_dependents("shared", space.id)
assert len(dependents) == 3
assert set(dependents) == {"doc-1", "doc-2", "doc-3"}
def test_clear_references_from(self, space_service):
"""Test clearing references from a source document."""
space = space_service.create_space(name="ref-space")
space_service.add_reference("doc-1", "a", space.id)
space_service.add_reference("doc-1", "b", space.id)
space_service.add_reference("doc-2", "a", space.id)
count = space_service.clear_references_from("doc-1", space.id)
assert count == 2
# doc-1 refs should be gone
refs1 = space_service.get_references_from("doc-1", space.id)
assert len(refs1) == 0
# doc-2 refs should still exist
refs2 = space_service.get_references_from("doc-2", space.id)
assert len(refs2) == 1
def test_remove_document_clears_references(self, space_service):
"""Test that removing a document clears its references."""
space = space_service.create_space(name="ref-space")
doc = space_service.add_document(space.id, "/source.md")
# Add reference from this document
space_service.add_reference(doc.id, "target", space.id)
# Verify reference exists
refs = space_service.get_references_from(doc.id, space.id)
assert len(refs) == 1
# Remove document
space_service.remove_document(doc.id)
# References should be cleared
refs = space_service.get_references_from(doc.id, space.id)
assert len(refs) == 0
class TestFullWorkflow:
"""End-to-end workflow tests."""
def test_documentation_space_workflow(self, space_service):
"""Test a complete documentation space workflow."""
# Create a documentation space
space = space_service.create_space(
name="api-docs",
description="API Documentation",
config=SpaceConfig(theme="minimal"),
metadata=SpaceMetadata(tags=["api", "v2"]),
)
# Add documents
intro = space_service.add_document(
space.id,
"/intro.md",
order_index=0,
metadata={"title": "Introduction"},
)
endpoints = space_service.add_document(
space.id,
"/api/endpoints.md",
order_index=1,
metadata={"title": "API Endpoints"},
)
auth = space_service.add_document(
space.id,
"/api/auth.md",
order_index=2,
metadata={"title": "Authentication"},
)
# Add variables for transclusion
space_service.set_variable(space.id, "api_base_url", "https://api.example.com")
space_service.set_variable(space.id, "version", "2.0")
# Track references (e.g., endpoints includes auth)
space_service.add_reference(endpoints.id, auth.id, space.id)
# Activate the space
space_service.activate_space(space.id)
# Get stats
stats = space_service.get_space_stats(space.id)
assert stats["document_count"] == 3
assert stats["variable_count"] == 2
assert stats["status"] == "active"
# Verify the space
retrieved = space_service.get_space(space.id)
assert retrieved.status == SpaceStatus.ACTIVE
# List documents in order
docs = space_service.list_documents(space.id)
assert len(docs) == 3
assert docs[0].space_path == "/intro.md"
# Get transclusion context
context = space_service.get_variables_dict(space.id)
assert context["api_base_url"] == "https://api.example.com"
# Check dependencies for cache invalidation
dependents = space_service.get_dependents(auth.id, space.id)
assert endpoints.id in dependents
def test_space_stats(self, space_service):
"""Test getting space statistics."""
space = space_service.create_space(name="stats-test")
space_service.add_document(space.id, "/doc1.md")
space_service.add_document(space.id, "/doc2.md")
space_service.set_variable(space.id, "var1", "value1")
space_service.create_space(name="child", parent_space_id=space.id)
stats = space_service.get_space_stats(space.id)
assert stats["name"] == "stats-test"
assert stats["document_count"] == 2
assert stats["variable_count"] == 1
assert stats["child_space_count"] == 1
class TestErrorHandling:
"""Tests for error handling scenarios."""
def test_create_space_empty_name_raises(self, space_service):
"""Test that empty name raises ValueError."""
with pytest.raises(ValueError, match="cannot be empty"):
space_service.create_space(name="")
with pytest.raises(ValueError, match="cannot be empty"):
space_service.create_space(name=" ")
def test_create_space_duplicate_name_raises(self, space_service):
"""Test that duplicate name raises ValueError."""
space_service.create_space(name="taken")
with pytest.raises(ValueError, match="already exists"):
space_service.create_space(name="taken")
def test_update_nonexistent_space_raises(self, space_service):
"""Test that updating non-existent space raises ValueError."""
with pytest.raises(ValueError, match="not found"):
space_service.update_space("non-existent", name="new-name")
def test_add_document_to_nonexistent_space_raises(self, space_service):
"""Test that adding document to non-existent space raises."""
with pytest.raises(ValueError, match="not found"):
space_service.add_document("non-existent", "/doc.md")
def test_set_variable_in_nonexistent_space_raises(self, space_service):
"""Test that setting variable in non-existent space raises."""
with pytest.raises(ValueError, match="not found"):
space_service.set_variable("non-existent", "var", "value")
def test_create_child_with_nonexistent_parent_raises(self, space_service):
"""Test that creating child with non-existent parent raises."""
with pytest.raises(ValueError, match="Parent space.*not found"):
space_service.create_space(name="orphan", parent_space_id="non-existent")