# pyright: reportUnknownParameterType=false, reportMissingParameterType=false, reportUnusedParameter=false """Unit tests for telemetry event classes. Tests all telemetry event types for: - Correct instantiation with required fields - event_type(), schema_version(), category() class methods - get_resource_id() returns expected format - generate_id() is deterministic (same inputs = same ID) - Timestamp defaults to UTC now """ from datetime import UTC, datetime import pytest from pydantic import ValidationError from src.telemetry.events.agent import ( AgentIterationEvent, AgentToolConclusionsCreatedEvent, AgentToolConclusionsDeletedEvent, AgentToolPeerCardUpdatedEvent, AgentToolSummaryCreatedEvent, ) from src.telemetry.events.api import ( FileUploadedEvent, GetContextEvent, MessageCreatedEvent, ) from src.telemetry.events.base import BaseEvent, generate_event_id from src.telemetry.events.deletion import DeletionCompletedEvent from src.telemetry.events.dialectic import DialecticCompletedEvent from src.telemetry.events.dream import DreamRunEvent, DreamSpecialistEvent from src.telemetry.events.llm import CallPurpose, LLMCallCompletedEvent from src.telemetry.events.reconciliation import ( CleanupStaleItemsCompletedEvent, SyncVectorsCompletedEvent, ) from src.telemetry.events.representation import RepresentationCompletedEvent # ============================================================================= # Tests for generate_event_id function # ============================================================================= class TestGenerateEventId: """Tests for the generate_event_id function.""" def test_deterministic_output(self, fixed_timestamp: datetime): """Same inputs always produce the same ID.""" event_type = "test.event" resource_id = "resource_123" id1 = generate_event_id(event_type, fixed_timestamp, resource_id) id2 = generate_event_id(event_type, fixed_timestamp, resource_id) assert id1 == id2 def test_different_event_types_produce_different_ids( self, fixed_timestamp: datetime ): """Different event types produce different IDs.""" resource_id = "resource_123" id1 = generate_event_id("type.a", fixed_timestamp, resource_id) id2 = generate_event_id("type.b", fixed_timestamp, resource_id) assert id1 != id2 def test_different_timestamps_produce_different_ids(self): """Different timestamps produce different IDs.""" event_type = "test.event" resource_id = "resource_123" ts1 = datetime(2024, 1, 1, 12, 0, 0, tzinfo=UTC) ts2 = datetime(2024, 1, 1, 12, 0, 1, tzinfo=UTC) id1 = generate_event_id(event_type, ts1, resource_id) id2 = generate_event_id(event_type, ts2, resource_id) assert id1 != id2 def test_different_resource_ids_produce_different_ids( self, fixed_timestamp: datetime ): """Different resource IDs produce different IDs.""" event_type = "test.event" id1 = generate_event_id(event_type, fixed_timestamp, "resource_a") id2 = generate_event_id(event_type, fixed_timestamp, "resource_b") assert id1 != id2 def test_id_format(self, fixed_timestamp: datetime): """Event ID has correct format: evt_{base64_hash}.""" event_id = generate_event_id("test.event", fixed_timestamp, "resource_123") assert event_id.startswith("evt_") # Base64url encoded 16 bytes = 22 chars (without padding) assert len(event_id) == 4 + 22 # "evt_" + 22 chars def test_honcho_version_changes_id(self, fixed_timestamp: datetime): """Same event from different deploys must produce distinct IDs so downstream dedupe doesn't silently merge events whose payload shape may have shifted between versions.""" id_a = generate_event_id( "test.event", fixed_timestamp, "resource_1", honcho_version="2.0.0" ) id_b = generate_event_id( "test.event", fixed_timestamp, "resource_1", honcho_version="2.0.1" ) assert id_a != id_b def test_honcho_version_none_matches_empty(self, fixed_timestamp: datetime): """None and unset version segments are equivalent — backwards- compatible with callers that don't pass the new kwarg yet.""" id_default = generate_event_id("test.event", fixed_timestamp, "resource_1") id_explicit_none = generate_event_id( "test.event", fixed_timestamp, "resource_1", honcho_version=None ) assert id_default == id_explicit_none # ============================================================================= # Tests for BaseEvent class # ============================================================================= class TestBaseEvent: """Tests for BaseEvent base class behavior.""" def test_timestamp_defaults_to_utc_now(self): """Events without explicit timestamp get current UTC time.""" # Create event without timestamp event = RepresentationCompletedEvent( workspace_name="test", session_name="test_session", observed="user", queue_items_processed=1, earliest_message_id="msg_1", latest_message_id="msg_2", message_count=2, explicit_conclusion_count=1, context_preparation_ms=10.0, llm_call_ms=100.0, total_duration_ms=110.0, input_tokens=100, total_input_tokens=150, output_tokens=50, ) # Timestamp should be set and be UTC assert event.timestamp is not None assert event.timestamp.tzinfo == UTC def test_get_resource_id_not_implemented_on_base(self): """BaseEvent.get_resource_id raises NotImplementedError.""" # Can't instantiate BaseEvent directly, but we can test the method with pytest.raises(NotImplementedError, match="Subclasses must implement"): BaseEvent.get_resource_id(BaseEvent(timestamp=datetime.now(UTC))) # ============================================================================= # Tests for RepresentationCompletedEvent # ============================================================================= class TestRepresentationCompletedEvent: """Tests for RepresentationCompletedEvent.""" def test_event_type(self): """event_type() returns correct value.""" assert RepresentationCompletedEvent.event_type() == "representation.completed" def test_category(self): """category() returns correct value.""" assert RepresentationCompletedEvent.category() == "representation" def test_get_resource_id( self, sample_representation_event: RepresentationCompletedEvent ): """get_resource_id() returns workspace:session:message format.""" resource_id = sample_representation_event.get_resource_id() assert resource_id == "test_workspace:test_session:msg_010" def test_generate_id_deterministic( self, sample_representation_event: RepresentationCompletedEvent ): """generate_id() produces same ID for same event.""" id1 = sample_representation_event.generate_id() id2 = sample_representation_event.generate_id() assert id1 == id2 assert id1.startswith("evt_") def test_required_fields(self): """Event requires all mandatory fields.""" with pytest.raises(ValidationError): RepresentationCompletedEvent() # pyright: ignore[reportCallIssue] def test_model_dump( self, sample_representation_event: RepresentationCompletedEvent ): """Event can be serialized to dict.""" data = sample_representation_event.model_dump(mode="json") assert data["workspace_name"] == "test_workspace" assert data["message_count"] == 10 assert data["explicit_conclusion_count"] == 5 # ============================================================================= # Tests for LLMCallCompletedEvent () # ============================================================================= class TestLLMCallCompletedEvent: """Tests for the LLMCallCompletedEvent.""" def test_event_type(self): assert LLMCallCompletedEvent.event_type() == "llm.call.completed" def test_category(self): assert LLMCallCompletedEvent.category() == "llm" def test_volume_class(self): # event must be high_volume so the sampler picks it up. assert LLMCallCompletedEvent.volume_class() == "high_volume" def test_get_resource_id_includes_attempt( self, sample_llm_call_event: LLMCallCompletedEvent ): # Resource id must include attempt so multiple retry attempts in one # iteration get distinct deterministic ids. assert ( sample_llm_call_event.get_resource_id() == "abc12345:1:1:anthropic:claude-sonnet-4-5" ) def test_call_purpose_enum_values(self): # The closed taxonomy used by callers. assert CallPurpose.DERIVER_REPRESENTATION.value == "deriver.representation" assert CallPurpose.DIALECTIC_ANSWER.value == "dialectic.answer" assert CallPurpose.DREAM_DEDUCTION.value == "dream.deduction" assert CallPurpose.DREAM_INDUCTION.value == "dream.induction" assert CallPurpose.DREAM_CARD_REFRESH.value == "dream.card_refresh" assert CallPurpose.SUMMARY_SHORT.value == "summary.short" assert CallPurpose.SUMMARY_LONG.value == "summary.long" def test_error_outcome_with_error_class(self, fixed_timestamp: datetime): event = LLMCallCompletedEvent( timestamp=fixed_timestamp, transport="openai", model="gpt-4", effective_max_output_tokens=512, outcome="error", is_final_attempt=True, error_class="RateLimitError", attempt=3, retry_attempts=3, was_fallback=True, duration_ms=200.0, ) assert event.outcome == "error" assert event.error_class == "RateLimitError" assert event.is_final_attempt is True # Token fields default to 0 when no result was produced. assert event.provider_input_tokens == 0 assert event.provider_output_tokens == 0 def test_stream_placeholder_has_zero_tokens(self, fixed_timestamp: datetime): event = LLMCallCompletedEvent( timestamp=fixed_timestamp, transport="anthropic", model="claude-sonnet-4-5", effective_max_output_tokens=2048, outcome="success", is_final_attempt=False, attempt=1, retry_attempts=3, was_fallback=False, duration_ms=0.0, was_stream=True, ) assert event.was_stream is True assert event.provider_input_tokens == 0 assert event.provider_output_tokens == 0 # ============================================================================= # Tests for MessageCreatedEvent # ============================================================================= class TestMessageCreatedEvent: """Tests for MessageCreatedEvent.""" def test_event_type(self): """event_type() returns correct value.""" assert MessageCreatedEvent.event_type() == "message.created" def test_category(self): """category() returns correct value.""" assert MessageCreatedEvent.category() == "api" def test_get_resource_id(self, sample_message_created_event: MessageCreatedEvent): """get_resource_id() keys on workspace:session:source:last_message_id.""" assert ( sample_message_created_event.get_resource_id() == "test_workspace:test_session:api:msg_abc123_fixture_____" ) def test_source_defaults_to_api(self, fixed_timestamp: datetime): """source defaults to api.""" event = MessageCreatedEvent( timestamp=fixed_timestamp, workspace_name="test_workspace", session_name="test_session", message_count=1, total_tokens=100, last_message_id="msg_default_source_____", ) assert event.source == "api" def test_distinct_batches_get_distinct_ids(self, fixed_timestamp: datetime): """Two batches of the same size in the same session+source must produce different event ids — the previous (v1) key collided here.""" e1 = MessageCreatedEvent( timestamp=fixed_timestamp, workspace_name="ws", session_name="sess", message_count=5, total_tokens=500, source="api", last_message_id="msg_first_batch________", ) e2 = MessageCreatedEvent( timestamp=fixed_timestamp, workspace_name="ws", session_name="sess", message_count=5, total_tokens=500, source="api", last_message_id="msg_second_batch_______", ) assert e1.generate_id() != e2.generate_id() assert e1.get_resource_id() != e2.get_resource_id() # ============================================================================= # Tests for FileUploadedEvent # ============================================================================= class TestFileUploadedEvent: """Tests for FileUploadedEvent.""" def test_event_type(self): """event_type() returns correct value.""" assert FileUploadedEvent.event_type() == "file.uploaded" def test_category(self): """category() returns correct value.""" assert FileUploadedEvent.category() == "api" def test_get_resource_id(self, sample_file_uploaded_event: FileUploadedEvent): """get_resource_id() returns workspace:session:file format.""" assert ( sample_file_uploaded_event.get_resource_id() == "test_workspace:test_session:file_123" ) def test_optional_file_fields(self, fixed_timestamp: datetime): """filename, content_type, and file_size_bytes are optional.""" event = FileUploadedEvent( timestamp=fixed_timestamp, workspace_name="test_workspace", session_name="test_session", peer_name="user_peer", file_id="file_123", message_count=1, total_tokens=100, ) assert event.filename is None assert event.content_type is None assert event.file_size_bytes is None # ============================================================================= # Tests for GetContextEvent # ============================================================================= class TestGetContextEvent: """Tests for GetContextEvent.""" def test_event_type(self): """event_type() returns correct value.""" assert GetContextEvent.event_type() == "context.retrieved" def test_category(self): """category() returns correct value.""" assert GetContextEvent.category() == "api" def test_get_resource_id_session(self, sample_get_context_event: GetContextEvent): """session context resource ID includes workspace and session. Uses empty-string sentinel for unset peer/target so that a peer literally named "none" can't collide with the absent-peer case. """ assert ( sample_get_context_event.get_resource_id() == "test_workspace:session:test_session::" ) def test_get_resource_id_disambiguates_peer_named_none( self, fixed_timestamp: datetime ): """Regression: a peer literally named "none" must NOT collide with absent-peer resource ids. Empty-string sentinel guards this. """ absent = GetContextEvent( timestamp=fixed_timestamp, workspace_name="ws", context_scope="peer", total_duration_ms=1.0, ) literal_none = GetContextEvent( timestamp=fixed_timestamp, workspace_name="ws", context_scope="peer", peer_name="none", target_name="none", total_duration_ms=1.0, ) assert absent.get_resource_id() != literal_none.get_resource_id() def test_get_resource_id_peer(self, fixed_timestamp: datetime): """peer context resource ID includes observer and observed peers.""" event = GetContextEvent( timestamp=fixed_timestamp, workspace_name="test_workspace", context_scope="peer", peer_name="observer", target_name="observed", total_duration_ms=10.0, ) assert event.get_resource_id() == "test_workspace:peer:observer:observed" def test_context_defaults(self, fixed_timestamp: datetime): """Context booleans and counts have conservative defaults.""" event = GetContextEvent( timestamp=fixed_timestamp, workspace_name="test_workspace", context_scope="session", session_name="test_session", total_duration_ms=10.0, ) assert event.message_count == 0 assert event.has_summary is False assert event.has_representation is False assert event.include_summary is None assert event.tokens_requested is None assert event.peer_perspective_provided is False def test_session_context_can_record_raw_request_options( self, fixed_timestamp: datetime ): """Raw request options can be recorded independently of resolved values.""" event = GetContextEvent( timestamp=fixed_timestamp, workspace_name="test_workspace", context_scope="session", session_name="test_session", peer_name="observer", target_name="observed", tokens_requested=8000, include_summary=False, peer_perspective_provided=True, total_duration_ms=10.0, ) assert event.tokens_requested == 8000 assert event.include_summary is False assert event.peer_perspective_provided is True # ============================================================================= # Tests for DreamRunEvent # ============================================================================= class TestDreamRunEvent: """Tests for DreamRunEvent.""" def test_event_type(self): """event_type() returns correct value.""" assert DreamRunEvent.event_type() == "dream.run" def test_category(self): """category() returns correct value.""" assert DreamRunEvent.category() == "dream" def test_get_resource_id(self, sample_dream_run_event: DreamRunEvent): """get_resource_id() returns run_id.""" assert sample_dream_run_event.get_resource_id() == "abc12345" def test_generate_id_deterministic(self, sample_dream_run_event: DreamRunEvent): """generate_id() produces same ID for same event.""" id1 = sample_dream_run_event.generate_id() id2 = sample_dream_run_event.generate_id() assert id1 == id2 def test_specialists_run_list(self, sample_dream_run_event: DreamRunEvent): """specialists_run contains expected values.""" assert "deduction" in sample_dream_run_event.specialists_run assert "induction" in sample_dream_run_event.specialists_run # ============================================================================= # Tests for DreamSpecialistEvent # ============================================================================= class TestDreamSpecialistEvent: """Tests for DreamSpecialistEvent.""" def test_event_type(self): """event_type() returns correct value.""" assert DreamSpecialistEvent.event_type() == "dream.specialist" def test_category(self): """category() returns correct value.""" assert DreamSpecialistEvent.category() == "dream" def test_get_resource_id(self, sample_dream_specialist_event: DreamSpecialistEvent): """get_resource_id() returns run_id:specialist_type format.""" assert sample_dream_specialist_event.get_resource_id() == "abc12345:deduction" def test_generate_id_deterministic( self, sample_dream_specialist_event: DreamSpecialistEvent ): """generate_id() produces same ID for same event.""" id1 = sample_dream_specialist_event.generate_id() id2 = sample_dream_specialist_event.generate_id() assert id1 == id2 # ============================================================================= # Tests for DialecticCompletedEvent # ============================================================================= class TestDialecticCompletedEvent: """Tests for DialecticCompletedEvent.""" def test_event_type(self): """event_type() returns correct value.""" assert DialecticCompletedEvent.event_type() == "dialectic.completed" def test_category(self): """category() returns correct value.""" assert DialecticCompletedEvent.category() == "dialectic" def test_get_resource_id(self, sample_dialectic_event: DialecticCompletedEvent): """get_resource_id() returns run_id.""" assert sample_dialectic_event.get_resource_id() == "def67890" def test_generate_id_deterministic( self, sample_dialectic_event: DialecticCompletedEvent ): """generate_id() produces same ID for same event.""" id1 = sample_dialectic_event.generate_id() id2 = sample_dialectic_event.generate_id() assert id1 == id2 def test_optional_session_fields(self, fixed_timestamp: datetime): """session_id and session_name are optional.""" event = DialecticCompletedEvent( timestamp=fixed_timestamp, run_id="test123", workspace_name="test", peer_name="user", reasoning_level="low", total_duration_ms=1000.0, input_tokens=500, output_tokens=100, ) assert event.session_name is None def test_cache_token_defaults(self, fixed_timestamp: datetime): """Cache tokens default to 0.""" event = DialecticCompletedEvent( timestamp=fixed_timestamp, run_id="test123", workspace_name="test", peer_name="user", reasoning_level="low", total_duration_ms=1000.0, input_tokens=500, output_tokens=100, ) assert event.cache_read_tokens == 0 assert event.cache_creation_tokens == 0 # ============================================================================= # Tests for AgentIterationEvent # ============================================================================= class TestAgentIterationEvent: """Tests for AgentIterationEvent.""" def test_event_type(self): """event_type() returns correct value.""" assert AgentIterationEvent.event_type() == "agent.iteration" def test_category(self): """category() returns correct value.""" assert AgentIterationEvent.category() == "agent" def test_get_resource_id(self, sample_agent_iteration_event: AgentIterationEvent): """get_resource_id() returns run_id:iteration format.""" assert sample_agent_iteration_event.get_resource_id() == "abc12345:3" def test_generate_id_deterministic( self, sample_agent_iteration_event: AgentIterationEvent ): """generate_id() produces same ID for same event.""" id1 = sample_agent_iteration_event.generate_id() id2 = sample_agent_iteration_event.generate_id() assert id1 == id2 def test_tool_calls_list(self, sample_agent_iteration_event: AgentIterationEvent): """tool_calls is a list of strings.""" assert isinstance(sample_agent_iteration_event.tool_calls, list) assert "search_memory" in sample_agent_iteration_event.tool_calls def test_optional_peer_fields(self, fixed_timestamp: datetime): """observer, observed, peer_id are optional.""" event = AgentIterationEvent( timestamp=fixed_timestamp, run_id="test123", parent_category="dialectic", agent_type="dialectic", workspace_name="test", iteration=1, input_tokens=100, output_tokens=50, ) assert event.observer is None assert event.observed is None # ============================================================================= # Tests for AgentToolConclusionsCreatedEvent # ============================================================================= class TestAgentToolConclusionsCreatedEvent: """Tests for AgentToolConclusionsCreatedEvent.""" def test_event_type(self): """event_type() returns correct value.""" assert ( AgentToolConclusionsCreatedEvent.event_type() == "agent.tool.conclusions.created" ) def test_category(self): """category() returns correct value.""" assert AgentToolConclusionsCreatedEvent.category() == "agent" def test_get_resource_id( self, sample_conclusions_created_event: AgentToolConclusionsCreatedEvent ): """get_resource_id() returns run_id:iteration:conclusions_created format.""" assert ( sample_conclusions_created_event.get_resource_id() == "abc12345:3:conclusions_created" ) def test_levels_list( self, sample_conclusions_created_event: AgentToolConclusionsCreatedEvent ): """levels is a list matching conclusion_count.""" assert len(sample_conclusions_created_event.levels) == 5 assert sample_conclusions_created_event.conclusion_count == 5 # ============================================================================= # Tests for AgentToolConclusionsDeletedEvent # ============================================================================= class TestAgentToolConclusionsDeletedEvent: """Tests for AgentToolConclusionsDeletedEvent.""" def test_event_type(self): """event_type() returns correct value.""" assert ( AgentToolConclusionsDeletedEvent.event_type() == "agent.tool.conclusions.deleted" ) def test_category(self): """category() returns correct value.""" assert AgentToolConclusionsDeletedEvent.category() == "agent" def test_get_resource_id( self, sample_conclusions_deleted_event: AgentToolConclusionsDeletedEvent ): """get_resource_id() returns run_id:iteration:conclusions_deleted format.""" assert ( sample_conclusions_deleted_event.get_resource_id() == "abc12345:5:conclusions_deleted" ) # ============================================================================= # Tests for AgentToolPeerCardUpdatedEvent # ============================================================================= class TestAgentToolPeerCardUpdatedEvent: """Tests for AgentToolPeerCardUpdatedEvent.""" def test_event_type(self): """event_type() returns correct value.""" assert ( AgentToolPeerCardUpdatedEvent.event_type() == "agent.tool.peer_card.updated" ) def test_category(self): """category() returns correct value.""" assert AgentToolPeerCardUpdatedEvent.category() == "agent" def test_get_resource_id( self, sample_peer_card_updated_event: AgentToolPeerCardUpdatedEvent ): """get_resource_id() returns run_id:iteration:peer_card_updated format.""" assert ( sample_peer_card_updated_event.get_resource_id() == "abc12345:7:peer_card_updated" ) # ============================================================================= # Tests for AgentToolSummaryCreatedEvent # ============================================================================= class TestAgentToolSummaryCreatedEvent: """Tests for AgentToolSummaryCreatedEvent.""" def test_event_type(self): """event_type() returns correct value.""" assert AgentToolSummaryCreatedEvent.event_type() == "agent.tool.summary.created" def test_category(self): """category() returns correct value.""" assert AgentToolSummaryCreatedEvent.category() == "agent" def test_get_resource_id( self, sample_summary_created_event: AgentToolSummaryCreatedEvent ): """get_resource_id() keys on message_id:summary_type (run_id/iteration are None for the non-agentic summarizer and can't identify the summary).""" assert ( sample_summary_created_event.get_resource_id() == "msg_020:short:summary_created" ) def test_summary_type_values(self, fixed_timestamp: datetime): """summary_type accepts 'short' and 'long'.""" for summary_type in ["short", "long"]: event = AgentToolSummaryCreatedEvent( timestamp=fixed_timestamp, run_id="test123", iteration=1, parent_category="representation", agent_type="summarizer", workspace_name="test", session_name="test_session", message_id="msg_1", message_count=10, message_seq_in_session=10, summary_type=summary_type, input_tokens=100, output_tokens=50, ) assert event.summary_type == summary_type # ============================================================================= # Tests for DeletionCompletedEvent # ============================================================================= class TestDeletionCompletedEvent: """Tests for DeletionCompletedEvent.""" def test_event_type(self): """event_type() returns correct value.""" assert DeletionCompletedEvent.event_type() == "deletion.completed" def test_category(self): """category() returns correct value.""" assert DeletionCompletedEvent.category() == "deletion" def test_get_resource_id(self, sample_deletion_event: DeletionCompletedEvent): """get_resource_id() returns workspace:type:resource format.""" assert ( sample_deletion_event.get_resource_id() == "test_workspace:workspace:ws_123abc" ) def test_cascade_counts_default_to_zero(self, fixed_timestamp: datetime): """Cascade counts default to 0 for non-workspace deletions.""" event = DeletionCompletedEvent( timestamp=fixed_timestamp, workspace_name="test", deletion_type="session", resource_id="sess_456", success=True, ) assert event.peers_deleted == 0 assert event.sessions_deleted == 0 assert event.messages_deleted == 0 assert event.conclusions_deleted == 0 def test_error_message_optional(self, fixed_timestamp: datetime): """error_message is optional and defaults to None.""" event = DeletionCompletedEvent( timestamp=fixed_timestamp, workspace_name="test", deletion_type="session", resource_id="sess_456", success=True, ) assert event.error_message is None def test_failed_deletion_with_error(self, fixed_timestamp: datetime): """Failed deletion can include error message.""" event = DeletionCompletedEvent( timestamp=fixed_timestamp, workspace_name="test", deletion_type="session", resource_id="sess_456", success=False, error_message="Foreign key constraint violation", ) assert event.success is False assert event.error_message == "Foreign key constraint violation" # ============================================================================= # Tests for SyncVectorsCompletedEvent # ============================================================================= class TestSyncVectorsCompletedEvent: """Tests for SyncVectorsCompletedEvent.""" def test_event_type(self): """event_type() returns correct value.""" assert ( SyncVectorsCompletedEvent.event_type() == "reconciliation.sync_vectors.completed" ) def test_category(self): """category() returns correct value.""" assert SyncVectorsCompletedEvent.category() == "reconciliation" def test_get_resource_id( self, sample_sync_vectors_event: SyncVectorsCompletedEvent ): """get_resource_id() returns fixed string.""" assert sample_sync_vectors_event.get_resource_id() == "sync_vectors" def test_metrics_default_to_zero(self, fixed_timestamp: datetime): """Sync metrics default to 0.""" event = SyncVectorsCompletedEvent( timestamp=fixed_timestamp, total_duration_ms=1000.0, ) assert event.documents_synced == 0 assert event.documents_failed == 0 assert event.message_embeddings_synced == 0 assert event.message_embeddings_failed == 0 # ============================================================================= # Tests for CleanupStaleItemsCompletedEvent # ============================================================================= class TestCleanupStaleItemsCompletedEvent: """Tests for CleanupStaleItemsCompletedEvent.""" def test_event_type(self): """event_type() returns correct value.""" assert ( CleanupStaleItemsCompletedEvent.event_type() == "reconciliation.cleanup_stale_items.completed" ) def test_category(self): """category() returns correct value.""" assert CleanupStaleItemsCompletedEvent.category() == "reconciliation" def test_get_resource_id( self, sample_cleanup_event: CleanupStaleItemsCompletedEvent ): """get_resource_id() returns fixed string.""" assert sample_cleanup_event.get_resource_id() == "cleanup_stale_items" def test_cleanup_metrics_default_to_zero(self, fixed_timestamp: datetime): """Cleanup metrics default to 0.""" event = CleanupStaleItemsCompletedEvent( timestamp=fixed_timestamp, total_duration_ms=500.0, ) assert event.documents_cleaned == 0 assert event.queue_items_cleaned == 0 def test_queue_items_cleaned_round_trips_through_pydantic( self, fixed_timestamp: datetime ): """Regression: `queue_items_cleaned` is a real field, not just plumbing. Previously the consumer emit site dropped the captured `deleted_count` and the field always defaulted to 0 on the wire. """ event = CleanupStaleItemsCompletedEvent( timestamp=fixed_timestamp, total_duration_ms=500.0, queue_items_cleaned=42, ) assert event.queue_items_cleaned == 42 # Serialize → deserialize to ensure the field crosses the wire. data = event.model_dump(mode="json") assert data["queue_items_cleaned"] == 42 round_tripped = CleanupStaleItemsCompletedEvent.model_validate(data) assert round_tripped.queue_items_cleaned == 42 # ============================================================================= # Parametrized tests across all event types # ============================================================================= class TestAllEventTypes: """Parametrized tests that run across all event types.""" def test_all_events_have_timestamp(self, all_sample_events: list[BaseEvent]): """All events have a timestamp field.""" for event in all_sample_events: assert hasattr(event, "timestamp") assert event.timestamp is not None def test_all_events_generate_valid_ids(self, all_sample_events: list[BaseEvent]): """All events generate valid event IDs.""" for event in all_sample_events: event_id = event.generate_id() assert event_id.startswith("evt_") assert len(event_id) == 26 # "evt_" + 22 chars def test_all_events_have_event_type(self, all_sample_events: list[BaseEvent]): """All events return a non-empty event_type.""" for event in all_sample_events: event_type = event.event_type() assert event_type assert isinstance(event_type, str) def test_all_events_have_category(self, all_sample_events: list[BaseEvent]): """All events return a non-empty category.""" for event in all_sample_events: category = event.category() assert category assert isinstance(category, str) def test_all_events_have_schema_version(self, all_sample_events: list[BaseEvent]): """All events return a positive schema version.""" for event in all_sample_events: version = event.schema_version() assert version >= 1 assert isinstance(version, int) def test_all_events_can_serialize_to_json(self, all_sample_events: list[BaseEvent]): """All events can be serialized to JSON-compatible dict.""" for event in all_sample_events: data = event.model_dump(mode="json") assert isinstance(data, dict) # Timestamp should be serialized as ISO string assert "timestamp" in data def test_all_events_have_get_resource_id(self, all_sample_events: list[BaseEvent]): """All events implement get_resource_id().""" for event in all_sample_events: resource_id = event.get_resource_id() assert resource_id assert isinstance(resource_id, str)