998 lines
37 KiB
Python
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)
|