""" Integration tests for event propagation in SpaceService. Tests that events are correctly emitted for all space operations. """ import pytest import tempfile import os from unittest.mock import Mock from markitect.spaces import ( SpaceService, SqliteSpaceRepository, SqliteDocumentRepository, SqliteVariableRepository, SqliteReferenceRepository, ) from markitect.spaces.events import ( EventBus, SpaceEvent, SpaceEventType, ) @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 event_bus(): """Create an EventBus for testing.""" return EventBus() @pytest.fixture def space_service(temp_db, event_bus): """Create a SpaceService with EventBus for testing.""" return SpaceService( space_repo=SqliteSpaceRepository(temp_db), document_repo=SqliteDocumentRepository(temp_db), variable_repo=SqliteVariableRepository(temp_db), reference_repo=SqliteReferenceRepository(temp_db), event_bus=event_bus, ) class TestSpaceEvents: """Tests for space lifecycle events.""" def test_space_created_event(self, space_service, event_bus): """Test that SPACE_CREATED event is emitted.""" received_events = [] event_bus.subscribe(SpaceEventType.SPACE_CREATED, received_events.append) space = space_service.create_space(name="test-space") assert len(received_events) == 1 event = received_events[0] assert event.event_type == SpaceEventType.SPACE_CREATED assert event.space_id == space.id assert event.payload["name"] == "test-space" def test_space_updated_event(self, space_service, event_bus): """Test that SPACE_UPDATED event is emitted.""" space = space_service.create_space(name="original") received_events = [] event_bus.subscribe(SpaceEventType.SPACE_UPDATED, received_events.append) space_service.update_space(space.id, name="updated", description="New desc") assert len(received_events) == 1 event = received_events[0] assert event.event_type == SpaceEventType.SPACE_UPDATED assert event.space_id == space.id assert event.payload["changes"]["name"] == "updated" assert event.payload["changes"]["description"] == "New desc" def test_space_deleted_event(self, space_service, event_bus): """Test that SPACE_DELETED event is emitted.""" space = space_service.create_space(name="to-delete") received_events = [] event_bus.subscribe(SpaceEventType.SPACE_DELETED, received_events.append) space_service.delete_space(space.id) assert len(received_events) == 1 event = received_events[0] assert event.event_type == SpaceEventType.SPACE_DELETED assert event.space_id == space.id assert event.payload["name"] == "to-delete" def test_space_activated_event(self, space_service, event_bus): """Test that SPACE_ACTIVATED event is emitted.""" space = space_service.create_space(name="to-activate") received_events = [] event_bus.subscribe(SpaceEventType.SPACE_ACTIVATED, received_events.append) space_service.activate_space(space.id) assert len(received_events) == 1 event = received_events[0] assert event.event_type == SpaceEventType.SPACE_ACTIVATED assert event.space_id == space.id def test_space_archived_event(self, space_service, event_bus): """Test that SPACE_ARCHIVED event is emitted.""" space = space_service.create_space(name="to-archive") received_events = [] event_bus.subscribe(SpaceEventType.SPACE_ARCHIVED, received_events.append) space_service.archive_space(space.id) assert len(received_events) == 1 event = received_events[0] assert event.event_type == SpaceEventType.SPACE_ARCHIVED assert event.space_id == space.id def test_cascade_delete_emits_multiple_events(self, space_service, event_bus): """Test that cascade delete emits events for all deleted spaces.""" parent = space_service.create_space(name="parent") child1 = space_service.create_space(name="child1", parent_space_id=parent.id) child2 = space_service.create_space(name="child2", parent_space_id=parent.id) received_events = [] event_bus.subscribe(SpaceEventType.SPACE_DELETED, received_events.append) space_service.delete_space(parent.id, cascade=True) # Should have events for parent and both children assert len(received_events) == 3 deleted_ids = {e.space_id for e in received_events} assert parent.id in deleted_ids assert child1.id in deleted_ids assert child2.id in deleted_ids class TestDocumentEvents: """Tests for document events.""" def test_document_added_event(self, space_service, event_bus): """Test that DOCUMENT_ADDED event is emitted.""" space = space_service.create_space(name="doc-space") received_events = [] event_bus.subscribe(SpaceEventType.DOCUMENT_ADDED, received_events.append) doc = space_service.add_document(space.id, "/intro.md") assert len(received_events) == 1 event = received_events[0] assert event.event_type == SpaceEventType.DOCUMENT_ADDED assert event.space_id == space.id assert event.payload["document_id"] == doc.id assert event.payload["space_path"] == "/intro.md" def test_document_removed_event(self, space_service, event_bus): """Test that DOCUMENT_REMOVED event is emitted.""" space = space_service.create_space(name="doc-space") doc = space_service.add_document(space.id, "/to-remove.md") received_events = [] event_bus.subscribe(SpaceEventType.DOCUMENT_REMOVED, received_events.append) space_service.remove_document(doc.id) assert len(received_events) == 1 event = received_events[0] assert event.event_type == SpaceEventType.DOCUMENT_REMOVED assert event.space_id == space.id assert event.payload["document_id"] == doc.id assert event.payload["space_path"] == "/to-remove.md" def test_document_moved_event(self, space_service, event_bus): """Test that DOCUMENT_MOVED event is emitted.""" space = space_service.create_space(name="doc-space") doc = space_service.add_document(space.id, "/old-path.md") received_events = [] event_bus.subscribe(SpaceEventType.DOCUMENT_MOVED, received_events.append) space_service.move_document(doc.id, "/new-path.md") assert len(received_events) == 1 event = received_events[0] assert event.event_type == SpaceEventType.DOCUMENT_MOVED assert event.space_id == space.id assert event.payload["old_path"] == "/old-path.md" assert event.payload["new_path"] == "/new-path.md" def test_document_content_changed_event(self, space_service, event_bus): """Test that DOCUMENT_CONTENT_CHANGED event is emitted.""" space = space_service.create_space(name="doc-space") doc = space_service.add_document( space.id, "/content.md", content_hash="hash-v1" ) received_events = [] event_bus.subscribe( SpaceEventType.DOCUMENT_CONTENT_CHANGED, received_events.append ) space_service.update_document_hash(doc.id, "hash-v2") assert len(received_events) == 1 event = received_events[0] assert event.event_type == SpaceEventType.DOCUMENT_CONTENT_CHANGED assert event.space_id == space.id assert event.payload["old_hash"] == "hash-v1" assert event.payload["new_hash"] == "hash-v2" def test_no_event_if_hash_unchanged(self, space_service, event_bus): """Test that no event is emitted if hash is unchanged.""" space = space_service.create_space(name="doc-space") doc = space_service.add_document( space.id, "/content.md", content_hash="same-hash" ) received_events = [] event_bus.subscribe( SpaceEventType.DOCUMENT_CONTENT_CHANGED, received_events.append ) space_service.update_document_hash(doc.id, "same-hash") assert len(received_events) == 0 class TestVariableEvents: """Tests for variable events.""" def test_variable_set_event(self, space_service, event_bus): """Test that VARIABLE_SET event is emitted.""" space = space_service.create_space(name="var-space") received_events = [] event_bus.subscribe(SpaceEventType.VARIABLE_SET, received_events.append) space_service.set_variable(space.id, "version", "1.0.0") assert len(received_events) == 1 event = received_events[0] assert event.event_type == SpaceEventType.VARIABLE_SET assert event.space_id == space.id assert event.payload["name"] == "version" def test_variable_deleted_event(self, space_service, event_bus): """Test that VARIABLE_DELETED event is emitted.""" space = space_service.create_space(name="var-space") space_service.set_variable(space.id, "temp", "value") received_events = [] event_bus.subscribe(SpaceEventType.VARIABLE_DELETED, received_events.append) space_service.delete_variable(space.id, "temp") assert len(received_events) == 1 event = received_events[0] assert event.event_type == SpaceEventType.VARIABLE_DELETED assert event.space_id == space.id assert event.payload["name"] == "temp" def test_no_event_if_variable_not_found(self, space_service, event_bus): """Test that no event is emitted if variable doesn't exist.""" space = space_service.create_space(name="var-space") received_events = [] event_bus.subscribe(SpaceEventType.VARIABLE_DELETED, received_events.append) space_service.delete_variable(space.id, "non-existent") assert len(received_events) == 0 class TestEventHandlerIntegration: """Tests for event handler integration patterns.""" def test_global_handler_receives_all_events(self, space_service, event_bus): """Test that a global handler receives all event types.""" received_events = [] event_bus.subscribe_all(received_events.append) space = space_service.create_space(name="test") space_service.add_document(space.id, "/doc.md") space_service.set_variable(space.id, "var", "value") space_service.delete_space(space.id) # Should have: SPACE_CREATED, DOCUMENT_ADDED, VARIABLE_SET, SPACE_DELETED assert len(received_events) >= 4 event_types = {e.event_type for e in received_events} assert SpaceEventType.SPACE_CREATED in event_types assert SpaceEventType.DOCUMENT_ADDED in event_types assert SpaceEventType.VARIABLE_SET in event_types assert SpaceEventType.SPACE_DELETED in event_types def test_event_history_records_all_events(self, space_service, event_bus): """Test that event history captures all events.""" event_bus.enable_history() space = space_service.create_space(name="history-test") space_service.add_document(space.id, "/doc1.md") space_service.add_document(space.id, "/doc2.md") space_service.activate_space(space.id) history = event_bus.get_history() assert len(history) == 4 # Newest first assert history[0].event_type == SpaceEventType.SPACE_ACTIVATED assert history[1].event_type == SpaceEventType.DOCUMENT_ADDED assert history[2].event_type == SpaceEventType.DOCUMENT_ADDED assert history[3].event_type == SpaceEventType.SPACE_CREATED def test_filter_history_by_space(self, space_service, event_bus): """Test filtering event history by space ID.""" event_bus.enable_history() space1 = space_service.create_space(name="space1") space2 = space_service.create_space(name="space2") space_service.add_document(space1.id, "/doc.md") space_service.add_document(space2.id, "/doc.md") history_space1 = event_bus.get_history(space_id=space1.id) assert len(history_space1) == 2 # create + add_document history_space2 = event_bus.get_history(space_id=space2.id) assert len(history_space2) == 2 def test_multiple_handlers_for_same_event(self, space_service, event_bus): """Test that multiple handlers can listen to the same event.""" handler1_events = [] handler2_events = [] event_bus.subscribe(SpaceEventType.SPACE_CREATED, handler1_events.append) event_bus.subscribe(SpaceEventType.SPACE_CREATED, handler2_events.append) space_service.create_space(name="multi-handler-test") assert len(handler1_events) == 1 assert len(handler2_events) == 1 assert handler1_events[0].event_id == handler2_events[0].event_id class TestEventDrivenWorkflow: """Tests for event-driven workflow patterns.""" def test_cache_invalidation_pattern(self, space_service, event_bus): """Test a cache invalidation pattern using events.""" cache = {} def on_content_changed(event: SpaceEvent): doc_id = event.payload["document_id"] if doc_id in cache: del cache[doc_id] event_bus.subscribe( SpaceEventType.DOCUMENT_CONTENT_CHANGED, on_content_changed ) # Setup space = space_service.create_space(name="cache-test") doc = space_service.add_document(space.id, "/doc.md", content_hash="v1") # Simulate cached content cache[doc.id] = "cached content" # Update triggers cache invalidation via event space_service.update_document_hash(doc.id, "v2") assert doc.id not in cache def test_audit_log_pattern(self, space_service, event_bus): """Test an audit log pattern using events.""" audit_log = [] def log_event(event: SpaceEvent): audit_log.append({ "event_type": event.event_type.value, "space_id": event.space_id, "timestamp": event.timestamp.isoformat(), }) event_bus.subscribe_all(log_event) space = space_service.create_space(name="audit-test") space_service.activate_space(space.id) space_service.archive_space(space.id) space_service.delete_space(space.id) # Should have complete audit trail assert len(audit_log) == 4 event_types = [entry["event_type"] for entry in audit_log] assert "space.created" in event_types assert "space.activated" in event_types assert "space.archived" in event_types assert "space.deleted" in event_types