honcho/tests/telemetry/test_events.py

998 lines
37 KiB
Python

# 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.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() returns run_id:iteration:summary_created format."""
assert (
sample_summary_created_event.get_resource_id()
== "ghi11111:1: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)