""" 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")