diff --git a/client b/client
index 34bed4d..1655334 160000
--- a/client
+++ b/client
@@ -1 +1 @@
-Subproject commit 34bed4dd55f996b124605ee0d42425ecc600845b
+Subproject commit 1655334a006f2b5db8d0a310919a39d31ca73279
diff --git a/src/noteflow/infrastructure/export/html.py b/src/noteflow/infrastructure/export/html.py
index 6c88d89..32e74a5 100644
--- a/src/noteflow/infrastructure/export/html.py
+++ b/src/noteflow/infrastructure/export/html.py
@@ -23,43 +23,48 @@ if TYPE_CHECKING:
from noteflow.domain.entities.summary import Summary
-# HTML template with embedded CSS for print-friendly output
-_HTML_TEMPLATE = """
-
-
-
-
- {title}
-
+ }
+ h1 { color: #1a1a1a; border-bottom: 2px solid #e0e0e0; padding-bottom: 0.5rem; }
+ h2 { color: #2c2c2c; margin-top: 2rem; }
+ h3 { color: #444; }
+ .metadata { background: #f5f5f5; padding: 1rem; border-radius: 8px; margin-bottom: 2rem; }
+ .metadata dt { font-weight: bold; display: inline; }
+ .metadata dd { display: inline; margin: 0 1rem 0 0.5rem; }
+ .transcript { margin: 1rem 0; }
+ .segment { margin-bottom: 1rem; padding: 0.5rem; border-left: 3px solid #e0e0e0; }
+ .segment:hover { background: #f9f9f9; }
+ .timestamp { color: #666; font-size: 0.9em; font-weight: bold; margin-right: 0.5rem; }
+ .summary { background: #f0f7ff; padding: 1rem; border-radius: 8px; margin-top: 2rem; }
+ .key-points li, .action-items li { margin-bottom: 0.5rem; }
+ .action-items li { list-style-type: none; }
+ .action-items li::before { content: '☐ '; }
+ .assignee { color: #0066cc; font-size: 0.9em; }
+ footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid #e0e0e0; color: #888; font-size: 0.9em; }
+ @media print {
+ body { max-width: none; padding: 1cm; }
+ .segment:hover { background: none; }
+ }
+"""
+
+
+def _build_html_document(title: str, content: str) -> str:
+ """Build complete HTML document with title and content."""
+ return f"""
+
+
+
+
+ {title}
+
{content}
@@ -184,4 +189,4 @@ class HtmlExporter:
)
)
content = "\n".join(content_parts)
- return _HTML_TEMPLATE.format(title=escape_html(meeting.title), content=content)
+ return _build_html_document(title=escape_html(meeting.title), content=content)
diff --git a/src/noteflow/infrastructure/observability/usage.py b/src/noteflow/infrastructure/observability/usage.py
index 6decfe0..e34d122 100644
--- a/src/noteflow/infrastructure/observability/usage.py
+++ b/src/noteflow/infrastructure/observability/usage.py
@@ -76,6 +76,73 @@ class LoggingUsageEventSink:
))
+def _build_event_attributes(event: UsageEvent) -> dict[str, str | int | float | bool]:
+ """Build OTel attributes dict from usage event, filtering None values.
+
+ Args:
+ event: Usage event with optional fields.
+
+ Returns:
+ Dictionary of primitive-typed attributes for OTel span.
+ """
+ from noteflow.config.constants import ERROR_DETAIL_PROJECT_ID
+
+ # Map event fields to attribute names (None values filtered out)
+ field_mappings: list[tuple[str, str | int | float | bool | None]] = [
+ ("meeting_id", event.meeting_id),
+ ("workspace_id", event.workspace_id),
+ (ERROR_DETAIL_PROJECT_ID, event.project_id),
+ ("provider_name", event.provider_name),
+ ("model_name", event.model_name),
+ ("tokens_input", event.tokens_input),
+ ("tokens_output", event.tokens_output),
+ ("latency_ms", event.latency_ms),
+ ("error_code", event.error_code),
+ ]
+
+ # Build attributes from non-None field values
+ attributes: dict[str, str | int | float | bool] = {
+ key: value for key, value in field_mappings if value is not None
+ }
+
+ # Always include success
+ attributes["success"] = event.success
+
+ # Add custom attributes (only primitive types)
+ primitive_custom = {
+ key: value
+ for key, value in event.attributes.items()
+ if isinstance(value, (str, int, float, bool))
+ }
+ attributes.update(primitive_custom)
+
+ return attributes
+
+
+def _set_span_filter_attributes(
+ span: object, event: UsageEvent
+) -> None:
+ """Set key attributes on span for filtering in observability backends.
+
+ Args:
+ span: OTel span to set attributes on.
+ event: Usage event with metric values.
+ """
+ # Map noteflow-prefixed attributes for span-level filtering
+ span_mappings: list[tuple[str, str | int | float | None]] = [
+ ("noteflow.provider", event.provider_name),
+ ("noteflow.model", event.model_name),
+ ("noteflow.tokens_input", event.tokens_input),
+ ("noteflow.tokens_output", event.tokens_output),
+ ("noteflow.latency_ms", event.latency_ms),
+ ]
+
+ # Set non-None attributes on span
+ for attr_name, value in span_mappings:
+ if value is not None:
+ span.set_attribute(attr_name, value) # type: ignore[union-attr]
+
+
class OtelUsageEventSink:
"""Usage event sink that records to OpenTelemetry spans.
@@ -92,53 +159,15 @@ class OtelUsageEventSink:
span = trace.get_current_span()
if span is None or not span.is_recording():
- # No active span, log instead
logger.debug("No active span for usage event: %s", event.event_type)
return
- # Build attributes dict, filtering None values
- attributes: dict[str, str | int | float | bool] = {}
- if event.meeting_id:
- attributes["meeting_id"] = event.meeting_id
- if event.workspace_id:
- attributes["workspace_id"] = event.workspace_id
- from noteflow.config.constants import ERROR_DETAIL_PROJECT_ID
-
- if event.project_id:
- attributes[ERROR_DETAIL_PROJECT_ID] = event.project_id
- if event.provider_name:
- attributes["provider_name"] = event.provider_name
- if event.model_name:
- attributes["model_name"] = event.model_name
- if event.tokens_input is not None:
- attributes["tokens_input"] = event.tokens_input
- if event.tokens_output is not None:
- attributes["tokens_output"] = event.tokens_output
- if event.latency_ms is not None:
- attributes["latency_ms"] = event.latency_ms
- attributes["success"] = event.success
- if event.error_code:
- attributes["error_code"] = event.error_code
-
- # Add custom attributes (only primitive types)
- for key, value in event.attributes.items():
- if isinstance(value, (str, int, float, bool)):
- attributes[key] = value
-
- # Add as span event
+ # Build and attach event attributes
+ attributes = _build_event_attributes(event)
span.add_event(event.event_type, attributes=attributes)
- # Also set key attributes on the span itself for filtering
- if event.provider_name:
- span.set_attribute("noteflow.provider", event.provider_name)
- if event.model_name:
- span.set_attribute("noteflow.model", event.model_name)
- if event.tokens_input is not None:
- span.set_attribute("noteflow.tokens_input", event.tokens_input)
- if event.tokens_output is not None:
- span.set_attribute("noteflow.tokens_output", event.tokens_output)
- if event.latency_ms is not None:
- span.set_attribute("noteflow.latency_ms", event.latency_ms)
+ # Set key attributes on span for filtering
+ _set_span_filter_attributes(span, event)
def record_simple(
self, event_type: str, *, meeting_id: str | None = None,
diff --git a/tests/application/test_recovery_service.py b/tests/application/test_recovery_service.py
index f49dccc..63eeb3b 100644
--- a/tests/application/test_recovery_service.py
+++ b/tests/application/test_recovery_service.py
@@ -153,8 +153,8 @@ class TestRecoveryServiceAudioValidation:
Note: Uses the global `meetings_dir` fixture from tests/conftest.py.
"""
- def test_audio_validation_skipped_without_meetings_dir(self, mock_uow: MagicMock) -> None:
- """Test audio validation skipped when no meetings_dir configured."""
+ def test_audio_validation_skipped_when_meetings_dir_is_none(self, mock_uow: MagicMock) -> None:
+ """Test audio validation skipped when meetings_dir is None."""
meeting = Meeting.create(title="Test Meeting")
meeting.start_recording()
diff --git a/tests/domain/auth/test_oidc.py b/tests/domain/auth/test_oidc.py
index 25bc025..39c0e24 100644
--- a/tests/domain/auth/test_oidc.py
+++ b/tests/domain/auth/test_oidc.py
@@ -37,9 +37,9 @@ class TestClaimMapping:
groups_claim="roles",
)
- assert mapping.subject_claim == "user_id"
- assert mapping.email_claim == "mail"
- assert mapping.groups_claim == "roles"
+ assert mapping.subject_claim == "user_id", f"expected 'user_id', got {mapping.subject_claim!r}"
+ assert mapping.email_claim == "mail", f"expected 'mail', got {mapping.email_claim!r}"
+ assert mapping.groups_claim == "roles", f"expected 'roles', got {mapping.groups_claim!r}"
def test_claim_mapping_to_dict_roundtrip(self) -> None:
"""Verify serialization and deserialization."""
@@ -52,18 +52,24 @@ class TestClaimMapping:
data = original.to_dict()
restored = ClaimMapping.from_dict(data)
- assert restored.subject_claim == original.subject_claim
- assert restored.groups_claim == original.groups_claim
- assert restored.first_name_claim == original.first_name_claim
+ assert restored.subject_claim == original.subject_claim, (
+ f"subject_claim should survive roundtrip: expected {original.subject_claim!r}, got {restored.subject_claim!r}"
+ )
+ assert restored.groups_claim == original.groups_claim, (
+ f"groups_claim should survive roundtrip: expected {original.groups_claim!r}, got {restored.groups_claim!r}"
+ )
+ assert restored.first_name_claim == original.first_name_claim, (
+ f"first_name_claim should survive roundtrip: expected {original.first_name_claim!r}, got {restored.first_name_claim!r}"
+ )
def test_from_dict_with_missing_keys_uses_defaults(self) -> None:
"""Verify missing keys in from_dict use default values."""
data: dict[str, str | None] = {"groups_claim": "roles"}
mapping = ClaimMapping.from_dict(data)
- assert mapping.groups_claim == "roles"
- assert mapping.subject_claim == "sub"
- assert mapping.email_claim == "email"
+ assert mapping.groups_claim == "roles", f"groups_claim should be 'roles', got {mapping.groups_claim!r}"
+ assert mapping.subject_claim == "sub", f"subject_claim should default to 'sub', got {mapping.subject_claim!r}"
+ assert mapping.email_claim == "email", f"email_claim should default to 'email', got {mapping.email_claim!r}"
class TestOidcDiscoveryConfig:
@@ -98,12 +104,12 @@ class TestOidcDiscoveryConfig:
def test_supports_pkce(self, discovery_data: dict[str, object]) -> None:
"""Verify PKCE support detection."""
config = OidcDiscoveryConfig.from_dict(discovery_data)
- assert config.supports_pkce() is True
+ assert config.supports_pkce() is True, "should support PKCE when S256 is in code_challenge_methods_supported"
# Without S256
discovery_data["code_challenge_methods_supported"] = ["plain"]
config_no_pkce = OidcDiscoveryConfig.from_dict(discovery_data)
- assert config_no_pkce.supports_pkce() is False
+ assert config_no_pkce.supports_pkce() is False, "should not support PKCE when S256 is not available"
def test_discovery_config_to_dict_roundtrip(self, discovery_data: dict[str, object]) -> None:
"""Verify serialization roundtrip."""
@@ -111,9 +117,15 @@ class TestOidcDiscoveryConfig:
data = original.to_dict()
restored = OidcDiscoveryConfig.from_dict(data)
- assert restored.issuer == original.issuer
- assert restored.authorization_endpoint == original.authorization_endpoint
- assert restored.scopes_supported == original.scopes_supported
+ assert restored.issuer == original.issuer, (
+ f"issuer should survive roundtrip: expected {original.issuer!r}, got {restored.issuer!r}"
+ )
+ assert restored.authorization_endpoint == original.authorization_endpoint, (
+ f"authorization_endpoint should survive roundtrip: expected {original.authorization_endpoint!r}, got {restored.authorization_endpoint!r}"
+ )
+ assert restored.scopes_supported == original.scopes_supported, (
+ f"scopes_supported should survive roundtrip: expected {original.scopes_supported!r}, got {restored.scopes_supported!r}"
+ )
class TestOidcProviderConfig:
@@ -151,7 +163,9 @@ class TestOidcProviderConfig:
preset=OidcProviderPreset.AUTHENTIK,
)
- assert provider.preset == OidcProviderPreset.AUTHENTIK
+ assert provider.preset == OidcProviderPreset.AUTHENTIK, (
+ f"preset should be AUTHENTIK, got {provider.preset!r}"
+ )
def test_issuer_url_trailing_slash_stripped(self, workspace_id: UUID) -> None:
"""Verify trailing slash is removed from issuer URL."""
@@ -162,7 +176,9 @@ class TestOidcProviderConfig:
client_id="test",
)
- assert provider.issuer_url == "https://auth.example.com"
+ assert provider.issuer_url == "https://auth.example.com", (
+ f"trailing slash should be stripped: got {provider.issuer_url!r}"
+ )
def test_discovery_url_property(self, workspace_id: UUID) -> None:
"""Verify discovery URL is correctly formed."""
@@ -173,7 +189,9 @@ class TestOidcProviderConfig:
client_id="test",
)
- assert provider.discovery_url == "https://auth.example.com/.well-known/openid-configuration"
+ assert provider.discovery_url == "https://auth.example.com/.well-known/openid-configuration", (
+ f"discovery_url should be well-known path: got {provider.discovery_url!r}"
+ )
def test_enable_disable(self, workspace_id: UUID) -> None:
"""Verify enable/disable methods."""
@@ -186,11 +204,13 @@ class TestOidcProviderConfig:
original_updated = provider.updated_at
provider.disable()
- assert provider.enabled is False
- assert provider.updated_at >= original_updated
+ assert provider.enabled is False, "provider should be disabled after disable()"
+ assert provider.updated_at >= original_updated, (
+ f"updated_at should be >= original: {provider.updated_at} vs {original_updated}"
+ )
provider.enable()
- assert provider.enabled is True
+ assert provider.enabled is True, "provider should be enabled after enable()"
def test_update_discovery(self, workspace_id: UUID) -> None:
"""Verify discovery update."""
@@ -247,9 +267,9 @@ class TestOidcProviderConfig:
client_id="test",
)
- assert "openid" in provider.scopes
- assert "profile" in provider.scopes
- assert "email" in provider.scopes
+ assert "openid" in provider.scopes, f"'openid' should be in default scopes: {provider.scopes!r}"
+ assert "profile" in provider.scopes, f"'profile' should be in default scopes: {provider.scopes!r}"
+ assert "email" in provider.scopes, f"'email' should be in default scopes: {provider.scopes!r}"
class TestOidcProviderPreset:
diff --git a/tests/domain/test_annotation.py b/tests/domain/test_annotation.py
index 4b18216..4b43933 100644
--- a/tests/domain/test_annotation.py
+++ b/tests/domain/test_annotation.py
@@ -9,6 +9,9 @@ import pytest
from noteflow.domain.entities.annotation import Annotation
from noteflow.domain.value_objects import AnnotationId, AnnotationType, MeetingId
+# Test constants
+TWO_HOURS_SECONDS = 7200.0
+
class TestAnnotation:
"""Tests for Annotation entity."""
@@ -69,7 +72,7 @@ class TestAnnotationEdgeCases:
start_time=5.0,
end_time=5.0,
)
- assert annotation.duration == 0.0
+ assert annotation.duration == 0.0, "duration should be 0.0 when start_time equals end_time"
def test_annotation_empty_text(self) -> None:
"""Test annotation with empty text."""
@@ -81,7 +84,7 @@ class TestAnnotationEdgeCases:
start_time=0.0,
end_time=1.0,
)
- assert annotation.text == ""
+ assert annotation.text == "", "empty text should be preserved as empty string"
def test_annotation_unicode_text(self) -> None:
"""Test annotation with unicode text."""
@@ -93,7 +96,7 @@ class TestAnnotationEdgeCases:
start_time=0.0,
end_time=1.0,
)
- assert "🎯" in annotation.text
+ assert "🎯" in annotation.text, f"unicode emoji should be preserved in text, got: {annotation.text}"
def test_annotation_many_segment_ids(self) -> None:
"""Test annotation with many segment IDs."""
@@ -118,9 +121,9 @@ class TestAnnotationEdgeCases:
annotation_type=AnnotationType.NOTE,
text="Long meeting note",
start_time=0.0,
- end_time=7200.0, # 2 hours
+ end_time=TWO_HOURS_SECONDS,
)
- assert annotation.duration == 7200.0
+ assert annotation.duration == TWO_HOURS_SECONDS, f"duration should be {TWO_HOURS_SECONDS} seconds (2 hours), got {annotation.duration}"
@pytest.mark.parametrize("annotation_type", list(AnnotationType))
def test_annotation_all_types(self, annotation_type: AnnotationType) -> None:
@@ -134,7 +137,7 @@ class TestAnnotationEdgeCases:
start_time=0.0,
end_time=1.0,
)
- assert annotation.annotation_type == annotation_type
+ assert annotation.annotation_type == annotation_type, f"annotation_type should match {annotation_type}, got {annotation.annotation_type}"
def test_annotation_empty_segment_ids_list(self) -> None:
"""Test annotation with explicitly empty segment_ids."""
@@ -147,4 +150,4 @@ class TestAnnotationEdgeCases:
end_time=1.0,
segment_ids=[],
)
- assert annotation.has_segments() is False
+ assert annotation.has_segments() is False, "empty segment_ids list should result in has_segments() returning False"
diff --git a/tests/domain/test_meeting.py b/tests/domain/test_meeting.py
index 2997e16..d462633 100644
--- a/tests/domain/test_meeting.py
+++ b/tests/domain/test_meeting.py
@@ -13,6 +13,9 @@ from noteflow.domain.entities.summary import Summary
from noteflow.domain.utils.time import utc_now
from noteflow.domain.value_objects import MeetingState
+# Test constants
+HALF_HOUR_SECONDS = 1800.0
+
class TestMeetingCreation:
"""Tests for Meeting creation methods."""
@@ -30,23 +33,24 @@ class TestMeetingCreation:
def test_create_default_attributes(self, attr: str, expected: object) -> None:
"""Test factory method sets default attribute values."""
meeting = Meeting.create()
- assert getattr(meeting, attr) == expected
+ actual = getattr(meeting, attr)
+ assert actual == expected, f"expected {attr}={expected!r}, got {actual!r}"
def test_create_generates_default_title(self) -> None:
"""Test factory method generates default title prefix."""
meeting = Meeting.create()
- assert meeting.title.startswith("Meeting ")
+ assert meeting.title.startswith("Meeting "), f"expected title starting with 'Meeting ', got {meeting.title!r}"
def test_create_with_custom_title(self) -> None:
"""Test factory method accepts custom title."""
meeting = Meeting.create(title="Team Standup")
- assert meeting.title == "Team Standup"
+ assert meeting.title == "Team Standup", f"expected title 'Team Standup', got {meeting.title!r}"
def test_create_with_metadata(self) -> None:
"""Test factory method accepts metadata."""
metadata = {"project": "NoteFlow", "team": "Engineering"}
meeting = Meeting.create(title="Sprint Planning", metadata=metadata)
- assert meeting.metadata == metadata
+ assert meeting.metadata == metadata, f"expected metadata {metadata!r}, got {meeting.metadata!r}"
def test_from_uuid_str(self) -> None:
"""Test creation from existing UUID string."""
@@ -85,7 +89,7 @@ class TestMeetingStateTransitions:
meeting = Meeting.create()
meeting.start_recording()
meeting.begin_stopping()
- assert meeting.state == MeetingState.STOPPING
+ assert meeting.state == MeetingState.STOPPING, f"expected state STOPPING, got {meeting.state}"
def test_begin_stopping_invalid_state_raises(self) -> None:
"""Test begin_stopping from invalid state raises."""
@@ -125,7 +129,7 @@ class TestMeetingStateTransitions:
meeting.begin_stopping()
meeting.stop_recording()
meeting.complete()
- assert meeting.state == MeetingState.COMPLETED
+ assert meeting.state == MeetingState.COMPLETED, f"expected state COMPLETED, got {meeting.state}"
def test_complete_invalid_state_raises(self) -> None:
"""Test completing from invalid state raises."""
@@ -137,7 +141,7 @@ class TestMeetingStateTransitions:
"""Test marking meeting as error state."""
meeting = Meeting.create()
meeting.mark_error()
- assert meeting.state == MeetingState.ERROR
+ assert meeting.state == MeetingState.ERROR, f"expected state ERROR, got {meeting.state}"
def test_stopping_to_recording_invalid(self) -> None:
"""Test cannot transition from STOPPING back to RECORDING."""
@@ -162,33 +166,33 @@ class TestMeetingSegments:
def test_next_segment_id_empty(self) -> None:
"""Test next segment ID when no segments exist."""
meeting = Meeting.create()
- assert meeting.next_segment_id == 0
+ assert meeting.next_segment_id == 0, f"expected next_segment_id 0 for empty meeting, got {meeting.next_segment_id}"
def test_next_segment_id_with_segments(self) -> None:
"""Test next segment ID increments correctly."""
meeting = Meeting.create()
meeting.add_segment(Segment(segment_id=0, text="First", start_time=0.0, end_time=1.0))
meeting.add_segment(Segment(segment_id=1, text="Second", start_time=1.0, end_time=2.0))
- assert meeting.next_segment_id == 2
+ assert meeting.next_segment_id == 2, f"expected next_segment_id 2, got {meeting.next_segment_id}"
def test_next_segment_id_non_contiguous(self) -> None:
"""Test next segment ID uses max + 1 for non-contiguous IDs."""
meeting = Meeting.create()
meeting.add_segment(Segment(segment_id=0, text="First", start_time=0.0, end_time=1.0))
meeting.add_segment(Segment(segment_id=5, text="Sixth", start_time=1.0, end_time=2.0))
- assert meeting.next_segment_id == 6
+ assert meeting.next_segment_id == 6, f"expected next_segment_id 6 for max id 5, got {meeting.next_segment_id}"
def test_full_transcript(self) -> None:
"""Test concatenating all segment text."""
meeting = Meeting.create()
meeting.add_segment(Segment(segment_id=0, text="Hello", start_time=0.0, end_time=1.0))
meeting.add_segment(Segment(segment_id=1, text="world", start_time=1.0, end_time=2.0))
- assert meeting.full_transcript == "Hello world"
+ assert meeting.full_transcript == "Hello world", f"expected 'Hello world', got {meeting.full_transcript!r}"
def test_full_transcript_empty(self) -> None:
"""Test full_transcript is empty when there are no segments."""
meeting = Meeting.create()
- assert meeting.full_transcript == ""
+ assert meeting.full_transcript == "", f"expected empty transcript, got {meeting.full_transcript!r}"
class TestMeetingProperties:
@@ -197,38 +201,38 @@ class TestMeetingProperties:
def test_duration_seconds_not_started(self) -> None:
"""Test duration is 0 when not started."""
meeting = Meeting.create()
- assert meeting.duration_seconds == 0.0
+ assert meeting.duration_seconds == 0.0, f"expected 0.0 duration for unstarted meeting, got {meeting.duration_seconds}"
def test_duration_seconds_with_times(self) -> None:
"""Test duration calculation with start and end times."""
meeting = Meeting.create()
meeting.started_at = datetime(2024, 1, 1, 10, 0, 0)
meeting.ended_at = datetime(2024, 1, 1, 10, 30, 0)
- assert meeting.duration_seconds == 1800.0
+ assert meeting.duration_seconds == HALF_HOUR_SECONDS, f"expected {HALF_HOUR_SECONDS} seconds (30 min), got {meeting.duration_seconds}"
def test_duration_seconds_in_progress(self) -> None:
"""Test duration is > 0 when started but not ended."""
meeting = Meeting.create()
meeting.started_at = utc_now() - timedelta(seconds=5)
- assert meeting.duration_seconds >= 5.0
+ assert meeting.duration_seconds >= 5.0, f"expected duration >= 5.0 seconds, got {meeting.duration_seconds}"
def test_is_in_active_state_created(self) -> None:
"""Test is_in_active_state returns True for CREATED state."""
meeting = Meeting.create()
- assert meeting.is_in_active_state() is True
+ assert meeting.is_in_active_state() is True, "CREATED state should be considered active"
def test_is_in_active_state_recording(self) -> None:
"""Test is_in_active_state returns True for RECORDING state."""
meeting = Meeting.create()
meeting.start_recording()
- assert meeting.is_in_active_state() is True
+ assert meeting.is_in_active_state() is True, "RECORDING state should be considered active"
def test_is_in_active_state_stopping(self) -> None:
"""Test is_in_active_state returns False for STOPPING state."""
meeting = Meeting.create()
meeting.start_recording()
meeting.begin_stopping()
- assert meeting.is_in_active_state() is False
+ assert meeting.is_in_active_state() is False, "STOPPING state should not be considered active"
def test_is_in_active_state_stopped(self) -> None:
"""Test is_in_active_state returns False for STOPPED state."""
@@ -236,19 +240,19 @@ class TestMeetingProperties:
meeting.start_recording()
meeting.begin_stopping()
meeting.stop_recording()
- assert meeting.is_in_active_state() is False
+ assert meeting.is_in_active_state() is False, "STOPPED state should not be considered active"
def test_has_summary_false(self) -> None:
"""Test has_summary returns False when no summary."""
meeting = Meeting.create()
- assert meeting.has_summary() is False
+ assert meeting.has_summary() is False, "meeting without summary should return has_summary=False"
def test_has_summary_true(self) -> None:
"""Test has_summary returns True when summary set."""
meeting = Meeting.create()
summary = Summary(meeting_id=meeting.id)
meeting.set_summary(summary)
- assert meeting.has_summary() is True
+ assert meeting.has_summary() is True, "meeting with summary should return has_summary=True"
class TestMeetingEdgeCases:
@@ -260,7 +264,7 @@ class TestMeetingEdgeCases:
meeting.add_segment(Segment(segment_id=0, text="Hello", start_time=0.0, end_time=1.0))
meeting.add_segment(Segment(segment_id=1, text="", start_time=1.0, end_time=1.5))
meeting.add_segment(Segment(segment_id=2, text="world", start_time=1.5, end_time=2.0))
- assert meeting.full_transcript == "Hello world"
+ assert meeting.full_transcript == "Hello world", f"expected 'Hello world', got {meeting.full_transcript!r}"
def test_full_transcript_segments_whitespace_only(self) -> None:
"""Test full_transcript handles segments with whitespace-only text."""
@@ -278,7 +282,8 @@ class TestMeetingEdgeCases:
meeting.add_segment(Segment(segment_id=0, text="你好", start_time=0.0, end_time=1.0))
meeting.add_segment(Segment(segment_id=1, text="🚀", start_time=1.0, end_time=2.0))
meeting.add_segment(Segment(segment_id=2, text="café", start_time=2.0, end_time=3.0))
- assert meeting.full_transcript == "你好 🚀 café"
+ expected = "你好 🚀 café"
+ assert meeting.full_transcript == expected, f"expected {expected!r}, got {meeting.full_transcript!r}"
def test_start_recording_sets_recent_timestamp(self) -> None:
"""Test started_at is set to a recent timestamp."""
@@ -350,13 +355,13 @@ class TestMeetingEdgeCases:
"""Test empty title triggers default title generation."""
meeting = Meeting.create(title="")
# Factory generates default title when empty string passed
- assert meeting.title.startswith("Meeting ")
+ assert meeting.title.startswith("Meeting "), f"expected title starting with 'Meeting ', got {meeting.title!r}"
def test_create_with_very_long_title(self) -> None:
"""Test meeting accepts very long titles."""
long_title = "A" * 1000
meeting = Meeting.create(title=long_title)
- assert len(meeting.title) == 1000
+ assert len(meeting.title) == 1000, f"expected title length 1000, got {len(meeting.title)}"
def test_immediate_stop_after_start_zero_duration(self) -> None:
"""Test immediate stop after start yields near-zero duration (ED-01).
@@ -475,7 +480,7 @@ class TestMeetingSegmentMutability:
meeting.stop_recording()
meeting.complete()
- assert meeting.state == MeetingState.COMPLETED
+ assert meeting.state == MeetingState.COMPLETED, f"expected state COMPLETED, got {meeting.state}"
# Adding segment to completed meeting is allowed
segment = Segment(segment_id=0, text="Late transcription", start_time=0.0, end_time=1.0)
@@ -493,7 +498,7 @@ class TestMeetingSegmentMutability:
meeting = Meeting.create()
meeting.mark_error()
- assert meeting.state == MeetingState.ERROR
+ assert meeting.state == MeetingState.ERROR, f"expected state ERROR, got {meeting.state}"
segment = Segment(segment_id=0, text="Recovered text", start_time=0.0, end_time=1.0)
meeting.add_segment(segment)
@@ -515,4 +520,4 @@ class TestMeetingSegmentMutability:
segment_id=0, text="Modified", start_time=0.0, end_time=1.0
)
- assert meeting.segments[0].text == "Modified"
+ assert meeting.segments[0].text == "Modified", f"expected segment text 'Modified', got {meeting.segments[0].text!r}"
diff --git a/tests/domain/test_named_entity.py b/tests/domain/test_named_entity.py
index 6077265..4317ca5 100644
--- a/tests/domain/test_named_entity.py
+++ b/tests/domain/test_named_entity.py
@@ -30,13 +30,14 @@ class TestEntityCategory:
self, value: str, expected: EntityCategory
) -> None:
"""Convert lowercase string to EntityCategory."""
- assert EntityCategory.from_string(value) == expected
+ result = EntityCategory.from_string(value)
+ assert result == expected, f"from_string('{value}') should return {expected}, got {result}"
@pytest.mark.parametrize("value", ["PERSON", "Person", "COMPANY"])
def test_from_string_case_insensitive(self, value: str) -> None:
"""Convert mixed case string to EntityCategory."""
result = EntityCategory.from_string(value)
- assert result in EntityCategory
+ assert result in EntityCategory, f"from_string('{value}') should return valid EntityCategory, got {result}"
def test_from_string_invalid_raises(self) -> None:
"""Invalid category string raises ValueError."""
@@ -65,7 +66,7 @@ class TestNamedEntityValidation:
category=EntityCategory.PERSON,
confidence=confidence,
)
- assert entity.confidence == confidence
+ assert entity.confidence == confidence, f"expected confidence {confidence}, got {entity.confidence}"
def test_auto_computes_normalized_text(self) -> None:
"""Normalized text is auto-computed from text when not provided."""
@@ -74,7 +75,7 @@ class TestNamedEntityValidation:
category=EntityCategory.PERSON,
confidence=0.9,
)
- assert entity.normalized_text == "john smith"
+ assert entity.normalized_text == "john smith", f"expected normalized_text 'john smith', got '{entity.normalized_text}'"
def test_preserves_explicit_normalized_text(self) -> None:
"""Explicit normalized_text is preserved."""
@@ -84,7 +85,7 @@ class TestNamedEntityValidation:
category=EntityCategory.PERSON,
confidence=0.9,
)
- assert entity.normalized_text == "custom_normalization"
+ assert entity.normalized_text == "custom_normalization", f"expected explicit normalized_text 'custom_normalization', got '{entity.normalized_text}'"
class TestNamedEntityCreate:
@@ -116,8 +117,8 @@ class TestNamedEntityCreate:
segment_ids=[0],
confidence=0.9,
)
- assert entity.text == "John Smith"
- assert entity.normalized_text == "john smith"
+ assert entity.text == "John Smith", f"expected stripped text 'John Smith', got '{entity.text}'"
+ assert entity.normalized_text == "john smith", f"expected normalized_text 'john smith', got '{entity.normalized_text}'"
def test_create_deduplicates_segment_ids(self) -> None:
"""Create deduplicates and sorts segment IDs."""
@@ -127,7 +128,7 @@ class TestNamedEntityCreate:
segment_ids=[3, 1, 1, 3, 2],
confidence=0.8,
)
- assert entity.segment_ids == [1, 2, 3]
+ assert entity.segment_ids == [1, 2, 3], f"expected deduplicated/sorted segment_ids [1, 2, 3], got {entity.segment_ids}"
def test_create_empty_text_raises(self) -> None:
"""Create with empty text raises ValueError."""
@@ -171,7 +172,7 @@ class TestNamedEntityOccurrenceCount:
segment_ids=[0, 1, 2],
confidence=0.8,
)
- assert entity.occurrence_count == 3
+ assert entity.occurrence_count == 3, f"expected occurrence_count 3 for 3 segments, got {entity.occurrence_count}"
def test_occurrence_count_empty_segments(self) -> None:
"""Occurrence count returns 0 for empty segment_ids."""
@@ -181,7 +182,7 @@ class TestNamedEntityOccurrenceCount:
segment_ids=[],
confidence=0.8,
)
- assert entity.occurrence_count == 0
+ assert entity.occurrence_count == 0, f"expected occurrence_count 0 for empty segments, got {entity.occurrence_count}"
def test_occurrence_count_single_segment(self) -> None:
"""Occurrence count returns 1 for single segment."""
@@ -191,7 +192,7 @@ class TestNamedEntityOccurrenceCount:
segment_ids=[5],
confidence=0.8,
)
- assert entity.occurrence_count == 1
+ assert entity.occurrence_count == 1, f"expected occurrence_count 1 for single segment, got {entity.occurrence_count}"
class TestNamedEntityMergeSegments:
@@ -206,7 +207,7 @@ class TestNamedEntityMergeSegments:
confidence=0.9,
)
entity.merge_segments([3, 4])
- assert entity.segment_ids == [0, 1, 3, 4]
+ assert entity.segment_ids == [0, 1, 3, 4], f"expected merged segment_ids [0, 1, 3, 4], got {entity.segment_ids}"
def test_merge_segments_deduplicates(self) -> None:
"""Merge segments deduplicates overlapping IDs."""
@@ -217,7 +218,7 @@ class TestNamedEntityMergeSegments:
confidence=0.9,
)
entity.merge_segments([1, 2, 3])
- assert entity.segment_ids == [0, 1, 2, 3]
+ assert entity.segment_ids == [0, 1, 2, 3], f"expected deduplicated segment_ids [0, 1, 2, 3], got {entity.segment_ids}"
def test_merge_segments_sorts(self) -> None:
"""Merge segments keeps result sorted."""
@@ -228,7 +229,7 @@ class TestNamedEntityMergeSegments:
confidence=0.9,
)
entity.merge_segments([1, 3])
- assert entity.segment_ids == [1, 3, 5, 10]
+ assert entity.segment_ids == [1, 3, 5, 10], f"expected sorted segment_ids [1, 3, 5, 10], got {entity.segment_ids}"
def test_merge_empty_segments(self) -> None:
"""Merge with empty list preserves original segments."""
@@ -239,7 +240,7 @@ class TestNamedEntityMergeSegments:
confidence=0.9,
)
entity.merge_segments([])
- assert entity.segment_ids == [0, 1]
+ assert entity.segment_ids == [0, 1], f"expected unchanged segment_ids [0, 1] after merging empty list, got {entity.segment_ids}"
class TestNamedEntityDefaults:
diff --git a/tests/domain/test_project.py b/tests/domain/test_project.py
index f077fb0..92e445a 100644
--- a/tests/domain/test_project.py
+++ b/tests/domain/test_project.py
@@ -142,7 +142,8 @@ class TestProjectCreation:
def test_default_attribute_values(self, attr: str, expected: object) -> None:
"""Test Project has expected default values."""
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
- assert getattr(project, attr) == expected
+ actual = getattr(project, attr)
+ assert actual == expected, f"expected {attr}={expected!r}, got {actual!r}"
def test_default_settings_factory(self) -> None:
"""Test Project creates empty ProjectSettings by default."""
@@ -191,7 +192,7 @@ class TestProjectSlugValidation:
name="Test",
slug=valid_slug,
)
- assert project.slug == valid_slug
+ assert project.slug == valid_slug, f"expected slug={valid_slug!r}, got {project.slug!r}"
@pytest.mark.parametrize(
"invalid_slug",
@@ -223,7 +224,7 @@ class TestProjectSlugValidation:
name="Test",
slug=None,
)
- assert project.slug is None
+ assert project.slug is None, f"expected slug=None, got {project.slug!r}"
class TestProjectArchiveRestore:
@@ -232,7 +233,7 @@ class TestProjectArchiveRestore:
def test_archive_sets_timestamp(self) -> None:
"""Test archive() sets archived_at timestamp."""
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
- assert project.archived_at is None
+ assert project.archived_at is None, "archived_at should be None before archive"
before = utc_now()
project.archive()
@@ -248,7 +249,7 @@ class TestProjectArchiveRestore:
project.archive()
- assert project.updated_at >= original_updated
+ assert project.updated_at >= original_updated, "updated_at should be >= original after archive"
def test_archive_default_project_raises(self) -> None:
"""Test archiving default project raises CannotArchiveDefaultProjectError."""
@@ -265,7 +266,7 @@ class TestProjectArchiveRestore:
"""Test restore() clears archived_at timestamp."""
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
project.archive()
- assert project.archived_at is not None
+ assert project.archived_at is not None, "archived_at should be set after archive"
project.restore()
@@ -279,7 +280,7 @@ class TestProjectArchiveRestore:
project.restore()
- assert project.updated_at >= archived_updated
+ assert project.updated_at >= archived_updated, "updated_at should be >= archived_updated after restore"
class TestProjectProperties:
@@ -288,24 +289,24 @@ class TestProjectProperties:
def test_is_archived_false_by_default(self) -> None:
"""Test is_archived returns False for new project."""
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
- assert project.is_archived is False
+ assert project.is_archived is False, "is_archived should be False for new project"
def test_is_archived_true_when_archived(self) -> None:
"""Test is_archived returns True after archive()."""
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
project.archive()
- assert project.is_archived is True
+ assert project.is_archived is True, "is_archived should be True after archive"
def test_is_active_true_by_default(self) -> None:
"""Test is_active returns True for new project."""
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
- assert project.is_active is True
+ assert project.is_active is True, "is_active should be True for new project"
def test_is_active_false_when_archived(self) -> None:
"""Test is_active returns False after archive()."""
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
project.archive()
- assert project.is_active is False
+ assert project.is_active is False, "is_active should be False after archive"
def test_is_archived_and_is_active_are_inverse(self) -> None:
"""Test is_archived and is_active are always inverse."""
@@ -326,7 +327,7 @@ class TestProjectMutations:
"""Test update_name() changes name field."""
project = Project(id=uuid4(), workspace_id=uuid4(), name="Original")
project.update_name("Updated Name")
- assert project.name == "Updated Name"
+ assert project.name == "Updated Name", f"expected name='Updated Name', got {project.name!r}"
def test_update_name_updates_timestamp(self) -> None:
"""Test update_name() updates updated_at timestamp."""
@@ -335,13 +336,13 @@ class TestProjectMutations:
project.update_name("Updated")
- assert project.updated_at >= original_updated
+ assert project.updated_at >= original_updated, "updated_at should be >= original after update_name"
def test_update_description(self) -> None:
"""Test update_description() changes description field."""
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
project.update_description("New description")
- assert project.description == "New description"
+ assert project.description == "New description", f"expected description='New description', got {project.description!r}"
def test_update_description_to_none(self) -> None:
"""Test update_description() can set description to None."""
@@ -352,7 +353,7 @@ class TestProjectMutations:
description="Original",
)
project.update_description(None)
- assert project.description is None
+ assert project.description is None, f"expected description=None, got {project.description!r}"
def test_update_description_updates_timestamp(self) -> None:
"""Test update_description() updates updated_at timestamp."""
@@ -361,7 +362,7 @@ class TestProjectMutations:
project.update_description("Description")
- assert project.updated_at >= original_updated
+ assert project.updated_at >= original_updated, "updated_at should be >= original after update_description"
def test_update_settings(self) -> None:
"""Test update_settings() replaces settings object."""
@@ -380,7 +381,7 @@ class TestProjectMutations:
project.update_settings(ProjectSettings())
- assert project.updated_at >= original_updated
+ assert project.updated_at >= original_updated, "updated_at should be >= original after update_settings"
def test_update_slug_with_valid_pattern(self) -> None:
"""Test update_slug() accepts valid slug patterns."""
@@ -418,14 +419,14 @@ class TestProjectMutations:
"""Test set_metadata() adds key-value pair."""
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
project.set_metadata("key", "value")
- assert project.metadata["key"] == "value"
+ assert project.metadata["key"] == "value", f"expected metadata['key']='value', got {project.metadata.get('key')!r}"
def test_set_metadata_overwrites_existing(self) -> None:
"""Test set_metadata() overwrites existing key."""
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
project.set_metadata("key", "original")
project.set_metadata("key", "updated")
- assert project.metadata["key"] == "updated"
+ assert project.metadata["key"] == "updated", f"expected metadata['key']='updated', got {project.metadata.get('key')!r}"
def test_set_metadata_updates_timestamp(self) -> None:
"""Test set_metadata() updates updated_at timestamp."""
@@ -434,7 +435,7 @@ class TestProjectMutations:
project.set_metadata("key", "value")
- assert project.updated_at >= original_updated
+ assert project.updated_at >= original_updated, "updated_at should be >= original after set_metadata"
class TestProjectEdgeCases:
@@ -450,17 +451,17 @@ class TestProjectEdgeCases:
# Small delay to ensure different timestamp
project.archive()
- assert project.archived_at is not None
- assert project.archived_at >= first_archived
+ assert project.archived_at is not None, "archived_at should still be set after second archive"
+ assert project.archived_at >= first_archived, "archived_at should be >= first_archived after second archive"
def test_restore_non_archived_project(self) -> None:
"""Test restoring non-archived project is idempotent."""
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
- assert project.archived_at is None
+ assert project.archived_at is None, "archived_at should be None before restore"
project.restore()
- assert project.archived_at is None
+ assert project.archived_at is None, "archived_at should remain None after restore on non-archived project"
def test_metadata_complex_structures(self) -> None:
"""Test metadata can store complex nested structures."""
@@ -471,13 +472,13 @@ class TestProjectEdgeCases:
"boolean": True,
}
project.set_metadata("complex", complex_value)
- assert project.metadata["complex"] == complex_value
+ assert project.metadata["complex"] == complex_value, "complex nested structure should be stored in metadata"
def test_very_long_name(self) -> None:
"""Test project accepts very long names."""
long_name = "A" * 1000
project = Project(id=uuid4(), workspace_id=uuid4(), name=long_name)
- assert len(project.name) == 1000
+ assert len(project.name) == 1000, f"expected name length 1000, got {len(project.name)}"
def test_unicode_name_and_description(self) -> None:
"""Test project accepts unicode in name and description."""
@@ -487,8 +488,8 @@ class TestProjectEdgeCases:
name="项目名称", # Chinese for "project name"
description="プロジェクトの説明", # Japanese for "project description"
)
- assert project.name == "项目名称"
- assert project.description == "プロジェクトの説明"
+ assert project.name == "项目名称", f"expected Chinese name, got {project.name!r}"
+ assert project.description == "プロジェクトの説明", f"expected Japanese description, got {project.description!r}"
def test_emoji_in_name(self) -> None:
"""Test project accepts emoji in name."""
@@ -497,7 +498,7 @@ class TestProjectEdgeCases:
workspace_id=uuid4(),
name="My Project 🚀",
)
- assert "🚀" in project.name
+ assert "🚀" in project.name, f"expected emoji in name, got {project.name!r}"
def test_settings_with_full_configuration(self) -> None:
"""Test project with fully configured settings."""
@@ -523,10 +524,10 @@ class TestProjectEdgeCases:
name="Fully Configured",
settings=settings,
)
- assert project.settings.export_rules is not None
- assert project.settings.export_rules.default_format == ExportFormat.PDF
- assert project.settings.trigger_rules is not None
- assert project.settings.trigger_rules.auto_start_enabled is True
+ assert project.settings.export_rules is not None, "export_rules should not be None"
+ assert project.settings.export_rules.default_format == ExportFormat.PDF, "default_format should be PDF"
+ assert project.settings.trigger_rules is not None, "trigger_rules should not be None"
+ assert project.settings.trigger_rules.auto_start_enabled is True, "auto_start_enabled should be True"
class TestProjectInvariants:
@@ -545,19 +546,19 @@ class TestProjectInvariants:
project.archive()
# Project state should be unchanged
- assert project.archived_at is None
- assert project.is_active is True
+ assert project.archived_at is None, "archived_at should remain None for default project"
+ assert project.is_active is True, "is_active should remain True for default project"
def test_updated_at_never_before_created_at(self) -> None:
"""Test updated_at is always >= created_at."""
project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
- assert project.updated_at >= project.created_at
+ assert project.updated_at >= project.created_at, "updated_at should be >= created_at on creation"
project.update_name("New Name")
- assert project.updated_at >= project.created_at
+ assert project.updated_at >= project.created_at, "updated_at should be >= created_at after update_name"
project.archive()
- assert project.updated_at >= project.created_at
+ assert project.updated_at >= project.created_at, "updated_at should be >= created_at after archive"
def test_explicit_timestamps_preserved(self) -> None:
"""Test explicit timestamps are preserved on creation."""
@@ -571,8 +572,8 @@ class TestProjectInvariants:
created_at=created,
updated_at=updated,
)
- assert project.created_at == created
- assert project.updated_at == updated
+ assert project.created_at == created, f"expected created_at={created}, got {project.created_at}"
+ assert project.updated_at == updated, f"expected updated_at={updated}, got {project.updated_at}"
def test_id_immutability(self) -> None:
"""Test project id cannot be changed after creation."""
@@ -584,7 +585,7 @@ class TestProjectInvariants:
project.id = uuid4()
# Just verifying the field was changed (dataclass allows this)
- assert project.id != original_id
+ assert project.id != original_id, "id should have been changed (dataclass allows reassignment)"
class TestCannotArchiveDefaultProjectError:
@@ -594,11 +595,11 @@ class TestCannotArchiveDefaultProjectError:
"""Test error message includes the project ID."""
project_id = "test-project-123"
error = CannotArchiveDefaultProjectError(project_id)
- assert project_id in str(error.message)
+ assert project_id in str(error.message), f"expected '{project_id}' in error message, got {error.message!r}"
def test_error_details_contain_project_id(self) -> None:
"""Test error details contain project_id key."""
project_id = "test-project-456"
error = CannotArchiveDefaultProjectError(project_id)
- assert error.details is not None
- assert error.details.get("project_id") == project_id
+ assert error.details is not None, "error.details should not be None"
+ assert error.details.get("project_id") == project_id, f"expected project_id='{project_id}' in details, got {error.details.get('project_id')!r}"
diff --git a/tests/domain/test_segment.py b/tests/domain/test_segment.py
index 862b41f..a5caa0b 100644
--- a/tests/domain/test_segment.py
+++ b/tests/domain/test_segment.py
@@ -6,6 +6,9 @@ import pytest
from noteflow.domain.entities.segment import Segment, WordTiming
+# Test constants
+TEN_HOURS_SECONDS = 36000.0
+
class TestWordTiming:
"""Tests for WordTiming entity."""
@@ -17,7 +20,8 @@ class TestWordTiming:
def test_word_timing_attributes(self, attr: str, expected: object) -> None:
"""Test WordTiming stores attribute values correctly."""
word = WordTiming(word="hello", start_time=0.0, end_time=0.5, probability=0.95)
- assert getattr(word, attr) == expected
+ actual = getattr(word, attr)
+ assert actual == expected, f"WordTiming.{attr} expected {expected!r}, got {actual!r}"
def test_word_timing_invalid_times_raises(self) -> None:
"""Test WordTiming raises on end_time < start_time."""
@@ -34,7 +38,7 @@ class TestWordTiming:
def test_word_timing_valid_probability_bounds(self, prob: float) -> None:
"""Test WordTiming accepts probability at boundaries."""
word = WordTiming(word="test", start_time=0.0, end_time=0.5, probability=prob)
- assert word.probability == prob
+ assert word.probability == prob, f"probability expected {prob}, got {word.probability}"
class TestSegment:
@@ -55,7 +59,8 @@ class TestSegment:
segment = Segment(
segment_id=0, text="Hello world", start_time=0.0, end_time=2.5, language="en"
)
- assert getattr(segment, attr) == expected
+ actual = getattr(segment, attr)
+ assert actual == expected, f"Segment.{attr} expected {expected!r}, got {actual!r}"
def test_segment_invalid_times_raises(self) -> None:
"""Test Segment raises on end_time < start_time."""
@@ -70,12 +75,12 @@ class TestSegment:
def test_segment_duration(self) -> None:
"""Test duration property calculation."""
segment = Segment(segment_id=0, text="test", start_time=1.5, end_time=4.0)
- assert segment.duration == 2.5
+ assert segment.duration == 2.5, f"duration expected 2.5, got {segment.duration}"
def test_segment_word_count_from_text(self) -> None:
"""Test word_count from text when no words list."""
segment = Segment(segment_id=0, text="Hello beautiful world", start_time=0.0, end_time=1.0)
- assert segment.word_count == 3
+ assert segment.word_count == 3, f"word_count expected 3, got {segment.word_count}"
def test_segment_word_count_from_words(self) -> None:
"""Test word_count from words list when provided."""
@@ -90,7 +95,7 @@ class TestSegment:
end_time=0.5,
words=words,
)
- assert segment.word_count == 2
+ assert segment.word_count == 2, f"word_count expected 2, got {segment.word_count}"
@pytest.mark.parametrize(
"embedding,expected",
@@ -111,7 +116,8 @@ class TestSegment:
end_time=1.0,
embedding=embedding,
)
- assert segment.has_embedding() is expected
+ result = segment.has_embedding()
+ assert result is expected, f"has_embedding() expected {expected}, got {result}"
# --- Edge case tests ---
@@ -128,7 +134,9 @@ class TestSegment:
def test_segment_word_count_edge_cases(self, text: str, expected_count: int) -> None:
"""Test word_count correctly handles various text patterns."""
segment = Segment(segment_id=0, text=text, start_time=0.0, end_time=1.0)
- assert segment.word_count == expected_count
+ assert segment.word_count == expected_count, (
+ f"word_count for {text!r} expected {expected_count}, got {segment.word_count}"
+ )
def test_segment_word_count_empty_words_list(self) -> None:
"""Test word_count from text when words list is empty."""
@@ -139,22 +147,24 @@ class TestSegment:
end_time=0.5,
words=[],
)
- assert segment.word_count == 2
+ assert segment.word_count == 2, (
+ f"word_count expected 2 from text fallback, got {segment.word_count}"
+ )
def test_segment_unicode_text_contains_emoji(self) -> None:
"""Test segment preserves unicode emoji in text."""
segment = Segment(segment_id=0, text="你好世界 🚀 café", start_time=0.0, end_time=1.0)
- assert "🚀" in segment.text
+ assert "🚀" in segment.text, f"expected emoji in text, got {segment.text!r}"
def test_segment_zero_duration(self) -> None:
"""Test segment with zero duration is valid."""
segment = Segment(segment_id=0, text="instant", start_time=5.0, end_time=5.0)
- assert segment.duration == 0.0
+ assert segment.duration == 0.0, f"duration expected 0.0, got {segment.duration}"
def test_segment_very_long_duration(self) -> None:
"""Test segment with very long duration."""
- segment = Segment(segment_id=0, text="marathon", start_time=0.0, end_time=36000.0) # 10 hours
- assert segment.duration == 36000.0
+ segment = Segment(segment_id=0, text="marathon", start_time=0.0, end_time=TEN_HOURS_SECONDS)
+ assert segment.duration == TEN_HOURS_SECONDS, f"duration expected {TEN_HOURS_SECONDS}, got {segment.duration}"
class TestWordTimingEdgeCases:
@@ -163,24 +173,26 @@ class TestWordTimingEdgeCases:
def test_word_timing_boundary_probability_zero(self) -> None:
"""Test probability at exact lower boundary."""
word = WordTiming(word="test", start_time=0.0, end_time=0.5, probability=0.0)
- assert word.probability == 0.0
+ assert word.probability == 0.0, f"probability expected 0.0, got {word.probability}"
def test_word_timing_boundary_probability_one(self) -> None:
"""Test probability at exact upper boundary."""
word = WordTiming(word="test", start_time=0.0, end_time=0.5, probability=1.0)
- assert word.probability == 1.0
+ assert word.probability == 1.0, f"probability expected 1.0, got {word.probability}"
def test_word_timing_equal_times(self) -> None:
"""Test word timing with equal start and end times."""
word = WordTiming(word="instant", start_time=1.5, end_time=1.5, probability=0.9)
- assert word.start_time == word.end_time
+ assert word.start_time == word.end_time, (
+ f"start_time and end_time should be equal, got {word.start_time} and {word.end_time}"
+ )
def test_word_timing_empty_word(self) -> None:
"""Test word timing with empty word string."""
word = WordTiming(word="", start_time=0.0, end_time=0.1, probability=0.5)
- assert word.word == ""
+ assert word.word == "", f"word expected empty string, got {word.word!r}"
def test_word_timing_unicode_word(self) -> None:
"""Test word timing with unicode characters."""
word = WordTiming(word="日本語", start_time=0.0, end_time=0.5, probability=0.95)
- assert word.word == "日本語"
+ assert word.word == "日本語", f"word expected '日本語', got {word.word!r}"
diff --git a/tests/domain/test_summary.py b/tests/domain/test_summary.py
index f198a28..d2141fd 100644
--- a/tests/domain/test_summary.py
+++ b/tests/domain/test_summary.py
@@ -9,6 +9,12 @@ import pytest
from noteflow.domain.entities.summary import ActionItem, KeyPoint, Summary
from noteflow.domain.value_objects import MeetingId
+# Test constants
+MANY_SEGMENT_IDS_COUNT = 50
+KEY_POINT_START_TIME = 10.5
+KEY_POINT_END_TIME = 25.0
+VERY_LONG_SUMMARY_LENGTH = 10000
+
class TestKeyPoint:
"""Tests for KeyPoint entity."""
@@ -25,28 +31,28 @@ class TestKeyPoint:
def test_key_point_defaults(self, attr: str, expected: object) -> None:
"""Test KeyPoint default attribute values."""
kp = KeyPoint(text="Important discussion about architecture")
- assert getattr(kp, attr) == expected
+ assert getattr(kp, attr) == expected, f"expected {attr}={expected!r}, got {getattr(kp, attr)!r}"
def test_key_point_is_sourced_false(self) -> None:
"""Test is_sourced returns False when no segment_ids."""
kp = KeyPoint(text="No evidence")
- assert kp.is_sourced() is False
+ assert kp.is_sourced() is False, "key point without segment_ids should not be sourced"
def test_key_point_is_sourced_true(self) -> None:
"""Test is_sourced returns True with segment_ids."""
kp = KeyPoint(text="With evidence", segment_ids=[1, 2, 3])
- assert kp.is_sourced() is True
+ assert kp.is_sourced() is True, "key point with segment_ids should be sourced"
def test_key_point_with_timing(self) -> None:
"""Test KeyPoint with timing information."""
kp = KeyPoint(
text="Timed point",
segment_ids=[0, 1],
- start_time=10.5,
- end_time=25.0,
+ start_time=KEY_POINT_START_TIME,
+ end_time=KEY_POINT_END_TIME,
)
- assert kp.start_time == 10.5, "start_time should match provided value"
- assert kp.end_time == 25.0, "end_time should match provided value"
+ assert kp.start_time == KEY_POINT_START_TIME, "start_time should match provided value"
+ assert kp.end_time == KEY_POINT_END_TIME, "end_time should match provided value"
class TestActionItem:
@@ -65,37 +71,37 @@ class TestActionItem:
def test_action_item_defaults(self, attr: str, expected: object) -> None:
"""Test ActionItem default attribute values."""
ai = ActionItem(text="Review PR #123")
- assert getattr(ai, attr) == expected
+ assert getattr(ai, attr) == expected, f"expected {attr}={expected!r}, got {getattr(ai, attr)!r}"
def test_action_item_has_evidence_false(self) -> None:
"""Test has_evidence returns False when no segment_ids."""
ai = ActionItem(text="Task without evidence")
- assert ai.has_evidence() is False
+ assert ai.has_evidence() is False, "action item without segment_ids should not have evidence"
def test_action_item_has_evidence_true(self) -> None:
"""Test has_evidence returns True with segment_ids."""
ai = ActionItem(text="Task with evidence", segment_ids=[5])
- assert ai.has_evidence() is True
+ assert ai.has_evidence() is True, "action item with segment_ids should have evidence"
def test_action_item_is_assigned_false(self) -> None:
"""Test is_assigned returns False when no assignee."""
ai = ActionItem(text="Unassigned task")
- assert ai.is_assigned() is False
+ assert ai.is_assigned() is False, "action item without assignee should not be assigned"
def test_action_item_is_assigned_true(self) -> None:
"""Test is_assigned returns True with assignee."""
ai = ActionItem(text="Assigned task", assignee="Alice")
- assert ai.is_assigned() is True
+ assert ai.is_assigned() is True, "action item with assignee should be assigned"
def test_action_item_has_due_date_false(self) -> None:
"""Test has_due_date returns False when no due_date."""
ai = ActionItem(text="No deadline")
- assert ai.has_due_date() is False
+ assert ai.has_due_date() is False, "action item without due_date should not have due date"
def test_action_item_has_due_date_true(self) -> None:
"""Test has_due_date returns True with due_date."""
ai = ActionItem(text="With deadline", due_date=datetime(2024, 12, 31))
- assert ai.has_due_date() is True
+ assert ai.has_due_date() is True, "action item with due_date should have due date"
class TestSummary:
@@ -114,12 +120,12 @@ class TestSummary:
def test_summary_defaults(self, meeting_id: MeetingId, attr: str, expected: object) -> None:
"""Test Summary default attribute values."""
summary = Summary(meeting_id=meeting_id)
- assert getattr(summary, attr) == expected
+ assert getattr(summary, attr) == expected, f"expected {attr}={expected!r}, got {getattr(summary, attr)!r}"
def test_summary_meeting_id(self, meeting_id: MeetingId) -> None:
"""Test Summary stores meeting_id correctly."""
summary = Summary(meeting_id=meeting_id)
- assert summary.meeting_id == meeting_id
+ assert summary.meeting_id == meeting_id, f"expected meeting_id={meeting_id}, got {summary.meeting_id}"
def test_summary_key_point_count(self, meeting_id: MeetingId) -> None:
"""Test key_point_count property."""
@@ -131,7 +137,7 @@ class TestSummary:
KeyPoint(text="Point 3"),
],
)
- assert summary.key_point_count == 3
+ assert summary.key_point_count == 3, f"expected key_point_count=3, got {summary.key_point_count}"
def test_summary_action_item_count(self, meeting_id: MeetingId) -> None:
"""Test action_item_count property."""
@@ -142,7 +148,7 @@ class TestSummary:
ActionItem(text="Task 2"),
],
)
- assert summary.action_item_count == 2
+ assert summary.action_item_count == 2, f"expected action_item_count=2, got {summary.action_item_count}"
def test_all_points_have_evidence_true(self, meeting_id: MeetingId) -> None:
"""Test all_points_have_evidence returns True when all evidenced."""
@@ -153,7 +159,7 @@ class TestSummary:
KeyPoint(text="Point 2", segment_ids=[1, 2]),
],
)
- assert summary.all_points_have_evidence() is True
+ assert summary.all_points_have_evidence() is True, "all points with segment_ids should be evidenced"
def test_all_points_have_evidence_false(self, meeting_id: MeetingId) -> None:
"""Test all_points_have_evidence returns False when some unevidenced."""
@@ -164,7 +170,7 @@ class TestSummary:
KeyPoint(text="Point 2"), # No evidence
],
)
- assert summary.all_points_have_evidence() is False
+ assert summary.all_points_have_evidence() is False, "should be False when some points lack segment_ids"
def test_all_actions_have_evidence_true(self, meeting_id: MeetingId) -> None:
"""Test all_actions_have_evidence returns True when all evidenced."""
@@ -174,7 +180,7 @@ class TestSummary:
ActionItem(text="Task 1", segment_ids=[0]),
],
)
- assert summary.all_actions_have_evidence() is True
+ assert summary.all_actions_have_evidence() is True, "all actions with segment_ids should be evidenced"
def test_all_actions_have_evidence_false(self, meeting_id: MeetingId) -> None:
"""Test all_actions_have_evidence returns False when some unevidenced."""
@@ -184,7 +190,7 @@ class TestSummary:
ActionItem(text="Task 1"), # No evidence
],
)
- assert summary.all_actions_have_evidence() is False
+ assert summary.all_actions_have_evidence() is False, "should be False when some actions lack segment_ids"
def test_is_fully_evidenced_true(self, meeting_id: MeetingId) -> None:
"""Test is_fully_evidenced returns True when all items evidenced."""
@@ -193,7 +199,7 @@ class TestSummary:
key_points=[KeyPoint(text="KP", segment_ids=[0])],
action_items=[ActionItem(text="AI", segment_ids=[1])],
)
- assert summary.is_fully_evidenced() is True
+ assert summary.is_fully_evidenced() is True, "summary with all evidenced items should be fully evidenced"
def test_is_fully_evidenced_false_points(self, meeting_id: MeetingId) -> None:
"""Test is_fully_evidenced returns False with unevidenced points."""
@@ -202,7 +208,7 @@ class TestSummary:
key_points=[KeyPoint(text="KP")], # No evidence
action_items=[ActionItem(text="AI", segment_ids=[1])],
)
- assert summary.is_fully_evidenced() is False
+ assert summary.is_fully_evidenced() is False, "summary with unevidenced points should not be fully evidenced"
def test_unevidenced_points(self, meeting_id: MeetingId) -> None:
"""Test unevidenced_points property filters correctly."""
@@ -235,17 +241,17 @@ class TestSummaryEdgeCases:
def test_all_points_have_evidence_empty_list(self, meeting_id: MeetingId) -> None:
"""Test all_points_have_evidence returns True for empty key_points."""
summary = Summary(meeting_id=meeting_id, key_points=[])
- assert summary.all_points_have_evidence() is True
+ assert summary.all_points_have_evidence() is True, "empty key_points list should be considered all evidenced"
def test_all_actions_have_evidence_empty_list(self, meeting_id: MeetingId) -> None:
"""Test all_actions_have_evidence returns True for empty action_items."""
summary = Summary(meeting_id=meeting_id, action_items=[])
- assert summary.all_actions_have_evidence() is True
+ assert summary.all_actions_have_evidence() is True, "empty action_items list should be considered all evidenced"
def test_is_fully_evidenced_empty_summary(self, meeting_id: MeetingId) -> None:
"""Test is_fully_evidenced returns True for empty summary."""
summary = Summary(meeting_id=meeting_id)
- assert summary.is_fully_evidenced() is True
+ assert summary.is_fully_evidenced() is True, "empty summary should be considered fully evidenced"
def test_all_points_unevidenced(self, meeting_id: MeetingId) -> None:
"""Test all_points_have_evidence returns False when all points unevidenced."""
@@ -280,10 +286,10 @@ class TestSummaryEdgeCases:
def test_key_point_with_many_segment_ids(self) -> None:
"""Test key point with many segment references."""
- many_segments = list(range(50))
+ many_segments = list(range(MANY_SEGMENT_IDS_COUNT))
kp = KeyPoint(text="Well-sourced point", segment_ids=many_segments)
assert kp.is_sourced() is True, "key point with segments should be sourced"
- assert len(kp.segment_ids) == 50, "all segment_ids should be preserved"
+ assert len(kp.segment_ids) == MANY_SEGMENT_IDS_COUNT, "all segment_ids should be preserved"
def test_action_item_with_all_fields(self) -> None:
"""Test action item with all optional fields populated."""
@@ -313,6 +319,6 @@ class TestSummaryEdgeCases:
def test_summary_very_long_executive_summary(self, meeting_id: MeetingId) -> None:
"""Test summary handles very long executive summary."""
- long_summary = "A" * 10000
+ long_summary = "A" * VERY_LONG_SUMMARY_LENGTH
summary = Summary(meeting_id=meeting_id, executive_summary=long_summary)
- assert len(summary.executive_summary) == 10000
+ assert len(summary.executive_summary) == VERY_LONG_SUMMARY_LENGTH, f"expected length {VERY_LONG_SUMMARY_LENGTH}, got {len(summary.executive_summary)}"
diff --git a/tests/domain/test_triggers.py b/tests/domain/test_triggers.py
index c9574ea..941ce70 100644
--- a/tests/domain/test_triggers.py
+++ b/tests/domain/test_triggers.py
@@ -16,7 +16,7 @@ def test_trigger_signal_weight_bounds() -> None:
TriggerSignal(source=TriggerSource.AUDIO_ACTIVITY, weight=1.1)
signal = TriggerSignal(source=TriggerSource.AUDIO_ACTIVITY, weight=0.5)
- assert signal.weight == 0.5
+ assert signal.weight == 0.5, f"expected weight 0.5, got {signal.weight}"
@pytest.mark.parametrize(
@@ -36,7 +36,8 @@ def test_trigger_decision_attributes(attr: str, expected: object) -> None:
confidence=0.6,
signals=(audio, foreground),
)
- assert getattr(decision, attr) == expected
+ actual = getattr(decision, attr)
+ assert actual == expected, f"expected {attr}={expected!r}, got {actual!r}"
def test_trigger_decision_primary_signal() -> None:
@@ -52,14 +53,17 @@ def test_trigger_decision_primary_signal() -> None:
confidence=0.6,
signals=(audio, foreground),
)
- assert decision.primary_signal == foreground
+ assert decision.primary_signal == foreground, (
+ f"expected foreground signal as primary, got {decision.primary_signal}"
+ )
@pytest.mark.parametrize("attr", ["primary_signal", "detected_app"])
def test_trigger_decision_empty_signals_returns_none(attr: str) -> None:
"""Empty signals returns None for primary_signal and detected_app."""
empty = TriggerDecision(action=TriggerAction.IGNORE, confidence=0.0, signals=())
- assert getattr(empty, attr) is None
+ actual = getattr(empty, attr)
+ assert actual is None, f"expected {attr} to be None for empty signals, got {actual!r}"
class TestTriggerSignalEdgeCases:
@@ -68,28 +72,28 @@ class TestTriggerSignalEdgeCases:
def test_weight_exactly_zero(self) -> None:
"""Test weight at exact lower boundary (0.0) is valid."""
signal = TriggerSignal(source=TriggerSource.AUDIO_ACTIVITY, weight=0.0)
- assert signal.weight == 0.0
+ assert signal.weight == 0.0, f"expected weight 0.0 at lower boundary, got {signal.weight}"
def test_weight_exactly_one(self) -> None:
"""Test weight at exact upper boundary (1.0) is valid."""
signal = TriggerSignal(source=TriggerSource.AUDIO_ACTIVITY, weight=1.0)
- assert signal.weight == 1.0
+ assert signal.weight == 1.0, f"expected weight 1.0 at upper boundary, got {signal.weight}"
def test_weight_very_small_positive(self) -> None:
"""Test very small positive weight."""
signal = TriggerSignal(source=TriggerSource.AUDIO_ACTIVITY, weight=0.0001)
- assert signal.weight == 0.0001
+ assert signal.weight == 0.0001, f"expected small positive weight 0.0001, got {signal.weight}"
def test_weight_just_below_one(self) -> None:
"""Test weight just below upper boundary."""
signal = TriggerSignal(source=TriggerSource.AUDIO_ACTIVITY, weight=0.9999)
- assert signal.weight == 0.9999
+ assert signal.weight == 0.9999, f"expected weight 0.9999 just below boundary, got {signal.weight}"
@pytest.mark.parametrize("source", list(TriggerSource))
def test_all_trigger_sources(self, source: TriggerSource) -> None:
"""Test all trigger sources can create signals."""
signal = TriggerSignal(source=source, weight=0.5)
- assert signal.source == source
+ assert signal.source == source, f"expected source {source}, got {signal.source}"
def test_foreground_app_signal_with_app_name(self) -> None:
"""Test foreground app signal stores app name."""
@@ -98,7 +102,9 @@ class TestTriggerSignalEdgeCases:
weight=0.8,
app_name="Microsoft Teams",
)
- assert signal.app_name == "Microsoft Teams"
+ assert signal.app_name == "Microsoft Teams", (
+ f"expected app_name 'Microsoft Teams', got {signal.app_name!r}"
+ )
def test_calendar_signal_basic(self) -> None:
"""Test calendar trigger source is valid."""
@@ -106,8 +112,10 @@ class TestTriggerSignalEdgeCases:
source=TriggerSource.CALENDAR,
weight=0.9,
)
- assert signal.source == TriggerSource.CALENDAR
- assert signal.weight == 0.9
+ assert signal.source == TriggerSource.CALENDAR, (
+ f"expected source CALENDAR, got {signal.source}"
+ )
+ assert signal.weight == 0.9, f"expected weight 0.9, got {signal.weight}"
class TestTriggerDecisionEdgeCases:
@@ -121,7 +129,9 @@ class TestTriggerDecisionEdgeCases:
confidence=0.5,
signals=(signal,),
)
- assert decision.primary_signal == signal
+ assert decision.primary_signal == signal, (
+ f"expected single signal to be primary, got {decision.primary_signal}"
+ )
@pytest.mark.parametrize("action", list(TriggerAction))
def test_all_trigger_actions(self, action: TriggerAction) -> None:
@@ -131,7 +141,7 @@ class TestTriggerDecisionEdgeCases:
confidence=0.5,
signals=(),
)
- assert decision.action == action
+ assert decision.action == action, f"expected action {action}, got {decision.action}"
def test_confidence_zero(self) -> None:
"""Test zero confidence decision."""
@@ -140,7 +150,7 @@ class TestTriggerDecisionEdgeCases:
confidence=0.0,
signals=(),
)
- assert decision.confidence == 0.0
+ assert decision.confidence == 0.0, f"expected zero confidence, got {decision.confidence}"
def test_confidence_one(self) -> None:
"""Test full confidence decision."""
@@ -149,7 +159,7 @@ class TestTriggerDecisionEdgeCases:
confidence=1.0,
signals=(),
)
- assert decision.confidence == 1.0
+ assert decision.confidence == 1.0, f"expected full confidence 1.0, got {decision.confidence}"
def test_many_signals_finds_highest_weight(self) -> None:
"""Test primary_signal with many signals finds highest."""
@@ -162,8 +172,10 @@ class TestTriggerDecisionEdgeCases:
confidence=0.9,
signals=signals,
)
- assert decision.primary_signal is not None
- assert decision.primary_signal.weight == 0.9
+ assert decision.primary_signal is not None, "expected primary_signal to exist for non-empty signals"
+ assert decision.primary_signal.weight == 0.9, (
+ f"expected highest weight 0.9 as primary, got {decision.primary_signal.weight}"
+ )
def test_equal_weights_returns_first(self) -> None:
"""Test when signals have equal weights, first is returned."""
@@ -175,4 +187,6 @@ class TestTriggerDecisionEdgeCases:
signals=(signal1, signal2),
)
# max() returns first element when weights are equal
- assert decision.primary_signal in (signal1, signal2)
+ assert decision.primary_signal in (signal1, signal2), (
+ f"expected primary_signal to be one of the equal-weight signals, got {decision.primary_signal}"
+ )
diff --git a/tests/domain/test_value_objects.py b/tests/domain/test_value_objects.py
index d36a625..d066004 100644
--- a/tests/domain/test_value_objects.py
+++ b/tests/domain/test_value_objects.py
@@ -51,7 +51,11 @@ class TestMeetingState:
expected: bool,
) -> None:
"""Test state transition validation."""
- assert current.can_transition_to(target) == expected
+ result = current.can_transition_to(target)
+ assert result == expected, (
+ f"transition {current.name} -> {target.name}: "
+ f"expected {expected}, got {result}"
+ )
@pytest.mark.parametrize(
("value", "expected"),
@@ -67,7 +71,10 @@ class TestMeetingState:
)
def test_from_int_valid(self, value: int, expected: MeetingState) -> None:
"""Test conversion from valid integers."""
- assert MeetingState.from_int(value) == expected
+ result = MeetingState.from_int(value)
+ assert result == expected, (
+ f"from_int({value}): expected {expected.name}, got {result.name}"
+ )
def test_from_int_invalid_raises(self) -> None:
"""Test conversion from invalid integer raises ValueError."""
@@ -82,10 +89,16 @@ class TestMeetingId:
"""Test MeetingId wraps UUID."""
uuid = UUID("12345678-1234-5678-1234-567812345678")
meeting_id = MeetingId(uuid)
- assert meeting_id == uuid
+ assert meeting_id == uuid, (
+ f"MeetingId should equal underlying UUID: expected {uuid}, got {meeting_id}"
+ )
def test_meeting_id_string_conversion(self) -> None:
"""Test MeetingId can be converted to string."""
uuid = UUID("12345678-1234-5678-1234-567812345678")
meeting_id = MeetingId(uuid)
- assert str(meeting_id) == "12345678-1234-5678-1234-567812345678"
+ expected_str = "12345678-1234-5678-1234-567812345678"
+ result_str = str(meeting_id)
+ assert result_str == expected_str, (
+ f"MeetingId string conversion failed: expected {expected_str!r}, got {result_str!r}"
+ )
diff --git a/tests/grpc/test_annotation_mixin.py b/tests/grpc/test_annotation_mixin.py
index f1f16f9..c628de5 100644
--- a/tests/grpc/test_annotation_mixin.py
+++ b/tests/grpc/test_annotation_mixin.py
@@ -21,6 +21,12 @@ from noteflow.domain.value_objects import AnnotationId, AnnotationType, MeetingI
from noteflow.grpc._mixins.annotation import AnnotationMixin
from noteflow.grpc.proto import noteflow_pb2
+# Test constants for annotation timestamps and time ranges
+SAMPLE_ANNOTATION_END_TIME = 120.0
+SAMPLE_ANNOTATION_START_TIME_SHORT = 15.0
+SAMPLE_ANNOTATION_START_TIME_ACTION = 25.0
+TIME_RANGE_FILTER_START = 20.0
+
class MockRepositoryProvider:
"""Mock repository provider as async context manager."""
@@ -121,7 +127,7 @@ class TestAddAnnotation:
"""AddAnnotation creates annotation with all fields populated."""
meeting_id = MeetingId(uuid4())
expected_text = "Important decision"
- expected_start = 15.0
+ expected_start = SAMPLE_ANNOTATION_START_TIME_SHORT
expected_end = 30.0
expected_segments = [1, 2, 3]
@@ -199,7 +205,7 @@ class TestAddAnnotation:
meeting_id=str(meeting_id),
annotation_type=noteflow_pb2.ANNOTATION_TYPE_ACTION_ITEM,
text="Follow up with client",
- start_time=25.0,
+ start_time=SAMPLE_ANNOTATION_START_TIME_ACTION,
end_time=35.0,
)
@@ -267,7 +273,7 @@ class TestAddAnnotation:
# (We need to check the mock provider's commit - access via the servicer)
mock_annotations_repo.add.assert_called_once()
- async def test_aborts_on_invalid_meeting_id(
+ async def test_aborts_on_invalid_meeting_id_add_annotation(
self,
servicer: MockServicerHost,
mock_annotations_repo: AsyncMock,
@@ -312,7 +318,7 @@ class TestGetAnnotation:
annotation_type=AnnotationType.DECISION,
text="Key decision made",
start_time=100.0,
- end_time=120.0,
+ end_time=SAMPLE_ANNOTATION_END_TIME,
segment_ids=[5, 6, 7],
)
mock_annotations_repo.get.return_value = expected_annotation
@@ -324,13 +330,13 @@ class TestGetAnnotation:
assert response.meeting_id == str(meeting_id), "meeting_id should match"
assert response.text == "Key decision made", "text should match"
assert response.start_time == 100.0, "start_time should match"
- assert response.end_time == 120.0, "end_time should match"
+ assert response.end_time == SAMPLE_ANNOTATION_END_TIME, "end_time should match"
assert list(response.segment_ids) == [5, 6, 7], "segment_ids should match"
assert (
response.annotation_type == noteflow_pb2.ANNOTATION_TYPE_DECISION
), "annotation_type should be DECISION"
- async def test_aborts_when_annotation_not_found(
+ async def test_aborts_when_annotation_not_found_get_annotation(
self,
servicer: MockServicerHost,
mock_annotations_repo: AsyncMock,
@@ -347,7 +353,7 @@ class TestGetAnnotation:
mock_grpc_context.abort.assert_called_once()
- async def test_aborts_on_invalid_annotation_id(
+ async def test_aborts_on_invalid_annotation_id_get_annotation(
self,
servicer: MockServicerHost,
mock_annotations_repo: AsyncMock,
@@ -452,7 +458,7 @@ class TestListAnnotations:
request = noteflow_pb2.ListAnnotationsRequest(
meeting_id=str(meeting_id),
- start_time=20.0,
+ start_time=TIME_RANGE_FILTER_START,
end_time=40.0,
)
response = await servicer.ListAnnotations(request, mock_grpc_context)
@@ -462,7 +468,7 @@ class TestListAnnotations:
response.annotations[0].text == "Annotation in range"
), "filtered annotation text should match"
mock_annotations_repo.get_by_time_range.assert_called_once_with(
- meeting_id, 20.0, 40.0
+ meeting_id, TIME_RANGE_FILTER_START, 40.0
)
mock_annotations_repo.get_by_meeting.assert_not_called()
@@ -488,7 +494,7 @@ class TestListAnnotations:
meeting_id, 50.0, 0.0
)
- async def test_aborts_on_invalid_meeting_id(
+ async def test_aborts_on_invalid_meeting_id_list_annotations(
self,
servicer: MockServicerHost,
mock_annotations_repo: AsyncMock,
@@ -541,8 +547,8 @@ class TestUpdateAnnotation:
annotation_id=str(annotation_id),
annotation_type=noteflow_pb2.ANNOTATION_TYPE_DECISION,
text="Updated text",
- start_time=15.0,
- end_time=25.0,
+ start_time=SAMPLE_ANNOTATION_START_TIME_SHORT,
+ end_time=SAMPLE_ANNOTATION_START_TIME_ACTION,
segment_ids=[2, 3],
)
@@ -553,8 +559,8 @@ class TestUpdateAnnotation:
assert (
response.annotation_type == noteflow_pb2.ANNOTATION_TYPE_DECISION
), "annotation_type should be updated to DECISION"
- assert response.start_time == 15.0, "start_time should be updated"
- assert response.end_time == 25.0, "end_time should be updated"
+ assert response.start_time == SAMPLE_ANNOTATION_START_TIME_SHORT, "start_time should be updated"
+ assert response.end_time == SAMPLE_ANNOTATION_START_TIME_ACTION, "end_time should be updated"
assert list(response.segment_ids) == [2, 3], "segment_ids should be updated"
mock_annotations_repo.update.assert_called_once()
@@ -573,7 +579,7 @@ class TestUpdateAnnotation:
annotation_type=AnnotationType.NOTE,
text="Original text",
start_time=10.0,
- end_time=20.0,
+ end_time=TIME_RANGE_FILTER_START,
)
mock_annotations_repo.get.return_value = original_annotation
@@ -592,9 +598,9 @@ class TestUpdateAnnotation:
assert response.text == "Only text updated", "text should be updated"
# Note: annotation_type stays as original since UNSPECIFIED is sent
assert response.start_time == 10.0, "start_time should remain unchanged"
- assert response.end_time == 20.0, "end_time should remain unchanged"
+ assert response.end_time == TIME_RANGE_FILTER_START, "end_time should remain unchanged"
- async def test_aborts_when_annotation_not_found(
+ async def test_aborts_when_annotation_not_found_update_annotation(
self,
servicer: MockServicerHost,
mock_annotations_repo: AsyncMock,
@@ -615,7 +621,7 @@ class TestUpdateAnnotation:
mock_grpc_context.abort.assert_called_once()
mock_annotations_repo.update.assert_not_called()
- async def test_aborts_on_invalid_annotation_id(
+ async def test_aborts_on_invalid_annotation_id_update_annotation(
self,
servicer: MockServicerHost,
mock_annotations_repo: AsyncMock,
@@ -686,7 +692,7 @@ class TestDeleteAnnotation:
assert response.success is True, "should return success=True"
mock_annotations_repo.delete.assert_called_once()
- async def test_aborts_when_annotation_not_found(
+ async def test_aborts_when_annotation_not_found_delete_annotation(
self,
servicer: MockServicerHost,
mock_annotations_repo: AsyncMock,
@@ -703,7 +709,7 @@ class TestDeleteAnnotation:
mock_grpc_context.abort.assert_called_once()
- async def test_aborts_on_invalid_annotation_id(
+ async def test_aborts_on_invalid_annotation_id_delete_annotation(
self,
servicer: MockServicerHost,
mock_annotations_repo: AsyncMock,
diff --git a/tests/grpc/test_diarization_cancel.py b/tests/grpc/test_diarization_cancel.py
index 29f2b95..fb48d6d 100644
--- a/tests/grpc/test_diarization_cancel.py
+++ b/tests/grpc/test_diarization_cancel.py
@@ -13,6 +13,9 @@ from noteflow.grpc.proto import noteflow_pb2
from noteflow.grpc.service import NoteFlowServicer
from noteflow.infrastructure.persistence.repositories import DiarizationJob
+# Test constants for progress calculation
+EXPECTED_PROGRESS_PERCENT = 50.0
+
class _DummyContext:
"""Minimal gRPC context that raises if abort is invoked."""
@@ -151,7 +154,7 @@ async def test_progress_percent_running() -> None:
# With 120s audio at 0.17 ratio -> ~20s estimated duration
# 10s elapsed / 20s estimated = 50% progress
- assert response.progress_percent == pytest.approx(50.0, rel=0.2), "running job progress should be approximately 50% based on elapsed time"
+ assert response.progress_percent == pytest.approx(EXPECTED_PROGRESS_PERCENT, rel=0.2), "running job progress should be approximately 50% based on elapsed time"
@pytest.mark.asyncio
diff --git a/tests/grpc/test_diarization_mixin.py b/tests/grpc/test_diarization_mixin.py
index 6e34553..6904c71 100644
--- a/tests/grpc/test_diarization_mixin.py
+++ b/tests/grpc/test_diarization_mixin.py
@@ -20,6 +20,9 @@ from noteflow.grpc.proto import noteflow_pb2
from noteflow.grpc.service import NoteFlowServicer
from noteflow.infrastructure.persistence.repositories import DiarizationJob
+# Test constants for progress calculation
+EXPECTED_RUNNING_JOB_PROGRESS_PERCENT = 50.0
+
if TYPE_CHECKING:
from noteflow.grpc.meeting_store import InMemoryMeetingStore
@@ -419,7 +422,7 @@ class TestGetDiarizationJobStatusProgress:
)
# 120s audio at ~0.17 ratio = ~20s estimated; 10s elapsed = ~50%
- assert response.progress_percent == pytest.approx(50.0, rel=0.25), "Running job progress should be ~50% based on elapsed time"
+ assert response.progress_percent == pytest.approx(EXPECTED_RUNNING_JOB_PROGRESS_PERCENT, rel=0.25), "Running job progress should be ~50% based on elapsed time"
@pytest.mark.asyncio
async def test_status_progress_completed_is_full(
diff --git a/tests/grpc/test_entities_mixin.py b/tests/grpc/test_entities_mixin.py
index 0f90875..d65ab0b 100644
--- a/tests/grpc/test_entities_mixin.py
+++ b/tests/grpc/test_entities_mixin.py
@@ -285,7 +285,7 @@ class TestExtractEntities:
mock_grpc_context.abort.assert_called_once()
- async def test_aborts_with_invalid_meeting_id(
+ async def test_extract_aborts_with_invalid_meeting_id_format(
self,
servicer: MockServicerHost,
mock_grpc_context: MagicMock,
@@ -417,7 +417,7 @@ class TestUpdateEntity:
assert response.entity.category == "company", "entity category should be updated"
- async def test_aborts_when_entity_not_found(
+ async def test_update_aborts_when_entity_not_found(
self,
servicer: MockServicerHost,
mock_entities_repo: AsyncMock,
@@ -437,7 +437,7 @@ class TestUpdateEntity:
mock_grpc_context.abort.assert_called_once()
- async def test_aborts_when_entity_belongs_to_different_meeting(
+ async def test_update_aborts_when_entity_belongs_to_different_meeting(
self,
servicer: MockServicerHost,
mock_entities_repo: AsyncMock,
@@ -461,7 +461,7 @@ class TestUpdateEntity:
mock_grpc_context.abort.assert_called_once()
- async def test_aborts_with_invalid_entity_id_format(
+ async def test_update_aborts_with_invalid_entity_id_format(
self,
servicer: MockServicerHost,
mock_grpc_context: MagicMock,
@@ -478,7 +478,7 @@ class TestUpdateEntity:
mock_grpc_context.abort.assert_called_once()
- async def test_aborts_with_invalid_meeting_id_format(
+ async def test_update_aborts_with_invalid_meeting_id_format(
self,
servicer: MockServicerHost,
mock_grpc_context: MagicMock,
@@ -554,7 +554,7 @@ class TestDeleteEntity:
assert response.success is True, "delete should succeed"
mock_entities_repo.delete.assert_called_once_with(entity.id)
- async def test_aborts_when_entity_not_found(
+ async def test_delete_aborts_when_entity_not_found(
self,
servicer: MockServicerHost,
mock_entities_repo: AsyncMock,
@@ -573,7 +573,7 @@ class TestDeleteEntity:
mock_grpc_context.abort.assert_called_once()
- async def test_aborts_when_entity_belongs_to_different_meeting(
+ async def test_delete_aborts_when_entity_belongs_to_different_meeting(
self,
servicer: MockServicerHost,
mock_entities_repo: AsyncMock,
@@ -619,7 +619,7 @@ class TestDeleteEntity:
mock_grpc_context.abort.assert_called_once()
- async def test_aborts_with_invalid_entity_id_format(
+ async def test_delete_aborts_with_invalid_entity_id_format(
self,
servicer: MockServicerHost,
mock_grpc_context: MagicMock,
@@ -635,7 +635,7 @@ class TestDeleteEntity:
mock_grpc_context.abort.assert_called_once()
- async def test_aborts_with_invalid_meeting_id_format(
+ async def test_delete_aborts_with_invalid_meeting_id_format(
self,
servicer: MockServicerHost,
mock_grpc_context: MagicMock,
diff --git a/tests/grpc/test_export_mixin.py b/tests/grpc/test_export_mixin.py
index f0e0571..6174bb1 100644
--- a/tests/grpc/test_export_mixin.py
+++ b/tests/grpc/test_export_mixin.py
@@ -376,8 +376,6 @@ class TestExportTranscriptMeetingNotFound:
async def test_export_aborts_when_meeting_not_found(
self,
export_servicer: MockServicerHost,
- mock_meetings_repo: AsyncMock,
- mock_segments_repo: AsyncMock,
mock_grpc_context: MagicMock,
) -> None:
"""ExportTranscript aborts with NOT_FOUND when meeting does not exist."""
diff --git a/tests/grpc/test_meeting_mixin.py b/tests/grpc/test_meeting_mixin.py
index 5cb4099..9ac8f07 100644
--- a/tests/grpc/test_meeting_mixin.py
+++ b/tests/grpc/test_meeting_mixin.py
@@ -21,6 +21,10 @@ from noteflow.domain.value_objects import MeetingId, MeetingState
from noteflow.grpc._mixins.meeting import MeetingMixin
from noteflow.grpc.proto import noteflow_pb2
+# Test constants for pagination
+PAGE_LIMIT_SMALL = 25
+PAGE_OFFSET_STANDARD = 50
+
if TYPE_CHECKING:
pass
@@ -467,11 +471,11 @@ class TestListMeetings:
"""ListMeetings respects limit parameter."""
meeting_mixin_meetings_repo.list_all.return_value = ([], 0)
- request = noteflow_pb2.ListMeetingsRequest(limit=25)
+ request = noteflow_pb2.ListMeetingsRequest(limit=PAGE_LIMIT_SMALL)
await meeting_mixin_servicer.ListMeetings(request, mock_grpc_context)
call_kwargs = meeting_mixin_meetings_repo.list_all.call_args[1]
- assert call_kwargs["limit"] == 25, "Limit should be passed to repository"
+ assert call_kwargs["limit"] == PAGE_LIMIT_SMALL, "Limit should be passed to repository"
async def test_list_meetings_uses_default_limit_of_100(
self,
@@ -497,11 +501,11 @@ class TestListMeetings:
"""ListMeetings respects offset parameter."""
meeting_mixin_meetings_repo.list_all.return_value = ([], 0)
- request = noteflow_pb2.ListMeetingsRequest(offset=50)
+ request = noteflow_pb2.ListMeetingsRequest(offset=PAGE_OFFSET_STANDARD)
await meeting_mixin_servicer.ListMeetings(request, mock_grpc_context)
call_kwargs = meeting_mixin_meetings_repo.list_all.call_args[1]
- assert call_kwargs["offset"] == 50, "Offset should be passed to repository"
+ assert call_kwargs["offset"] == PAGE_OFFSET_STANDARD, "Offset should be passed to repository"
@pytest.mark.parametrize(
("proto_sort_order", "expected_desc"),
diff --git a/tests/grpc/test_preferences_mixin.py b/tests/grpc/test_preferences_mixin.py
index c4aba96..e416cca 100644
--- a/tests/grpc/test_preferences_mixin.py
+++ b/tests/grpc/test_preferences_mixin.py
@@ -22,6 +22,9 @@ from noteflow.infrastructure.persistence.repositories.preferences_repo import (
PreferenceWithMetadata,
)
+# Test constants
+ETAG_HEX_DIGEST_LENGTH = 32
+
if TYPE_CHECKING:
from datetime import datetime
@@ -81,7 +84,7 @@ class TestComputeEtag:
etag2 = _compute_etag(prefs, updated_at)
assert etag1 == etag2, "ETag should be identical for same inputs"
- assert len(etag1) == 32, "ETag should be 32 chars (MD5 hex digest)"
+ assert len(etag1) == ETAG_HEX_DIGEST_LENGTH, "ETag should be 32 chars (MD5 hex digest)"
def test_different_values_produce_different_etag(self) -> None:
"""Different preference values produce different ETags."""
diff --git a/tests/grpc/test_project_mixin.py b/tests/grpc/test_project_mixin.py
index 4a36187..6805b4f 100644
--- a/tests/grpc/test_project_mixin.py
+++ b/tests/grpc/test_project_mixin.py
@@ -579,7 +579,7 @@ class TestDeleteProject:
assert response.success is True, "Should return success"
- async def test_delete_project_not_found(
+ async def test_delete_project_not_found_returns_success_false(
self,
project_mixin_servicer: MockProjectServicerHost,
mock_project_service: MagicMock,
diff --git a/tests/grpc/test_sprint_15_1_critical_bugs.py b/tests/grpc/test_sprint_15_1_critical_bugs.py
index 8088437..61d3fa6 100644
--- a/tests/grpc/test_sprint_15_1_critical_bugs.py
+++ b/tests/grpc/test_sprint_15_1_critical_bugs.py
@@ -231,11 +231,11 @@ class TestDiarizationDatetimeAwareness:
diarization_dir = Path("src/noteflow/grpc/_mixins/diarization")
# Check all Python files in the diarization package
- for diarization_path in diarization_dir.glob("*.py"):
- content = diarization_path.read_text()
-
- # Should not find datetime.now() pattern
- assert "datetime.now()" not in content, (
- f"datetime.now() found in {diarization_path.name} - "
- "should use utc_now() for timezone-aware datetimes"
- )
+ files_with_datetime_now = [
+ p.name for p in diarization_dir.glob("*.py")
+ if "datetime.now()" in p.read_text()
+ ]
+ assert not files_with_datetime_now, (
+ f"datetime.now() found in {files_with_datetime_now} - "
+ "should use utc_now() for timezone-aware datetimes"
+ )
diff --git a/tests/grpc/test_stream_lifecycle.py b/tests/grpc/test_stream_lifecycle.py
index bb4d77b..4839526 100644
--- a/tests/grpc/test_stream_lifecycle.py
+++ b/tests/grpc/test_stream_lifecycle.py
@@ -22,6 +22,7 @@ if TYPE_CHECKING:
# Test constants
MULTI_SESSION_COUNT = 5
DIARIZATION_TASK_COUNT = 3
+EXPECTED_AUDIO_SAMPLE_COUNT = 3200
class TestStreamCancellation:
@@ -203,7 +204,7 @@ class TestAudioWriterCleanup:
@pytest.mark.asyncio
async def test_audio_writer_closed_on_cleanup(
- self, memory_servicer: NoteFlowServicer, crypto: MagicMock, tmp_path: Path
+ self, memory_servicer: NoteFlowServicer, tmp_path: Path
) -> None:
"""Verify audio writer is closed during cleanup."""
from noteflow.infrastructure.audio.writer import MeetingAudioWriter
@@ -366,7 +367,7 @@ class TestPartialBufferCleanup:
memory_servicer._partial_buffers[meeting_id].append(audio_chunk)
# PartialAudioBuffer len() returns sample count, not chunk count
- assert len(memory_servicer._partial_buffers[meeting_id]) == 3200, "Should have 3200 samples (2 chunks)"
+ assert len(memory_servicer._partial_buffers[meeting_id]) == EXPECTED_AUDIO_SAMPLE_COUNT, "Should have 3200 samples (2 chunks)"
memory_servicer._cleanup_streaming_state(meeting_id)
@@ -653,9 +654,7 @@ class TestGrpcContextCancellationReal:
assert meeting_id not in memory_servicer._vad_instances, "VAD should be cleaned up"
@pytest.mark.asyncio
- async def test_context_cancelled_check_pattern(
- self, memory_servicer: NoteFlowServicer
- ) -> None:
+ async def test_context_cancelled_check_pattern(self) -> None:
"""Verify the implicit cancellation detection pattern works correctly."""
import asyncio
diff --git a/tests/grpc/test_timestamp_converters.py b/tests/grpc/test_timestamp_converters.py
index 34b4fb0..1a8d66b 100644
--- a/tests/grpc/test_timestamp_converters.py
+++ b/tests/grpc/test_timestamp_converters.py
@@ -16,13 +16,21 @@ from noteflow.grpc._mixins.converters import (
proto_timestamp_to_datetime,
)
+# Test constants for timestamp conversion tests
+TEST_YEAR = 2024
+TEST_MONTH = 6
+TEST_DAY = 15
+TEST_HOUR = 14
+TEST_MINUTE = 30
+TEST_SECOND = 45
+
class TestDatetimeProtoTimestampConversion:
"""Test datetime <-> protobuf Timestamp conversions."""
def test_datetime_to_proto_timestamp_computes_correct_epoch(self) -> None:
"""Convert datetime to proto Timestamp with correct epoch seconds."""
- dt = datetime(2024, 6, 15, 14, 30, 45, tzinfo=UTC)
+ dt = datetime(TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE, TEST_SECOND, tzinfo=UTC)
expected_epoch = 1718461845
ts = datetime_to_proto_timestamp(dt)
@@ -34,7 +42,7 @@ class TestDatetimeProtoTimestampConversion:
def test_proto_timestamp_to_datetime_returns_utc(self) -> None:
"""Convert proto Timestamp to datetime with UTC timezone."""
ts = Timestamp()
- ts.FromDatetime(datetime(2024, 6, 15, 14, 30, 45, tzinfo=UTC))
+ ts.FromDatetime(datetime(TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE, TEST_SECOND, tzinfo=UTC))
dt = proto_timestamp_to_datetime(ts)
@@ -42,7 +50,7 @@ class TestDatetimeProtoTimestampConversion:
def test_datetime_proto_timestamp_roundtrip(self) -> None:
"""Datetime -> proto Timestamp -> datetime roundtrip preserves value."""
- original = datetime(2024, 6, 15, 14, 30, 45, tzinfo=UTC)
+ original = datetime(TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE, TEST_SECOND, tzinfo=UTC)
ts = datetime_to_proto_timestamp(original)
result = proto_timestamp_to_datetime(ts)
@@ -67,11 +75,11 @@ class TestEpochSecondsConversion:
dt = epoch_seconds_to_datetime(epoch_seconds)
assert dt.tzinfo == UTC, "Returned datetime should have UTC timezone"
- assert dt.year == 2024, f"Expected year 2024, got {dt.year}"
+ assert dt.year == TEST_YEAR, f"Expected year {TEST_YEAR}, got {dt.year}"
def test_datetime_to_epoch_seconds_computes_correct_value(self) -> None:
"""Convert datetime to epoch seconds with correct value."""
- dt = datetime(2024, 6, 15, 14, 30, 45, tzinfo=UTC)
+ dt = datetime(TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE, TEST_SECOND, tzinfo=UTC)
expected_seconds = 1718461845.0
seconds = datetime_to_epoch_seconds(dt)
@@ -98,7 +106,7 @@ class TestEpochSecondsConversion:
[
pytest.param(0.0, 1970, id="unix_epoch_start"),
pytest.param(1000000000.0, 2001, id="billion_seconds"),
- pytest.param(1718458245.0, 2024, id="recent_date"),
+ pytest.param(1718458245.0, TEST_YEAR, id="recent_date"),
],
)
def test_epoch_seconds_to_datetime_various_values(
@@ -120,12 +128,12 @@ class TestIsoStringConversion:
dt = iso_string_to_datetime(iso_str)
assert dt.tzinfo == UTC, "Z suffix should be parsed as UTC"
- assert dt.year == 2024, f"Expected year 2024, got {dt.year}"
- assert dt.month == 6, f"Expected month 6, got {dt.month}"
- assert dt.day == 15, f"Expected day 15, got {dt.day}"
- assert dt.hour == 14, f"Expected hour 14, got {dt.hour}"
- assert dt.minute == 30, f"Expected minute 30, got {dt.minute}"
- assert dt.second == 45, f"Expected second 45, got {dt.second}"
+ assert dt.year == TEST_YEAR, f"Expected year {TEST_YEAR}, got {dt.year}"
+ assert dt.month == TEST_MONTH, f"Expected month {TEST_MONTH}, got {dt.month}"
+ assert dt.day == TEST_DAY, f"Expected day {TEST_DAY}, got {dt.day}"
+ assert dt.hour == TEST_HOUR, f"Expected hour {TEST_HOUR}, got {dt.hour}"
+ assert dt.minute == TEST_MINUTE, f"Expected minute {TEST_MINUTE}, got {dt.minute}"
+ assert dt.second == TEST_SECOND, f"Expected second {TEST_SECOND}, got {dt.second}"
def test_iso_string_with_offset_preserved(self) -> None:
"""Parse ISO string with timezone offset."""
@@ -150,12 +158,12 @@ class TestIsoStringConversion:
def test_datetime_to_iso_string_includes_timezone(self) -> None:
"""Format datetime as ISO string with timezone."""
- dt = datetime(2024, 6, 15, 14, 30, 45, tzinfo=UTC)
+ dt = datetime(TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE, TEST_SECOND, tzinfo=UTC)
iso_str = datetime_to_iso_string(dt)
- assert "2024-06-15" in iso_str, f"Date should be in output: {iso_str}"
- assert "14:30:45" in iso_str, f"Time should be in output: {iso_str}"
+ assert f"{TEST_YEAR}-0{TEST_MONTH}-{TEST_DAY}" in iso_str, f"Date should be in output: {iso_str}"
+ assert f"{TEST_HOUR}:{TEST_MINUTE}:{TEST_SECOND}" in iso_str, f"Time should be in output: {iso_str}"
# UTC represented as +00:00 in isoformat
assert "+00:00" in iso_str or "Z" in iso_str, (
f"Timezone should be in output: {iso_str}"
@@ -163,7 +171,7 @@ class TestIsoStringConversion:
def test_iso_string_roundtrip(self) -> None:
"""Datetime -> ISO string -> datetime roundtrip preserves value."""
- original = datetime(2024, 6, 15, 14, 30, 45, tzinfo=UTC)
+ original = datetime(TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE, TEST_SECOND, tzinfo=UTC)
iso_str = datetime_to_iso_string(original)
result = iso_string_to_datetime(iso_str)
diff --git a/tests/infrastructure/asr/test_segmenter.py b/tests/infrastructure/asr/test_segmenter.py
index 7c682b4..d192cb7 100644
--- a/tests/infrastructure/asr/test_segmenter.py
+++ b/tests/infrastructure/asr/test_segmenter.py
@@ -13,6 +13,13 @@ from noteflow.infrastructure.asr.segmenter import (
SegmenterState,
)
+# Test constants for custom configuration values
+CUSTOM_SAMPLE_RATE_HZ = 44100
+"""Custom sample rate in Hz for testing non-default configuration."""
+
+MAX_SEGMENT_DURATION_SEC = 60.0
+"""Maximum segment duration in seconds for testing force-emit behavior."""
+
class TestSegmenterInitialization:
"""Tests for Segmenter initialization."""
@@ -26,11 +33,11 @@ class TestSegmenterInitialization:
def test_custom_config(self) -> None:
"""Segmenter accepts custom configuration."""
- config = SegmenterConfig(sample_rate=44100, max_segment_duration=60.0)
+ config = SegmenterConfig(sample_rate=CUSTOM_SAMPLE_RATE_HZ, max_segment_duration=MAX_SEGMENT_DURATION_SEC)
segmenter = Segmenter(config=config)
- assert segmenter.config.sample_rate == 44100, "custom sample rate should be applied"
- assert segmenter.config.max_segment_duration == 60.0, "custom max segment duration should be applied"
+ assert segmenter.config.sample_rate == CUSTOM_SAMPLE_RATE_HZ, "custom sample rate should be applied"
+ assert segmenter.config.max_segment_duration == MAX_SEGMENT_DURATION_SEC, "custom max segment duration should be applied"
def test_initial_state_is_idle(self) -> None:
"""Segmenter starts in IDLE state."""
diff --git a/tests/infrastructure/audio/test_capture.py b/tests/infrastructure/audio/test_capture.py
index 0a16a8a..c0240ce 100644
--- a/tests/infrastructure/audio/test_capture.py
+++ b/tests/infrastructure/audio/test_capture.py
@@ -11,6 +11,10 @@ from numpy.typing import NDArray
from noteflow.config.constants import DEFAULT_SAMPLE_RATE
from noteflow.infrastructure.audio import SoundDeviceCapture
+# Test constants for audio capture configuration
+CUSTOM_SAMPLE_RATE_HZ = 44100
+"""Custom sample rate in Hz for testing non-default audio capture configuration."""
+
class TestSoundDeviceCapture:
"""Tests for SoundDeviceCapture class."""
@@ -95,11 +99,11 @@ class TestSoundDeviceCapture:
capture.start(
device_id=None,
on_frames=dummy_callback,
- sample_rate=44100,
+ sample_rate=CUSTOM_SAMPLE_RATE_HZ,
channels=1,
)
- assert capture.sample_rate == 44100, "sample_rate should reflect configured value"
+ assert capture.sample_rate == CUSTOM_SAMPLE_RATE_HZ, "sample_rate should reflect configured value"
assert capture.channels == 1, "channels should reflect configured value"
assert capture.is_capturing() is True, "is_capturing should return True after start"
finally:
diff --git a/tests/infrastructure/audio/test_dto.py b/tests/infrastructure/audio/test_dto.py
index f69436e..9302e1c 100644
--- a/tests/infrastructure/audio/test_dto.py
+++ b/tests/infrastructure/audio/test_dto.py
@@ -10,6 +10,10 @@ import pytest
from noteflow.config.constants import DEFAULT_SAMPLE_RATE
from noteflow.infrastructure.audio import AudioDeviceInfo, TimestampedAudio
+# Test constants for audio frame sizes
+AUDIO_FRAME_SIZE_SAMPLES = 1600
+"""Standard audio frame size in samples (0.1 seconds at 16kHz)."""
+
class TestAudioDeviceInfo:
"""Tests for AudioDeviceInfo dataclass."""
@@ -50,19 +54,19 @@ class TestTimestampedAudio:
def test_timestamped_audio_creation(self) -> None:
"""Test TimestampedAudio can be created with valid values."""
- frames = np.zeros(1600, dtype=np.float32)
+ frames = np.zeros(AUDIO_FRAME_SIZE_SAMPLES, dtype=np.float32)
audio = TimestampedAudio(
frames=frames,
timestamp=1.0,
duration=0.1,
)
- assert len(audio.frames) == 1600, "frames length should match input"
+ assert len(audio.frames) == AUDIO_FRAME_SIZE_SAMPLES, "frames length should match input"
assert audio.timestamp == 1.0, "timestamp should match input"
assert audio.duration == 0.1, "duration should match input"
def test_timestamped_audio_negative_duration_raises(self) -> None:
"""Test TimestampedAudio raises on negative duration."""
- frames = np.zeros(1600, dtype=np.float32)
+ frames = np.zeros(AUDIO_FRAME_SIZE_SAMPLES, dtype=np.float32)
with pytest.raises(ValueError, match="Duration must be non-negative"):
TimestampedAudio(
frames=frames,
@@ -72,7 +76,7 @@ class TestTimestampedAudio:
def test_timestamped_audio_negative_timestamp_raises(self) -> None:
"""Test TimestampedAudio raises on negative timestamp."""
- frames = np.zeros(1600, dtype=np.float32)
+ frames = np.zeros(AUDIO_FRAME_SIZE_SAMPLES, dtype=np.float32)
with pytest.raises(ValueError, match="Timestamp must be non-negative"):
TimestampedAudio(
frames=frames,
@@ -92,7 +96,7 @@ class TestTimestampedAudio:
def test_timestamped_audio_zero_timestamp_valid(self) -> None:
"""Test TimestampedAudio accepts zero timestamp."""
- frames = np.zeros(1600, dtype=np.float32)
+ frames = np.zeros(AUDIO_FRAME_SIZE_SAMPLES, dtype=np.float32)
audio = TimestampedAudio(
frames=frames,
timestamp=0.0,
diff --git a/tests/infrastructure/audio/test_reader.py b/tests/infrastructure/audio/test_reader.py
index 0f874f5..2ccff3f 100644
--- a/tests/infrastructure/audio/test_reader.py
+++ b/tests/infrastructure/audio/test_reader.py
@@ -14,6 +14,13 @@ from noteflow.infrastructure.audio.reader import MeetingAudioReader
from noteflow.infrastructure.audio.writer import MeetingAudioWriter
from noteflow.infrastructure.security.crypto import AesGcmCryptoBox
+# Test constants for audio configuration
+AUDIO_FRAME_SIZE_SAMPLES = 1600
+"""Standard audio frame size in samples (0.1 seconds at 16kHz)."""
+
+CUSTOM_SAMPLE_RATE_HZ = 48000
+"""Custom sample rate in Hz for testing non-default sample rate handling."""
+
# crypto and meetings_dir fixtures are provided by tests/conftest.py
@@ -46,13 +53,13 @@ def test_reader_uses_manifest_sample_rate(
wrapped_dek = crypto.wrap_dek(dek)
writer = MeetingAudioWriter(crypto, meetings_dir)
- writer.open(meeting_id, dek, wrapped_dek, sample_rate=48000)
- writer.write_chunk(np.zeros(1600, dtype=np.float32)) # 1600 samples @ 48kHz
+ writer.open(meeting_id, dek, wrapped_dek, sample_rate=CUSTOM_SAMPLE_RATE_HZ)
+ writer.write_chunk(np.zeros(AUDIO_FRAME_SIZE_SAMPLES, dtype=np.float32)) # 1600 samples @ 48kHz
writer.close()
reader = MeetingAudioReader(crypto, meetings_dir)
chunks = reader.load_meeting_audio(meeting_id)
- assert reader.sample_rate == 48000, "reader should expose sample_rate from manifest"
+ assert reader.sample_rate == CUSTOM_SAMPLE_RATE_HZ, "reader should expose sample_rate from manifest"
assert len(chunks) == 1, "should load exactly one chunk"
- assert chunks[0].duration == pytest.approx(1600 / 48000, rel=1e-6), "chunk duration should match sample count / sample rate"
+ assert chunks[0].duration == pytest.approx(AUDIO_FRAME_SIZE_SAMPLES / CUSTOM_SAMPLE_RATE_HZ, rel=1e-6), "chunk duration should match sample count / sample rate"
diff --git a/tests/infrastructure/audio/test_ring_buffer.py b/tests/infrastructure/audio/test_ring_buffer.py
index c737fcb..fe62e93 100644
--- a/tests/infrastructure/audio/test_ring_buffer.py
+++ b/tests/infrastructure/audio/test_ring_buffer.py
@@ -7,6 +7,13 @@ import pytest
from noteflow.infrastructure.audio import TimestampedAudio, TimestampedRingBuffer
+# Test constants for ring buffer configuration
+DEFAULT_MAX_DURATION_SEC = 30.0
+"""Default maximum buffer duration in seconds (30 seconds)."""
+
+CUSTOM_MAX_DURATION_SEC = 15.0
+"""Custom maximum buffer duration in seconds for testing configuration."""
+
class TestTimestampedRingBuffer:
"""Tests for TimestampedRingBuffer class."""
@@ -21,7 +28,7 @@ class TestTimestampedRingBuffer:
def test_init_with_default_duration(self) -> None:
"""Test buffer uses default max_duration of 30 seconds."""
buffer = TimestampedRingBuffer()
- assert buffer.max_duration == 30.0, "default max_duration should be 30 seconds"
+ assert buffer.max_duration == DEFAULT_MAX_DURATION_SEC, "default max_duration should be 30 seconds"
def test_init_with_invalid_duration_raises(self) -> None:
"""Test buffer raises on non-positive max_duration."""
@@ -172,8 +179,8 @@ class TestTimestampedRingBuffer:
def test_max_duration_property(self) -> None:
"""Test max_duration property returns configured value."""
- buffer = TimestampedRingBuffer(max_duration=15.0)
- assert buffer.max_duration == 15.0, "max_duration should return configured value"
+ buffer = TimestampedRingBuffer(max_duration=CUSTOM_MAX_DURATION_SEC)
+ assert buffer.max_duration == CUSTOM_MAX_DURATION_SEC, "max_duration should return configured value"
def test_len_returns_chunk_count(
self, timestamped_audio_sequence: list[TimestampedAudio]
diff --git a/tests/infrastructure/audio/test_writer.py b/tests/infrastructure/audio/test_writer.py
index e4808f4..e395c73 100644
--- a/tests/infrastructure/audio/test_writer.py
+++ b/tests/infrastructure/audio/test_writer.py
@@ -13,6 +13,13 @@ from noteflow.config.constants import DEFAULT_SAMPLE_RATE
from noteflow.infrastructure.audio.writer import MeetingAudioWriter
from noteflow.infrastructure.security.crypto import AesGcmCryptoBox, ChunkedAssetReader
+# Test constants for audio frame sizes
+AUDIO_FRAME_SIZE_SAMPLES = 1600
+"""Standard audio frame size in samples (0.1 seconds at 16kHz)."""
+
+PCM16_BYTES_PER_FRAME = 3200
+"""PCM16 encoded size in bytes for AUDIO_FRAME_SIZE_SAMPLES (2 bytes per sample)."""
+
# crypto and meetings_dir fixtures are provided by tests/conftest.py
@@ -78,13 +85,13 @@ class TestMeetingAudioWriterBasics:
writer.open(meeting_id, dek, wrapped_dek)
# Create test audio: 1600 samples = 0.1 seconds at 16kHz
- test_audio = np.linspace(-1.0, 1.0, 1600, dtype=np.float32)
+ test_audio = np.linspace(-1.0, 1.0, AUDIO_FRAME_SIZE_SAMPLES, dtype=np.float32)
writer.write_chunk(test_audio)
# Audio is 3200 bytes, buffer is 1000, so should flush
assert writer.bytes_written > 0, "bytes_written should be positive after flush"
# PCM16 = 2 bytes/sample = 3200 bytes raw, but encrypted with overhead
- assert writer.bytes_written > 3200, "bytes_written should exceed raw size due to encryption overhead"
+ assert writer.bytes_written > PCM16_BYTES_PER_FRAME, "bytes_written should exceed raw size due to encryption overhead"
assert writer.chunk_count == 1, "chunk_count should be 1 after first flush"
assert writer.write_count == 1, "write_count should be 1 after one write_chunk call"
@@ -107,10 +114,10 @@ class TestMeetingAudioWriterBasics:
# Write 100 chunks of 1600 samples each (3200 bytes per write)
# Buffer is 10000, so ~3 writes per encrypted chunk
num_writes = 100
- bytes_per_write = 1600 * 2 # 3200 bytes
+ bytes_per_write = PCM16_BYTES_PER_FRAME # 3200 bytes
for _ in range(num_writes):
- audio = np.random.uniform(-0.5, 0.5, 1600).astype(np.float32)
+ audio = np.random.uniform(-0.5, 0.5, AUDIO_FRAME_SIZE_SAMPLES).astype(np.float32)
writer.write_chunk(audio)
# write_count tracks incoming audio frames
@@ -221,7 +228,7 @@ class TestMeetingAudioWriterBasics:
writer.open(meeting_id, dek, wrapped_dek)
# Write small audio chunk (won't trigger auto-flush)
- writer.write_chunk(np.zeros(1600, dtype=np.float32))
+ writer.write_chunk(np.zeros(AUDIO_FRAME_SIZE_SAMPLES, dtype=np.float32))
# Data should be buffered, not written
assert writer.buffered_bytes > 0, "Data should be buffered before flush"
@@ -265,7 +272,7 @@ class TestMeetingAudioWriterErrors:
) -> None:
"""Test writer raises RuntimeError if write called before open."""
writer = MeetingAudioWriter(crypto, meetings_dir)
- audio = np.zeros(1600, dtype=np.float32)
+ audio = np.zeros(AUDIO_FRAME_SIZE_SAMPLES, dtype=np.float32)
with pytest.raises(RuntimeError, match="not open"):
writer.write_chunk(audio)
@@ -398,7 +405,7 @@ class TestMeetingAudioWriterIntegration:
wrapped_dek = crypto.wrap_dek(dek)
writer.open(meeting_id, dek, wrapped_dek)
- writer.write_chunk(np.zeros(1600, dtype=np.float32))
+ writer.write_chunk(np.zeros(AUDIO_FRAME_SIZE_SAMPLES, dtype=np.float32))
writer.close()
# Read manifest
@@ -485,7 +492,7 @@ class TestMeetingAudioWriterPeriodicFlush:
def write_audio() -> None:
try:
for _ in range(write_count):
- audio = np.random.uniform(-0.5, 0.5, 1600).astype(np.float32)
+ audio = np.random.uniform(-0.5, 0.5, AUDIO_FRAME_SIZE_SAMPLES).astype(np.float32)
writer.write_chunk(audio)
except (RuntimeError, ValueError, OSError) as e:
errors.append(e)
diff --git a/tests/infrastructure/ner/test_engine.py b/tests/infrastructure/ner/test_engine.py
index e9fdf3e..ffd059d 100644
--- a/tests/infrastructure/ner/test_engine.py
+++ b/tests/infrastructure/ner/test_engine.py
@@ -116,12 +116,12 @@ class TestEntityNormalization:
def test_normalized_text_is_lowercase(self, ner_engine: NerEngine) -> None:
"""Normalized text should be lowercase."""
entities = ner_engine.extract("John SMITH went to NYC.")
- for entity in entities:
- assert entity.normalized_text == entity.normalized_text.lower(), f"Normalized text '{entity.normalized_text}' should be lowercase"
+ non_lowercase = [e for e in entities if e.normalized_text != e.normalized_text.lower()]
+ assert not non_lowercase, f"All normalized text should be lowercase, but found: {[e.normalized_text for e in non_lowercase]}"
def test_confidence_is_set(self, ner_engine: NerEngine) -> None:
"""Entities should have confidence score."""
entities = ner_engine.extract("Microsoft Corporation is based in Seattle.")
assert entities, "Should find entities"
- for entity in entities:
- assert 0.0 <= entity.confidence <= 1.0, "Confidence should be between 0 and 1"
+ invalid_confidence = [e for e in entities if not (0.0 <= e.confidence <= 1.0)]
+ assert not invalid_confidence, f"All entities should have confidence between 0 and 1, but found: {[(e.text, e.confidence) for e in invalid_confidence]}"
diff --git a/tests/infrastructure/persistence/test_migrations.py b/tests/infrastructure/persistence/test_migrations.py
index 5228074..b590c4e 100644
--- a/tests/infrastructure/persistence/test_migrations.py
+++ b/tests/infrastructure/persistence/test_migrations.py
@@ -17,6 +17,34 @@ def _load_migration_module(path: Path) -> ast.Module:
return ast.parse(path.read_text())
+def _has_variable(path: Path, var_name: str) -> bool:
+ """Check if migration file has a specific variable assignment."""
+ tree = _load_migration_module(path)
+ nodes = list(ast.walk(tree))
+ assign_matches = [
+ n for n in nodes
+ if isinstance(n, ast.Assign)
+ and any(isinstance(t, ast.Name) and t.id == var_name for t in n.targets)
+ ]
+ annassign_matches = [
+ n for n in nodes
+ if isinstance(n, ast.AnnAssign)
+ and isinstance(n.target, ast.Name)
+ and n.target.id == var_name
+ ]
+ return bool(assign_matches or annassign_matches)
+
+
+def _has_function(path: Path, func_name: str) -> bool:
+ """Check if migration file has a specific function definition."""
+ tree = _load_migration_module(path)
+ matches = [
+ n for n in ast.walk(tree)
+ if isinstance(n, ast.FunctionDef) and n.name == func_name
+ ]
+ return bool(matches)
+
+
def _find_all_migration_files() -> list[Path]:
"""Find all migration files in versions directory."""
return sorted(MIGRATIONS_DIR.glob("*.py"))
@@ -31,65 +59,27 @@ class TestMigrationStructure:
def test_all_migrations_have_revision(self) -> None:
"""Each migration should have a revision identifier."""
- for path in _find_all_migration_files():
- tree = _load_migration_module(path)
- revision_found = False
- for node in ast.walk(tree):
- # Check both Assign (revision = ...) and AnnAssign (revision: str = ...)
- if isinstance(node, ast.Assign):
- for target in node.targets:
- if isinstance(target, ast.Name) and target.id == "revision":
- revision_found = True
- break
- elif (
- isinstance(node, ast.AnnAssign)
- and isinstance(node.target, ast.Name)
- and node.target.id == "revision"
- ):
- revision_found = True
- assert revision_found, f"Missing 'revision' in {path.name}"
+ missing_revision = [
+ path.name
+ for path in _find_all_migration_files()
+ if not _has_variable(path, "revision")
+ ]
+ assert not missing_revision, f"Missing 'revision' in: {missing_revision}"
def test_all_migrations_have_down_revision(self) -> None:
"""Each migration should have a down_revision."""
- for path in _find_all_migration_files():
- tree = _load_migration_module(path)
- down_revision_found = False
- for node in ast.walk(tree):
- # Check both Assign and AnnAssign
- if isinstance(node, ast.Assign):
- for target in node.targets:
- if isinstance(target, ast.Name) and target.id == "down_revision":
- down_revision_found = True
- break
- elif (
- isinstance(node, ast.AnnAssign)
- and isinstance(node.target, ast.Name)
- and node.target.id == "down_revision"
- ):
- down_revision_found = True
- assert down_revision_found, f"Missing 'down_revision' in {path.name}"
+ missing = [p.name for p in _find_all_migration_files() if not _has_variable(p, "down_revision")]
+ assert not missing, f"Missing 'down_revision' in: {missing}"
def test_all_migrations_have_upgrade_function(self) -> None:
"""Each migration should have an upgrade function."""
- for path in _find_all_migration_files():
- tree = _load_migration_module(path)
- upgrade_found = False
- for node in ast.walk(tree):
- if isinstance(node, ast.FunctionDef) and node.name == "upgrade":
- upgrade_found = True
- break
- assert upgrade_found, f"Missing 'upgrade()' function in {path.name}"
+ missing = [p.name for p in _find_all_migration_files() if not _has_function(p, "upgrade")]
+ assert not missing, f"Missing 'upgrade()' function in: {missing}"
def test_all_migrations_have_downgrade_function(self) -> None:
"""Each migration should have a downgrade function."""
- for path in _find_all_migration_files():
- tree = _load_migration_module(path)
- downgrade_found = False
- for node in ast.walk(tree):
- if isinstance(node, ast.FunctionDef) and node.name == "downgrade":
- downgrade_found = True
- break
- assert downgrade_found, f"Missing 'downgrade()' function in {path.name}"
+ missing = [p.name for p in _find_all_migration_files() if not _has_function(p, "downgrade")]
+ assert not missing, f"Missing 'downgrade()' function in: {missing}"
class TestSprint0Triggers:
diff --git a/tests/infrastructure/summarization/test_cloud_provider.py b/tests/infrastructure/summarization/test_cloud_provider.py
index d20b2b9..fa06fec 100644
--- a/tests/infrastructure/summarization/test_cloud_provider.py
+++ b/tests/infrastructure/summarization/test_cloud_provider.py
@@ -23,6 +23,10 @@ from noteflow.infrastructure.summarization import CloudBackend
from .conftest import build_valid_json_response, create_test_segment
+# Test constants for token usage
+TEST_TOKEN_COUNT = 150
+"""Token count used for mocked LLM response usage metrics."""
+
class TestCloudSummarizerProperties:
"""Tests for CloudSummarizer properties."""
@@ -174,10 +178,10 @@ class TestCloudSummarizerOpenAI:
assert result.summary.action_items == [], "empty segments should yield no action items"
@pytest.mark.asyncio
- async def test_summarize_returns_result(
+ async def test_summarize_returns_result_openai(
self, meeting_id: MeetingId, monkeypatch: pytest.MonkeyPatch
) -> None:
- """Summarize should return SummarizationResult."""
+ """Summarize should return SummarizationResult for OpenAI backend."""
response_content = build_valid_json_response(
summary="Project meeting summary.",
key_points=[{"text": "Key point", "segment_ids": [0]}],
@@ -189,7 +193,7 @@ class TestCloudSummarizerOpenAI:
choices=[
types.SimpleNamespace(message=types.SimpleNamespace(content=response_content))
],
- usage=types.SimpleNamespace(total_tokens=150),
+ usage=types.SimpleNamespace(total_tokens=TEST_TOKEN_COUNT),
)
mock_client = types.SimpleNamespace(
@@ -209,7 +213,7 @@ class TestCloudSummarizerOpenAI:
assert result.provider_name == "openai", "provider_name should be 'openai'"
assert result.summary.executive_summary == "Project meeting summary.", "summary should match"
- assert result.tokens_used == 150, "tokens_used should match response"
+ assert result.tokens_used == TEST_TOKEN_COUNT, "tokens_used should match response"
@pytest.mark.asyncio
async def test_raises_unavailable_on_auth_error(
@@ -237,10 +241,10 @@ class TestCloudSummarizerOpenAI:
await summarizer.summarize(request)
@pytest.mark.asyncio
- async def test_raises_invalid_response_on_empty_content(
+ async def test_raises_invalid_response_on_empty_content_openai(
self, meeting_id: MeetingId, monkeypatch: pytest.MonkeyPatch
) -> None:
- """Should raise InvalidResponseError on empty response."""
+ """Should raise InvalidResponseError on empty response from OpenAI."""
def create_empty_response(**_: object) -> types.SimpleNamespace:
return types.SimpleNamespace(
@@ -271,10 +275,10 @@ class TestCloudSummarizerAnthropic:
"""Tests for CloudSummarizer with Anthropic backend."""
@pytest.mark.asyncio
- async def test_summarize_returns_result(
+ async def test_summarize_returns_result_anthropic(
self, meeting_id: MeetingId, monkeypatch: pytest.MonkeyPatch
) -> None:
- """Summarize should return SummarizationResult."""
+ """Summarize should return SummarizationResult for Anthropic backend."""
response_content = build_valid_json_response(
summary="Anthropic summary.",
key_points=[{"text": "Point", "segment_ids": [0]}],
@@ -301,7 +305,7 @@ class TestCloudSummarizerAnthropic:
assert result.provider_name == "anthropic", "provider_name should be 'anthropic'"
assert result.summary.executive_summary == "Anthropic summary.", "summary should match"
- assert result.tokens_used == 150, "tokens_used should sum input and output"
+ assert result.tokens_used == TEST_TOKEN_COUNT, "tokens_used should sum input and output"
@pytest.mark.asyncio
async def test_raises_unavailable_when_package_missing(
@@ -335,10 +339,10 @@ class TestCloudSummarizerAnthropic:
await summarizer.summarize(request)
@pytest.mark.asyncio
- async def test_raises_invalid_response_on_empty_content(
+ async def test_raises_invalid_response_on_empty_content_anthropic(
self, meeting_id: MeetingId, monkeypatch: pytest.MonkeyPatch
) -> None:
- """Should raise InvalidResponseError on empty response."""
+ """Should raise InvalidResponseError on empty response from Anthropic."""
def create_empty_response(**_: object) -> types.SimpleNamespace:
return types.SimpleNamespace(
diff --git a/tests/infrastructure/test_diarization.py b/tests/infrastructure/test_diarization.py
index 5e5a997..0a2bcb3 100644
--- a/tests/infrastructure/test_diarization.py
+++ b/tests/infrastructure/test_diarization.py
@@ -9,6 +9,16 @@ import pytest
from noteflow.infrastructure.diarization import SpeakerTurn, assign_speaker, assign_speakers_batch
+# Test constants for speaker turn timing
+TURN_END_TIME_SHORT = 15.0
+"""Short turn end time in seconds for testing confidence and turn assignment."""
+
+TURN_END_TIME_MEDIUM = 12.0
+"""Medium turn end time in seconds for testing partial overlaps."""
+
+TURN_END_TIME_LONG = 20.0
+"""Long turn end time in seconds for testing full overlaps and containment."""
+
class TestSpeakerTurn:
"""Tests for the SpeakerTurn dataclass."""
@@ -24,7 +34,7 @@ class TestSpeakerTurn:
def test_create_turn_with_confidence(self) -> None:
"""Create a turn with custom confidence."""
- turn = SpeakerTurn(speaker="SPEAKER_01", start=10.0, end=15.0, confidence=0.85)
+ turn = SpeakerTurn(speaker="SPEAKER_01", start=10.0, end=TURN_END_TIME_SHORT, confidence=0.85)
assert turn.confidence == 0.85, "Custom confidence should be stored correctly"
def test_invalid_end_before_start_raises(self) -> None:
@@ -56,7 +66,7 @@ class TestSpeakerTurn:
assert turn.overlaps(start, end), f"Turn [5.0, 10.0] should overlap with [{start}, {end}]"
@pytest.mark.parametrize(
- "start,end", [(0.0, 5.0), (10.0, 15.0), (0.0, 3.0), (12.0, 20.0)]
+ "start,end", [(0.0, 5.0), (10.0, TURN_END_TIME_SHORT), (0.0, 3.0), (TURN_END_TIME_MEDIUM, TURN_END_TIME_LONG)]
)
def test_overlaps_returns_false(self, start: float, end: float) -> None:
"""overlaps() returns False when ranges don't overlap."""
@@ -66,7 +76,7 @@ class TestSpeakerTurn:
def test_overlap_duration_full_overlap(self) -> None:
"""overlap_duration() for full overlap returns turn duration."""
turn = SpeakerTurn(speaker="SPEAKER_00", start=5.0, end=10.0)
- assert turn.overlap_duration(0.0, 15.0) == 5.0, "Full overlap should return entire turn duration"
+ assert turn.overlap_duration(0.0, TURN_END_TIME_SHORT) == 5.0, "Full overlap should return entire turn duration"
def test_overlap_duration_partial_overlap_left(self) -> None:
"""overlap_duration() for partial overlap on left side."""
@@ -76,18 +86,18 @@ class TestSpeakerTurn:
def test_overlap_duration_partial_overlap_right(self) -> None:
"""overlap_duration() for partial overlap on right side."""
turn = SpeakerTurn(speaker="SPEAKER_00", start=5.0, end=10.0)
- assert turn.overlap_duration(8.0, 15.0) == 2.0, "Right partial overlap [8.0, 15.0] with [5.0, 10.0] should be 2.0"
+ assert turn.overlap_duration(8.0, TURN_END_TIME_SHORT) == 2.0, "Right partial overlap [8.0, 15.0] with [5.0, 10.0] should be 2.0"
def test_overlap_duration_contained(self) -> None:
"""overlap_duration() when range is contained within turn."""
- turn = SpeakerTurn(speaker="SPEAKER_00", start=0.0, end=20.0)
+ turn = SpeakerTurn(speaker="SPEAKER_00", start=0.0, end=TURN_END_TIME_LONG)
assert turn.overlap_duration(5.0, 10.0) == 5.0, "Contained range [5.0, 10.0] within [0.0, 20.0] should return range duration"
def test_overlap_duration_no_overlap(self) -> None:
"""overlap_duration() returns 0.0 when no overlap."""
turn = SpeakerTurn(speaker="SPEAKER_00", start=5.0, end=10.0)
assert turn.overlap_duration(0.0, 3.0) == 0.0, "No overlap before turn should return 0.0"
- assert turn.overlap_duration(12.0, 20.0) == 0.0, "No overlap after turn should return 0.0"
+ assert turn.overlap_duration(TURN_END_TIME_MEDIUM, TURN_END_TIME_LONG) == 0.0, "No overlap after turn should return 0.0"
class TestAssignSpeaker:
@@ -141,7 +151,7 @@ class TestAssignSpeaker:
"""No overlapping turns returns None."""
turns = [
SpeakerTurn(speaker="SPEAKER_00", start=0.0, end=5.0),
- SpeakerTurn(speaker="SPEAKER_01", start=10.0, end=15.0),
+ SpeakerTurn(speaker="SPEAKER_01", start=10.0, end=TURN_END_TIME_SHORT),
]
speaker, confidence = assign_speaker(6.0, 9.0, turns)
assert speaker is None, "No overlapping turns should return None speaker"
diff --git a/tests/infrastructure/test_integration_converters.py b/tests/infrastructure/test_integration_converters.py
index 40b012c..6724614 100644
--- a/tests/infrastructure/test_integration_converters.py
+++ b/tests/infrastructure/test_integration_converters.py
@@ -20,6 +20,19 @@ from noteflow.infrastructure.converters.integration_converters import (
SyncRunConverter,
)
+# Test constants for sync run metrics
+SYNC_RUN_ITEMS_SYNCED = 15
+"""Number of items synced in a standard test sync run fixture."""
+
+SYNC_RUN_DURATION_MS_SHORT = 5000
+"""Short sync run duration in milliseconds (5 seconds)."""
+
+SYNC_RUN_DURATION_MS_MEDIUM = 10000
+"""Medium sync run duration in milliseconds (10 seconds)."""
+
+SYNC_RUN_ITEMS_COMPLETE = 25
+"""Number of items in a complete sync run test case."""
+
class TestIntegrationConverterOrmToDomain:
"""Tests for IntegrationConverter.orm_to_domain."""
@@ -219,9 +232,9 @@ class TestSyncRunConverterOrmToDomain:
model.status = "success"
model.started_at = datetime(2024, 1, 15, 12, 0, 0, tzinfo=UTC)
model.ended_at = datetime(2024, 1, 15, 12, 0, 5, tzinfo=UTC)
- model.duration_ms = 5000
+ model.duration_ms = SYNC_RUN_DURATION_MS_SHORT
model.error_message = None
- model.stats = {"items_synced": 10, "items_total": 15}
+ model.stats = {"items_synced": 10, "items_total": SYNC_RUN_ITEMS_SYNCED}
return model
def test_sync_run_orm_to_domain(
@@ -234,7 +247,7 @@ class TestSyncRunConverterOrmToDomain:
assert result.id == mock_sync_run_model.id, "ID should match"
assert result.integration_id == mock_sync_run_model.integration_id, "Integration ID should match"
assert result.status == SyncRunStatus.SUCCESS, "Status should be enum"
- assert result.duration_ms == 5000, "Duration should match"
+ assert result.duration_ms == SYNC_RUN_DURATION_MS_SHORT, "Duration should match"
assert result.error_message is None, "Error message should be None"
@pytest.mark.parametrize(
@@ -261,7 +274,7 @@ class TestSyncRunConverterOrmToDomain:
result = SyncRunConverter.orm_to_domain(mock_sync_run_model)
assert isinstance(result.stats, dict), "Stats should be dict"
assert result.stats["items_synced"] == 10, "Items synced count should be preserved"
- assert result.stats["items_total"] == 15, "Items total count should be preserved"
+ assert result.stats["items_total"] == SYNC_RUN_ITEMS_SYNCED, "Items total count should be preserved"
def test_handles_none_stats(self, mock_sync_run_model: MagicMock) -> None:
"""None stats in ORM becomes empty dict in domain."""
@@ -291,9 +304,9 @@ class TestSyncRunConverterToOrmKwargs:
status=SyncRunStatus.SUCCESS,
started_at=datetime(2024, 1, 15, 12, 0, 0, tzinfo=UTC),
ended_at=datetime(2024, 1, 15, 12, 0, 10, tzinfo=UTC),
- duration_ms=10000,
+ duration_ms=SYNC_RUN_DURATION_MS_MEDIUM,
error_message=None,
- stats={"items_synced": 25},
+ stats={"items_synced": SYNC_RUN_ITEMS_COMPLETE},
)
result = SyncRunConverter.to_orm_kwargs(sync_run)
@@ -301,8 +314,8 @@ class TestSyncRunConverterToOrmKwargs:
assert result["id"] == sync_run.id, "ID should be preserved"
assert result["integration_id"] == sync_run.integration_id, "Integration ID should be preserved"
assert result["status"] == "success", "Status should be string value"
- assert result["duration_ms"] == 10000, "Duration should be preserved"
- assert result["stats"] == {"items_synced": 25}, "Stats should be preserved"
+ assert result["duration_ms"] == SYNC_RUN_DURATION_MS_MEDIUM, "Duration should be preserved"
+ assert result["stats"] == {"items_synced": SYNC_RUN_ITEMS_COMPLETE}, "Stats should be preserved"
@pytest.mark.parametrize(
("status_enum", "expected_string"),
diff --git a/tests/infrastructure/test_observability.py b/tests/infrastructure/test_observability.py
index 820519e..7b3f830 100644
--- a/tests/infrastructure/test_observability.py
+++ b/tests/infrastructure/test_observability.py
@@ -13,6 +13,10 @@ import pytest
from noteflow.infrastructure.logging.log_buffer import LogBuffer, LogEntry
from noteflow.infrastructure.metrics.collector import MetricsCollector, PerformanceMetrics
+# Test constants for log buffer capacity
+LARGE_DETAILS_KEY_COUNT = 50
+"""Number of keys in a large details dictionary for stress testing."""
+
class TestLogEntry:
"""Test LogEntry dataclass."""
@@ -336,13 +340,13 @@ class TestLogBufferEdgeCases:
def test_log_with_large_details(self) -> None:
"""Buffer handles entries with large details objects."""
buffer = LogBuffer(capacity=10)
- large_details = {f"key_{i}": f"value_{i}" * 100 for i in range(50)}
+ large_details = {f"key_{i}": f"value_{i}" * 100 for i in range(LARGE_DETAILS_KEY_COUNT)}
buffer.log("info", "app", "Large details", large_details)
recent = buffer.get_recent()
assert len(recent) == 1, "Buffer should contain one entry with large details"
- assert len(recent[0].details) == 50, "Entry should retain all 50 detail keys"
+ assert len(recent[0].details) == LARGE_DETAILS_KEY_COUNT, f"Entry should retain all {LARGE_DETAILS_KEY_COUNT} detail keys"
def test_rapid_sequential_logging(self) -> None:
"""Rapid sequential logging maintains order."""
@@ -355,9 +359,8 @@ class TestLogBufferEdgeCases:
recent = buffer.get_recent(limit=50)
# Recent returns newest first, so reverse to check order
- for i, entry in enumerate(reversed(recent)):
- expected_msg = f"Message {i}"
- assert entry.message == expected_msg, f"Expected '{expected_msg}', got '{entry.message}'"
+ actual_messages = [entry.message for entry in reversed(recent)]
+ assert actual_messages == messages, f"Messages should maintain sequential order"
def test_clear_then_append_works(self) -> None:
"""Buffer works correctly after clear."""
@@ -402,11 +405,12 @@ class TestMetricsCollectorEdgeCases:
history = collector.get_history()
- for i in range(len(history) - 1):
- assert history[i].timestamp <= history[i + 1].timestamp, (
- f"Timestamp at {i} ({history[i].timestamp}) is greater than "
- f"timestamp at {i+1} ({history[i + 1].timestamp})"
- )
+ out_of_order = [
+ (i, history[i].timestamp, history[i + 1].timestamp)
+ for i in range(len(history) - 1)
+ if history[i].timestamp > history[i + 1].timestamp
+ ]
+ assert not out_of_order, f"Timestamps should be in chronological order, but found out-of-order pairs: {out_of_order}"
def test_history_eviction_removes_oldest(self) -> None:
"""History eviction removes oldest entries first."""
diff --git a/tests/infrastructure/triggers/test_calendar.py b/tests/infrastructure/triggers/test_calendar.py
index fc9a19a..97e5bde 100644
--- a/tests/infrastructure/triggers/test_calendar.py
+++ b/tests/infrastructure/triggers/test_calendar.py
@@ -14,6 +14,10 @@ from noteflow.infrastructure.triggers.calendar import (
parse_calendar_event_config,
)
+# Test constants for non-iterable input validation
+NON_ITERABLE_TEST_VALUE = 12345
+"""Integer value used to test that non-iterable input returns empty list."""
+
def _settings(**overrides: object) -> CalendarTriggerSettings:
"""Create CalendarTriggerSettings with defaults and overrides."""
@@ -316,4 +320,4 @@ class TestParseCalendarEventConfig:
def test_non_iterable_returns_empty(self) -> None:
"""Non-iterable input should return empty list."""
- assert parse_calendar_event_config(12345) == [], "non-iterable returns empty"
+ assert parse_calendar_event_config(NON_ITERABLE_TEST_VALUE) == [], "non-iterable returns empty"
diff --git a/tests/integration/test_crash_scenarios.py b/tests/integration/test_crash_scenarios.py
index 429f8b6..6c1394c 100644
--- a/tests/integration/test_crash_scenarios.py
+++ b/tests/integration/test_crash_scenarios.py
@@ -352,14 +352,17 @@ class TestRecoveryIdempotence:
# Combined should be exactly the count (first wins, others find nothing)
total_recovered = result1 + result2 + result3
- assert total_recovered == CONCURRENT_RECOVERY_COUNT, "Total recovered should match"
+ assert total_recovered == CONCURRENT_RECOVERY_COUNT, (
+ f"expected {CONCURRENT_RECOVERY_COUNT} total recovered, got {total_recovered}"
+ )
# Verify all in ERROR state
async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
- for mid in meeting_ids:
- recovered = await uow.meetings.get(mid)
- assert recovered is not None, "Recovered meeting should exist"
- assert recovered.state == MeetingState.ERROR, "Recovered meeting state"
+ meetings = [await uow.meetings.get(mid) for mid in meeting_ids]
+ missing = [m for m in meetings if m is None]
+ wrong_state = [m for m in meetings if m is not None and m.state != MeetingState.ERROR]
+ assert not missing, f"All recovered meetings should exist, but {len(missing)} are missing"
+ assert not wrong_state, f"All meetings should be in ERROR state, but {len(wrong_state)} are not"
class TestMixedCrashRecovery:
@@ -485,8 +488,14 @@ class TestRecoveryResult:
recovery_service = RecoveryService(uow=uow, meetings_dir=tmp_path)
result = await recovery_service.recover_all()
- assert result.meetings_recovered == RECORDING_MEETINGS_COUNT
- assert result.diarization_jobs_failed == RUNNING_JOBS_COUNT
+ assert result.meetings_recovered == RECORDING_MEETINGS_COUNT, (
+ f"expected {RECORDING_MEETINGS_COUNT} meetings recovered, "
+ f"got {result.meetings_recovered}"
+ )
+ assert result.diarization_jobs_failed == RUNNING_JOBS_COUNT, (
+ f"expected {RUNNING_JOBS_COUNT} jobs failed, "
+ f"got {result.diarization_jobs_failed}"
+ )
@pytest.mark.integration
@pytest.mark.asyncio
@@ -517,8 +526,12 @@ class TestRecoveryResult:
recovery_service = RecoveryService(uow=uow, meetings_dir=tmp_path)
result = await recovery_service.recover_all()
- assert result.meetings_recovered == 0
- assert result.diarization_jobs_failed == 0
+ assert result.meetings_recovered == 0, (
+ f"expected 0 meetings recovered, got {result.meetings_recovered}"
+ )
+ assert result.diarization_jobs_failed == 0, (
+ f"expected 0 jobs failed, got {result.diarization_jobs_failed}"
+ )
class TestPartialTransactionRecovery:
@@ -570,7 +583,7 @@ class TestPartialTransactionRecovery:
# Simulate interrupted stop: begin_stopping but crash before stop_recording
async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
m = await uow.meetings.get(meeting_id)
- assert m is not None
+ assert m is not None, f"meeting {meeting_id} should exist before state transition"
m.begin_stopping() # State = STOPPING
await uow.meetings.update(m)
await uow.commit()
@@ -584,5 +597,7 @@ class TestPartialTransactionRecovery:
async with SqlAlchemyUnitOfWork(session_factory, tmp_path) as uow:
recovered = await uow.meetings.get(meeting_id)
- assert recovered is not None
- assert recovered.state == MeetingState.ERROR, "Should be marked ERROR"
+ assert recovered is not None, f"meeting {meeting_id} should exist after recovery"
+ assert recovered.state == MeetingState.ERROR, (
+ f"recovered meeting should be ERROR, got {recovered.state}"
+ )
diff --git a/tests/integration/test_database_resilience.py b/tests/integration/test_database_resilience.py
index eb81a2c..3fd4f20 100644
--- a/tests/integration/test_database_resilience.py
+++ b/tests/integration/test_database_resilience.py
@@ -50,8 +50,12 @@ class TestConnectionPoolBehavior:
tasks = [concurrent_operation(i) for i in range(pool_size)]
results = await asyncio.gather(*tasks)
- assert len(results) == pool_size
- assert set(results) == set(range(pool_size))
+ assert len(results) == pool_size, (
+ f"expected {pool_size} results, got {len(results)}"
+ )
+ assert set(results) == set(range(pool_size)), (
+ f"expected result indices {set(range(pool_size))}, got {set(results)}"
+ )
@pytest.mark.integration
@pytest.mark.asyncio
@@ -80,7 +84,9 @@ class TestConnectionPoolBehavior:
timeout=POOL_TIMEOUT_SECONDS,
)
- assert len(results) == pool_size + overflow
+ assert len(results) == pool_size + overflow, (
+ f"expected {pool_size + overflow} results, got {len(results)}"
+ )
@pytest.mark.integration
@pytest.mark.asyncio
@@ -119,7 +125,9 @@ class TestTransactionRollback:
# Verify not persisted
async with SqlAlchemyUnitOfWork(session_factory, ".") as uow:
retrieved = await uow.meetings.get(meeting_id)
- assert retrieved is None
+ assert retrieved is None, (
+ f"meeting {meeting_id} should not persist without commit"
+ )
@pytest.mark.integration
@pytest.mark.asyncio
@@ -139,7 +147,9 @@ class TestTransactionRollback:
# Verify not persisted
async with SqlAlchemyUnitOfWork(session_factory, ".") as uow:
retrieved = await uow.meetings.get(meeting_id)
- assert retrieved is None
+ assert retrieved is None, (
+ f"meeting {meeting_id} should not persist after explicit rollback"
+ )
@pytest.mark.integration
@pytest.mark.asyncio
@@ -159,8 +169,12 @@ class TestTransactionRollback:
# Verify persisted
async with SqlAlchemyUnitOfWork(session_factory, ".") as uow:
retrieved = await uow.meetings.get(meeting_id)
- assert retrieved is not None
- assert retrieved.title == "Commit Test"
+ assert retrieved is not None, (
+ f"meeting {meeting_id} should persist after commit"
+ )
+ assert retrieved.title == "Commit Test", (
+ f"expected title 'Commit Test', got '{retrieved.title}'"
+ )
class TestTransactionIsolation:
@@ -228,13 +242,16 @@ class TestTransactionIsolation:
ids = await asyncio.gather(*[create_meeting(i) for i in range(10)])
# All should have unique IDs
- assert len(set(ids)) == 10
+ assert len(set(ids)) == 10, (
+ f"expected 10 unique IDs, got {len(set(ids))} unique from {ids}"
+ )
# All should be persisted
async with SqlAlchemyUnitOfWork(session_factory, ".") as uow:
- for mid in ids:
- m = await uow.meetings.get(mid)
- assert m is not None
+ missing_ids = [
+ mid for mid in ids if await uow.meetings.get(mid) is None
+ ]
+ assert not missing_ids, f"meetings not persisted: {missing_ids}"
class TestDatabaseReconnection:
@@ -258,7 +275,7 @@ class TestDatabaseReconnection:
# Second operation should succeed (pool_pre_ping handles stale connections)
async with SqlAlchemyUnitOfWork(session_factory, ".") as uow:
count = await uow.meetings.count_by_state(MeetingState.CREATED)
- assert count >= 0
+ assert count >= 0, f"expected non-negative count, got {count}"
@pytest.mark.integration
@pytest.mark.asyncio
@@ -278,7 +295,10 @@ class TestDatabaseReconnection:
# Session 2: Should not see uncommitted changes
async with SqlAlchemyUnitOfWork(session_factory, ".") as uow2:
found = await uow2.meetings.get(meeting_id)
- assert found is None # Not visible in other session
+ assert found is None, (
+ f"meeting {meeting_id} should not be visible in other session "
+ "before commit"
+ )
# Still not committed in uow1
@@ -317,7 +337,9 @@ class TestSegmentOperations:
# Verify all segments persisted
async with SqlAlchemyUnitOfWork(session_factory, ".") as uow:
segments = await uow.segments.get_by_meeting(str(meeting_id))
- assert len(segments) == BULK_SEGMENT_COUNT
+ assert len(segments) == BULK_SEGMENT_COUNT, (
+ f"expected {BULK_SEGMENT_COUNT} segments, got {len(segments)}"
+ )
class TestDatabaseFailureChaos:
diff --git a/tests/integration/test_diarization_job_repository.py b/tests/integration/test_diarization_job_repository.py
index 6cd0106..fababad 100644
--- a/tests/integration/test_diarization_job_repository.py
+++ b/tests/integration/test_diarization_job_repository.py
@@ -78,7 +78,7 @@ class TestDiarizationJobRepository:
result = await job_repo.get("nonexistent-job-id")
- assert result is None
+ assert result is None, "get should return None for nonexistent job ID"
async def test_update_status_to_running(self, session: AsyncSession) -> None:
"""Test updating job status from QUEUED to RUNNING."""
@@ -100,11 +100,11 @@ class TestDiarizationJobRepository:
updated = await job_repo.update_status(job.job_id, JOB_STATUS_RUNNING)
await session.commit()
- assert updated is True
+ assert updated is True, "update_status should return True on success"
retrieved = await job_repo.get(job.job_id)
- assert retrieved is not None
- assert retrieved.status == JOB_STATUS_RUNNING
+ assert retrieved is not None, "job should still be retrievable after status update"
+ assert retrieved.status == JOB_STATUS_RUNNING, f"expected RUNNING status, got {retrieved.status}"
async def test_update_status_to_completed_with_results(self, session: AsyncSession) -> None:
"""Test updating job status to COMPLETED with segments and speakers."""
@@ -180,7 +180,7 @@ class TestDiarizationJobRepository:
result = await job_repo.update_status("nonexistent", JOB_STATUS_RUNNING)
await session.commit()
- assert result is False
+ assert result is False, "update_status should return False for nonexistent job"
async def test_list_for_meeting_returns_jobs_in_order(self, session: AsyncSession) -> None:
"""Test listing jobs for a meeting returns them newest first."""
@@ -204,8 +204,8 @@ class TestDiarizationJobRepository:
jobs = await job_repo.list_for_meeting(str(meeting.id))
- assert len(jobs) == 3
- assert [j.job_id for j in jobs] == list(reversed(job_ids))
+ assert len(jobs) == 3, f"expected 3 jobs for meeting, got {len(jobs)}"
+ assert [j.job_id for j in jobs] == list(reversed(job_ids)), "jobs should be returned in newest-first order"
async def test_list_for_meeting_excludes_other_meetings(self, session: AsyncSession) -> None:
"""Test listing jobs only returns jobs for the specified meeting."""
@@ -234,8 +234,8 @@ class TestDiarizationJobRepository:
jobs = await job_repo.list_for_meeting(str(meeting1.id))
- assert len(jobs) == 1
- assert jobs[0].job_id == job1.job_id
+ assert len(jobs) == 1, f"expected 1 job for meeting1, got {len(jobs)}"
+ assert jobs[0].job_id == job1.job_id, "returned job should be from meeting1, not meeting2"
@pytest.mark.integration
@@ -352,11 +352,11 @@ class TestDiarizationJobCrashRecovery:
failed_count = await job_repo.mark_running_as_failed("New crash")
await session.commit()
- assert failed_count == 0
+ assert failed_count == 0, "should not mark already FAILED jobs"
retrieved = await job_repo.get(job.job_id)
- assert retrieved is not None
- assert retrieved.error_message == "Original error"
+ assert retrieved is not None, "failed job should still be retrievable"
+ assert retrieved.error_message == "Original error", "original error message should be preserved"
async def test_mark_running_as_failed_handles_multiple_jobs(
self, session: AsyncSession
@@ -433,8 +433,8 @@ class TestDiarizationJobPruning:
pruned = await job_repo.prune_completed(ttl_seconds=3600)
await session.commit()
- assert pruned == 1
- assert await job_repo.get(job.job_id) is None
+ assert pruned == 1, f"expected 1 old completed job pruned, got {pruned}"
+ assert await job_repo.get(job.job_id) is None, "old completed job should be deleted after pruning"
async def test_prune_completed_removes_old_failed_jobs(
self, session: AsyncSession
@@ -461,7 +461,7 @@ class TestDiarizationJobPruning:
pruned = await job_repo.prune_completed(ttl_seconds=3600)
await session.commit()
- assert pruned == 1
+ assert pruned == 1, f"expected 1 old failed job pruned, got {pruned}"
async def test_prune_completed_keeps_recent_jobs(self, session: AsyncSession) -> None:
"""Test pruning keeps jobs within TTL window."""
@@ -483,8 +483,8 @@ class TestDiarizationJobPruning:
pruned = await job_repo.prune_completed(ttl_seconds=3600)
await session.commit()
- assert pruned == 0
- assert await job_repo.get(job.job_id) is not None
+ assert pruned == 0, "recent jobs should not be pruned"
+ assert await job_repo.get(job.job_id) is not None, "recent job should still exist after pruning"
async def test_prune_completed_keeps_running_jobs(self, session: AsyncSession) -> None:
"""Test pruning never removes RUNNING or QUEUED jobs."""
@@ -517,9 +517,9 @@ class TestDiarizationJobPruning:
pruned = await job_repo.prune_completed(ttl_seconds=0)
await session.commit()
- assert pruned == 0
- assert await job_repo.get(running_job.job_id) is not None
- assert await job_repo.get(queued_job.job_id) is not None
+ assert pruned == 0, "RUNNING and QUEUED jobs should never be pruned"
+ assert await job_repo.get(running_job.job_id) is not None, "old RUNNING job should still exist"
+ assert await job_repo.get(queued_job.job_id) is not None, "old QUEUED job should still exist"
@pytest.mark.integration
@@ -568,7 +568,7 @@ class TestStreamingDiarizationTurns:
turns = await job_repo.get_streaming_turns(str(meeting.id))
- assert turns == []
+ assert turns == [], "should return empty list for meeting with no turns"
async def test_add_streaming_turns_empty_list_returns_zero(
self, session: AsyncSession
@@ -584,7 +584,7 @@ class TestStreamingDiarizationTurns:
count = await job_repo.add_streaming_turns(str(meeting.id), [])
await session.commit()
- assert count == 0
+ assert count == 0, "adding empty turns list should return 0"
async def test_streaming_turns_ordered_by_start_time(self, session: AsyncSession) -> None:
"""Test turns are returned ordered by start_time regardless of insertion order."""
@@ -605,8 +605,8 @@ class TestStreamingDiarizationTurns:
retrieved = await job_repo.get_streaming_turns(str(meeting.id))
- assert len(retrieved) == 3
- assert [t.start_time for t in retrieved] == [0.0, 3.0, 5.0]
+ assert len(retrieved) == 3, f"expected 3 turns retrieved, got {len(retrieved)}"
+ assert [t.start_time for t in retrieved] == [0.0, 3.0, 5.0], "turns should be ordered by start_time"
async def test_clear_streaming_turns_removes_all(self, session: AsyncSession) -> None:
"""Test clearing streaming turns removes all turns for a meeting."""
@@ -627,10 +627,10 @@ class TestStreamingDiarizationTurns:
deleted = await job_repo.clear_streaming_turns(str(meeting.id))
await session.commit()
- assert deleted == 2
+ assert deleted == 2, f"expected 2 turns deleted, got {deleted}"
remaining = await job_repo.get_streaming_turns(str(meeting.id))
- assert remaining == []
+ assert remaining == [], "no turns should remain after clear"
async def test_clear_streaming_turns_isolates_meetings(self, session: AsyncSession) -> None:
"""Test clearing turns for one meeting doesn't affect others."""
@@ -652,10 +652,10 @@ class TestStreamingDiarizationTurns:
await job_repo.clear_streaming_turns(str(meeting1.id))
await session.commit()
- assert await job_repo.get_streaming_turns(str(meeting1.id)) == []
+ assert await job_repo.get_streaming_turns(str(meeting1.id)) == [], "meeting1 turns should be cleared"
remaining = await job_repo.get_streaming_turns(str(meeting2.id))
- assert len(remaining) == 1
- assert remaining[0].speaker == "S2"
+ assert len(remaining) == 1, "meeting2 should still have its turn"
+ assert remaining[0].speaker == "S2", f"meeting2 turn should have speaker S2, got {remaining[0].speaker}"
async def test_streaming_turns_deleted_on_meeting_cascade(
self, session: AsyncSession
@@ -676,7 +676,7 @@ class TestStreamingDiarizationTurns:
await session.commit()
remaining = await job_repo.get_streaming_turns(str(meeting.id))
- assert remaining == []
+ assert remaining == [], "turns should be cascade deleted when meeting is deleted"
@pytest.mark.integration
@@ -703,7 +703,7 @@ class TestDiarizationJobCascadeDelete:
await meeting_repo.delete(meeting.id)
await session.commit()
- assert await job_repo.get(job.job_id) is None
+ assert await job_repo.get(job.job_id) is None, "job should be cascade deleted when meeting is deleted"
@pytest.mark.integration
@@ -731,9 +731,9 @@ class TestDiarizationJobConcurrency:
active = await job_repo.get_active_for_meeting(str(meeting.id))
- assert active is not None
- assert active.job_id == job.job_id
- assert active.status == JOB_STATUS_QUEUED
+ assert active is not None, "get_active_for_meeting should return QUEUED job"
+ assert active.job_id == job.job_id, "returned job_id should match the created job"
+ assert active.status == JOB_STATUS_QUEUED, f"expected QUEUED status, got {active.status}"
async def test_get_active_for_meeting_returns_running_job(
self, session: AsyncSession
@@ -756,9 +756,9 @@ class TestDiarizationJobConcurrency:
active = await job_repo.get_active_for_meeting(str(meeting.id))
- assert active is not None
- assert active.job_id == job.job_id
- assert active.status == JOB_STATUS_RUNNING
+ assert active is not None, "get_active_for_meeting should return RUNNING job"
+ assert active.job_id == job.job_id, "returned job_id should match the created job"
+ assert active.status == JOB_STATUS_RUNNING, f"expected RUNNING status, got {active.status}"
async def test_get_active_for_meeting_returns_none_for_completed(
self, session: AsyncSession
@@ -781,7 +781,7 @@ class TestDiarizationJobConcurrency:
active = await job_repo.get_active_for_meeting(str(meeting.id))
- assert active is None
+ assert active is None, "get_active_for_meeting should return None for COMPLETED job"
async def test_get_active_for_meeting_returns_none_for_cancelled(
self, session: AsyncSession
@@ -804,7 +804,7 @@ class TestDiarizationJobConcurrency:
active = await job_repo.get_active_for_meeting(str(meeting.id))
- assert active is None
+ assert active is None, "get_active_for_meeting should return None for CANCELLED job"
async def test_get_active_for_meeting_returns_most_recent(
self, session: AsyncSession
@@ -835,8 +835,8 @@ class TestDiarizationJobConcurrency:
active = await job_repo.get_active_for_meeting(str(meeting.id))
- assert active is not None
- assert active.job_id == job2.job_id
+ assert active is not None, "should return an active job when multiple exist"
+ assert active.job_id == job2.job_id, "should return the most recently created active job"
@pytest.mark.integration
@@ -863,11 +863,11 @@ class TestDiarizationJobCancelledStatus:
updated = await job_repo.update_status(job.job_id, JOB_STATUS_CANCELLED)
await session.commit()
- assert updated is True
+ assert updated is True, "update_status to CANCELLED should return True"
retrieved = await job_repo.get(job.job_id)
- assert retrieved is not None
- assert retrieved.status == JOB_STATUS_CANCELLED
+ assert retrieved is not None, "cancelled job should still be retrievable"
+ assert retrieved.status == JOB_STATUS_CANCELLED, f"expected CANCELLED status, got {retrieved.status}"
async def test_prune_includes_cancelled_jobs(self, session: AsyncSession) -> None:
"""Test pruning removes old CANCELLED jobs along with COMPLETED/FAILED."""
@@ -892,8 +892,8 @@ class TestDiarizationJobCancelledStatus:
pruned = await job_repo.prune_completed(ttl_seconds=3600)
await session.commit()
- assert pruned == 1
- assert await job_repo.get(job.job_id) is None
+ assert pruned == 1, f"expected 1 old CANCELLED job pruned, got {pruned}"
+ assert await job_repo.get(job.job_id) is None, "old CANCELLED job should be deleted after pruning"
async def test_mark_running_ignores_cancelled_jobs(
self, session: AsyncSession
@@ -917,8 +917,8 @@ class TestDiarizationJobCancelledStatus:
failed_count = await job_repo.mark_running_as_failed()
await session.commit()
- assert failed_count == 0
+ assert failed_count == 0, "crash recovery should not affect CANCELLED jobs"
retrieved = await job_repo.get(job.job_id)
- assert retrieved is not None
- assert retrieved.status == JOB_STATUS_CANCELLED
+ assert retrieved is not None, "cancelled job should still be retrievable"
+ assert retrieved.status == JOB_STATUS_CANCELLED, "CANCELLED status should remain unchanged"
diff --git a/tests/integration/test_e2e_annotations.py b/tests/integration/test_e2e_annotations.py
index d85c2f7..18587b5 100644
--- a/tests/integration/test_e2e_annotations.py
+++ b/tests/integration/test_e2e_annotations.py
@@ -32,6 +32,7 @@ if TYPE_CHECKING:
# Annotation timestamps
ANNOTATION_START_TIME = 10.5
+ANNOTATION_END_TIME_SECONDS = 15.0
class MockContext:
@@ -71,24 +72,24 @@ class TestAnnotationCRUD:
annotation_type=noteflow_pb2.ANNOTATION_TYPE_NOTE,
text="Important point discussed",
start_time=ANNOTATION_START_TIME,
- end_time=15.0,
+ end_time=ANNOTATION_END_TIME_SECONDS,
segment_ids=[0, 1, 2],
)
result = await servicer.AddAnnotation(request, MockContext())
- assert result.id
- assert result.text == "Important point discussed"
- assert result.annotation_type == noteflow_pb2.ANNOTATION_TYPE_NOTE
- assert result.start_time == pytest.approx(ANNOTATION_START_TIME)
- assert result.end_time == pytest.approx(15.0)
- assert list(result.segment_ids) == [0, 1, 2]
+ assert result.id, "annotation ID should be assigned"
+ assert result.text == "Important point discussed", f"expected annotation text 'Important point discussed', got {result.text!r}"
+ assert result.annotation_type == noteflow_pb2.ANNOTATION_TYPE_NOTE, f"expected annotation type ANNOTATION_TYPE_NOTE, got {result.annotation_type}"
+ assert result.start_time == pytest.approx(ANNOTATION_START_TIME), f"expected start_time {ANNOTATION_START_TIME}, got {result.start_time}"
+ assert result.end_time == pytest.approx(ANNOTATION_END_TIME_SECONDS), f"expected end_time {ANNOTATION_END_TIME_SECONDS}, got {result.end_time}"
+ assert list(result.segment_ids) == [0, 1, 2], f"expected segment_ids [0, 1, 2], got {list(result.segment_ids)}"
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
from noteflow.domain.value_objects import AnnotationId
saved = await uow.annotations.get(AnnotationId(result.id))
- assert saved is not None
- assert saved.text == "Important point discussed"
+ assert saved is not None, "annotation should be persisted in database"
+ assert saved.text == "Important point discussed", f"expected persisted text 'Important point discussed', got {saved.text!r}"
async def test_get_annotation_retrieves_from_database(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -113,9 +114,9 @@ class TestAnnotationCRUD:
get_request = noteflow_pb2.GetAnnotationRequest(annotation_id=added.id)
result = await servicer.GetAnnotation(get_request, MockContext())
- assert result.id == added.id
- assert result.text == "Follow up on this"
- assert result.annotation_type == noteflow_pb2.ANNOTATION_TYPE_ACTION_ITEM
+ assert result.id == added.id, f"expected annotation ID {added.id}, got {result.id}"
+ assert result.text == "Follow up on this", f"expected text 'Follow up on this', got {result.text!r}"
+ assert result.annotation_type == noteflow_pb2.ANNOTATION_TYPE_ACTION_ITEM, f"expected annotation type ANNOTATION_TYPE_ACTION_ITEM, got {result.annotation_type}"
async def test_list_annotations_for_meeting(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -141,7 +142,7 @@ class TestAnnotationCRUD:
list_request = noteflow_pb2.ListAnnotationsRequest(meeting_id=str(meeting.id))
result = await servicer.ListAnnotations(list_request, MockContext())
- assert len(result.annotations) == 3
+ assert len(result.annotations) == 3, f"expected 3 annotations, got {len(result.annotations)}"
async def test_list_annotations_with_time_range_filter(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -177,7 +178,7 @@ class TestAnnotationCRUD:
)
result = await servicer.ListAnnotations(list_request, MockContext())
- assert len(result.annotations) == 2
+ assert len(result.annotations) == 2, f"expected 2 annotations in time range 10-50, got {len(result.annotations)}"
async def test_update_annotation_modifies_database(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -206,15 +207,15 @@ class TestAnnotationCRUD:
)
result = await servicer.UpdateAnnotation(update_request, MockContext())
- assert result.text == "Updated text"
- assert result.annotation_type == noteflow_pb2.ANNOTATION_TYPE_ACTION_ITEM
+ assert result.text == "Updated text", f"expected updated text 'Updated text', got {result.text!r}"
+ assert result.annotation_type == noteflow_pb2.ANNOTATION_TYPE_ACTION_ITEM, f"expected updated type ANNOTATION_TYPE_ACTION_ITEM, got {result.annotation_type}"
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
from noteflow.domain.value_objects import AnnotationId
saved = await uow.annotations.get(AnnotationId(added.id))
- assert saved is not None
- assert saved.text == "Updated text"
+ assert saved is not None, "updated annotation should exist in database"
+ assert saved.text == "Updated text", f"expected database text 'Updated text', got {saved.text!r}"
async def test_delete_annotation_removes_from_database(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -239,13 +240,13 @@ class TestAnnotationCRUD:
delete_request = noteflow_pb2.DeleteAnnotationRequest(annotation_id=added.id)
result = await servicer.DeleteAnnotation(delete_request, MockContext())
- assert result.success is True
+ assert result.success is True, "delete operation should return success=True"
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
from noteflow.domain.value_objects import AnnotationId
deleted = await uow.annotations.get(AnnotationId(added.id))
- assert deleted is None
+ assert deleted is None, "annotation should be removed from database after deletion"
@pytest.mark.integration
@@ -272,7 +273,7 @@ class TestAnnotationTypes:
)
result = await servicer.AddAnnotation(request, MockContext())
- assert result.annotation_type == noteflow_pb2.ANNOTATION_TYPE_NOTE
+ assert result.annotation_type == noteflow_pb2.ANNOTATION_TYPE_NOTE, f"expected ANNOTATION_TYPE_NOTE, got {result.annotation_type}"
async def test_action_item_annotation_type(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -294,7 +295,7 @@ class TestAnnotationTypes:
)
result = await servicer.AddAnnotation(request, MockContext())
- assert result.annotation_type == noteflow_pb2.ANNOTATION_TYPE_ACTION_ITEM
+ assert result.annotation_type == noteflow_pb2.ANNOTATION_TYPE_ACTION_ITEM, f"expected ANNOTATION_TYPE_ACTION_ITEM, got {result.annotation_type}"
async def test_decision_annotation_type(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -316,7 +317,7 @@ class TestAnnotationTypes:
)
result = await servicer.AddAnnotation(request, MockContext())
- assert result.annotation_type == noteflow_pb2.ANNOTATION_TYPE_DECISION
+ assert result.annotation_type == noteflow_pb2.ANNOTATION_TYPE_DECISION, f"expected ANNOTATION_TYPE_DECISION, got {result.annotation_type}"
@pytest.mark.integration
@@ -341,7 +342,7 @@ class TestAnnotationErrors:
with pytest.raises(grpc.RpcError, match=".*"):
await servicer.AddAnnotation(request, context)
- assert context.abort_code == grpc.StatusCode.INVALID_ARGUMENT
+ assert context.abort_code == grpc.StatusCode.INVALID_ARGUMENT, f"expected INVALID_ARGUMENT status code, got {context.abort_code}"
async def test_get_annotation_not_found(
self, session_factory: async_sessionmaker[AsyncSession]
@@ -355,7 +356,7 @@ class TestAnnotationErrors:
with pytest.raises(grpc.RpcError, match=".*"):
await servicer.GetAnnotation(request, context)
- assert context.abort_code == grpc.StatusCode.NOT_FOUND
+ assert context.abort_code == grpc.StatusCode.NOT_FOUND, f"expected NOT_FOUND status code for nonexistent annotation, got {context.abort_code}"
async def test_update_annotation_not_found(
self, session_factory: async_sessionmaker[AsyncSession]
@@ -372,7 +373,7 @@ class TestAnnotationErrors:
with pytest.raises(grpc.RpcError, match=".*"):
await servicer.UpdateAnnotation(request, context)
- assert context.abort_code == grpc.StatusCode.NOT_FOUND
+ assert context.abort_code == grpc.StatusCode.NOT_FOUND, f"expected NOT_FOUND status code for updating nonexistent annotation, got {context.abort_code}"
async def test_delete_annotation_not_found_e2e(
self, session_factory: async_sessionmaker[AsyncSession]
@@ -386,7 +387,7 @@ class TestAnnotationErrors:
with pytest.raises(grpc.RpcError, match=".*"):
await servicer.DeleteAnnotation(request, context)
- assert context.abort_code == grpc.StatusCode.NOT_FOUND
+ assert context.abort_code == grpc.StatusCode.NOT_FOUND, f"expected NOT_FOUND status code for deleting nonexistent annotation, got {context.abort_code}"
@pytest.mark.integration
@@ -427,8 +428,8 @@ class TestAnnotationIsolation:
list_request = noteflow_pb2.ListAnnotationsRequest(meeting_id=str(meeting1.id))
result = await servicer.ListAnnotations(list_request, MockContext())
- assert len(result.annotations) == 1
- assert result.annotations[0].text == "Meeting 1 annotation"
+ assert len(result.annotations) == 1, f"expected 1 annotation for meeting 1, got {len(result.annotations)}"
+ assert result.annotations[0].text == "Meeting 1 annotation", f"expected 'Meeting 1 annotation', got {result.annotations[0].text!r}"
async def test_annotations_deleted_with_meeting(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -460,4 +461,4 @@ class TestAnnotationIsolation:
with pytest.raises(grpc.RpcError, match=".*"):
await servicer.GetAnnotation(get_request, context)
- assert context.abort_code == grpc.StatusCode.NOT_FOUND
+ assert context.abort_code == grpc.StatusCode.NOT_FOUND, f"expected NOT_FOUND for annotation after meeting deletion, got {context.abort_code}"
diff --git a/tests/integration/test_e2e_export.py b/tests/integration/test_e2e_export.py
index 6e673bf..660f229 100644
--- a/tests/integration/test_e2e_export.py
+++ b/tests/integration/test_e2e_export.py
@@ -133,10 +133,12 @@ class TestExportServiceDatabase:
export_service = ExportService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
content = await export_service.export_transcript(meeting.id, ExportFormat.HTML)
- assert isinstance(content, str)
+ assert isinstance(content, str), f"HTML export should return string, got {type(content).__name__}"
- assert "= 1
+ assert len(segments) >= 1, (
+ f"expected at least 1 segment persisted to database, got {len(segments)}"
+ )
segment_texts = [s.text for s in segments]
- assert "Hello world" in segment_texts
+ assert "Hello world" in segment_texts, (
+ f"expected 'Hello world' in segment texts, got {segment_texts}"
+ )
@pytest.mark.integration
@@ -306,7 +320,9 @@ class TestStreamStateManagement:
meeting = Meeting.create(title="State Test")
await uow.meetings.create(meeting)
await uow.commit()
- assert meeting.state == MeetingState.CREATED
+ assert meeting.state == MeetingState.CREATED, (
+ f"expected initial meeting state CREATED, got {meeting.state}"
+ )
mock_asr = MagicMock()
mock_asr.is_loaded = True
@@ -325,8 +341,10 @@ class TestStreamStateManagement:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
m = await uow.meetings.get(meeting.id)
- assert m is not None
- assert m.state == MeetingState.RECORDING
+ assert m is not None, f"meeting {meeting.id} should exist in database after stream"
+ assert m.state == MeetingState.RECORDING, (
+ f"expected meeting state RECORDING after stream start, got {m.state}"
+ )
async def test_concurrent_streams_rejected(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -358,7 +376,9 @@ class TestStreamStateManagement:
async for _ in servicer.StreamTranscription(chunk_iter(), context):
pass
- assert context.abort_code == grpc.StatusCode.FAILED_PRECONDITION
+ assert context.abort_code == grpc.StatusCode.FAILED_PRECONDITION, (
+ f"expected FAILED_PRECONDITION for concurrent stream, got {context.abort_code}"
+ )
@pytest.mark.integration
@@ -389,7 +409,9 @@ class TestStreamCleanup:
async for _ in servicer.StreamTranscription(chunk_iter(), MockContext()):
pass
- assert str(meeting.id) not in servicer._active_streams
+ assert str(meeting.id) not in servicer._active_streams, (
+ f"meeting {meeting.id} should be removed from active streams after completion"
+ )
async def test_streaming_state_cleaned_up_on_error(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -419,7 +441,9 @@ class TestStreamCleanup:
pass # Expected: mock_asr.transcribe_async raises RuntimeError("ASR failed")
meeting_id_str = str(meeting.id)
- assert meeting_id_str not in servicer._active_streams
+ assert meeting_id_str not in servicer._active_streams, (
+ f"meeting {meeting_id_str} should be removed from active streams after error cleanup"
+ )
@pytest.mark.integration
@@ -459,4 +483,6 @@ class TestStreamStopRequest:
async for _ in servicer.StreamTranscription(chunk_iter(), MockContext()):
pass
- assert chunks_processed <= 5
+ assert chunks_processed <= 5, (
+ f"expected stream to stop after ~3 chunks due to stop request, but processed {chunks_processed}"
+ )
diff --git a/tests/integration/test_e2e_summarization.py b/tests/integration/test_e2e_summarization.py
index c6af375..6a6780b 100644
--- a/tests/integration/test_e2e_summarization.py
+++ b/tests/integration/test_e2e_summarization.py
@@ -87,8 +87,10 @@ class TestSummarizationGeneration:
await servicer.GenerateSummary(request, MockContext())
style_prompt = captured[0]
- assert style_prompt is not None
- assert all(kw in style_prompt.lower() for kw in ("formal", "bullet", "comprehensive"))
+ assert style_prompt is not None, "style_prompt should be set when options are provided"
+ assert all(
+ kw in style_prompt.lower() for kw in ("formal", "bullet", "comprehensive")
+ ), f"style_prompt should contain tone/format/verbosity keywords, got: {style_prompt}"
async def test_generate_summary_without_options_passes_none(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -106,7 +108,7 @@ class TestSummarizationGeneration:
noteflow_pb2.GenerateSummaryRequest(meeting_id=str(meeting.id)), MockContext()
)
- assert captured[0] is None
+ assert captured[0] is None, f"style_prompt should be None when no options provided, got: {captured[0]}"
async def test_generate_summary_with_summarization_service(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -131,8 +133,10 @@ class TestSummarizationGeneration:
assert len(result.action_items) == 1, "Should have 1 action item"
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
saved = await uow.summaries.get_by_meeting(meeting.id)
- assert saved is not None
- assert saved.executive_summary == "This meeting discussed important content."
+ assert saved is not None, "Summary should be persisted to database"
+ assert (
+ saved.executive_summary == "This meeting discussed important content."
+ ), f"expected 'This meeting discussed important content.', got '{saved.executive_summary}'"
async def _add_test_segments(
self, uow: SqlAlchemyUnitOfWork, meeting_id: MeetingId, count: int
@@ -200,7 +204,9 @@ class TestSummarizationGeneration:
)
result = await servicer.GenerateSummary(request, MockContext())
- assert result.executive_summary == "Existing summary content"
+ assert (
+ result.executive_summary == "Existing summary content"
+ ), f"expected 'Existing summary content', got '{result.executive_summary}'"
mock_service.summarize.assert_not_called()
async def test_generate_summary_regenerates_with_force_flag(
@@ -251,7 +257,9 @@ class TestSummarizationGeneration:
)
result = await servicer.GenerateSummary(request, MockContext())
- assert result.executive_summary == "New regenerated summary"
+ assert (
+ result.executive_summary == "New regenerated summary"
+ ), f"expected 'New regenerated summary', got '{result.executive_summary}'"
mock_service.summarize.assert_called_once()
async def test_generate_summary_placeholder_fallback(
@@ -280,8 +288,12 @@ class TestSummarizationGeneration:
request = noteflow_pb2.GenerateSummaryRequest(meeting_id=str(meeting.id))
result = await servicer.GenerateSummary(request, MockContext())
- assert "Segment 0" in result.executive_summary or "Segment 1" in result.executive_summary
- assert result.model_version == "placeholder/v0"
+ assert (
+ "Segment 0" in result.executive_summary or "Segment 1" in result.executive_summary
+ ), f"placeholder summary should contain segment text, got: {result.executive_summary}"
+ assert (
+ result.model_version == "placeholder/v0"
+ ), f"expected model_version 'placeholder/v0', got '{result.model_version}'"
async def test_generate_summary_placeholder_on_service_error(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -315,8 +327,12 @@ class TestSummarizationGeneration:
request = noteflow_pb2.GenerateSummaryRequest(meeting_id=str(meeting.id))
result = await servicer.GenerateSummary(request, MockContext())
- assert "Content that should appear" in result.executive_summary
- assert result.model_version == "placeholder/v0"
+ assert (
+ "Content that should appear" in result.executive_summary
+ ), f"placeholder summary should contain segment text, got: {result.executive_summary}"
+ assert (
+ result.model_version == "placeholder/v0"
+ ), f"expected model_version 'placeholder/v0', got '{result.model_version}'"
@pytest.mark.integration
@@ -365,9 +381,16 @@ class TestSummarizationPersistence:
)
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
saved = await uow.summaries.get_by_meeting(meeting.id)
- assert saved is not None and len(saved.key_points) == 3
- assert saved.key_points[0].text == "Key point 1"
- assert saved.key_points[1].segment_ids == [1, 2]
+ assert saved is not None, "Summary should be persisted to database"
+ assert (
+ len(saved.key_points) == 3
+ ), f"expected 3 key points, got {len(saved.key_points)}"
+ assert (
+ saved.key_points[0].text == "Key point 1"
+ ), f"expected first key point text 'Key point 1', got '{saved.key_points[0].text}'"
+ assert (
+ saved.key_points[1].segment_ids == [1, 2]
+ ), f"expected second key point segment_ids [1, 2], got {saved.key_points[1].segment_ids}"
async def test_summary_with_action_items_persisted(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -485,7 +508,9 @@ class TestSummarizationErrors:
with pytest.raises(grpc.RpcError, match=".*"):
await servicer.GenerateSummary(request, context)
- assert context.abort_code == grpc.StatusCode.NOT_FOUND
+ assert (
+ context.abort_code == grpc.StatusCode.NOT_FOUND
+ ), f"expected NOT_FOUND status, got {context.abort_code}"
async def test_generate_summary_invalid_meeting_id(
self, session_factory: async_sessionmaker[AsyncSession]
@@ -499,7 +524,9 @@ class TestSummarizationErrors:
with pytest.raises(grpc.RpcError, match=".*"):
await servicer.GenerateSummary(request, context)
- assert context.abort_code == grpc.StatusCode.INVALID_ARGUMENT
+ assert (
+ context.abort_code == grpc.StatusCode.INVALID_ARGUMENT
+ ), f"expected INVALID_ARGUMENT status, got {context.abort_code}"
async def test_generate_summary_empty_transcript(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -515,4 +542,6 @@ class TestSummarizationErrors:
request = noteflow_pb2.GenerateSummaryRequest(meeting_id=str(meeting.id))
result = await servicer.GenerateSummary(request, MockContext())
- assert "No transcript available" in result.executive_summary
+ assert (
+ "No transcript available" in result.executive_summary
+ ), f"empty transcript should produce 'No transcript available', got: {result.executive_summary}"
diff --git a/tests/integration/test_entity_repository.py b/tests/integration/test_entity_repository.py
index 34b6d3f..5af2b39 100644
--- a/tests/integration/test_entity_repository.py
+++ b/tests/integration/test_entity_repository.py
@@ -132,7 +132,7 @@ class TestEntityRepositorySave:
await session.commit()
retrieved = await entity_repo.get(entity.id)
- assert retrieved is not None
+ assert retrieved is not None, f"Entity {entity.id} should exist after save"
assert retrieved.segment_ids == [0, 2, 5, 7, 10], "Segment IDs should be preserved"
async def test_save_normalizes_text_to_lowercase(
@@ -154,7 +154,7 @@ class TestEntityRepositorySave:
await session.commit()
retrieved = await entity_repo.get(entity.id)
- assert retrieved is not None
+ assert retrieved is not None, f"Entity {entity.id} should exist after save"
assert retrieved.text == "ACME CORP", "Original text preserved"
assert retrieved.normalized_text == "acme corp", "Normalized text is lowercase"
@@ -190,8 +190,8 @@ class TestEntityRepositorySaveBatch:
await session.commit()
assert len(saved) == 5, "Should return all entities"
- for entity in saved:
- assert entity.db_id is not None, "Each entity should have db_id"
+ missing_db_id = [e for e in saved if e.db_id is None]
+ assert not missing_db_id, f"All entities should have db_id, but {len(missing_db_id)} are missing"
async def test_saves_empty_batch(
self,
@@ -268,9 +268,9 @@ class TestEntityRepositoryGet:
retrieved = await entity_repo.get(entity.id)
- assert retrieved is not None
+ assert retrieved is not None, f"Entity {entity.id} should exist after save"
assert isinstance(retrieved.category, EntityCategory), "Category should be enum"
- assert retrieved.category == EntityCategory.PERSON
+ assert retrieved.category == EntityCategory.PERSON, f"Expected PERSON category, got {retrieved.category}"
# ============================================================================
@@ -361,7 +361,7 @@ class TestEntityRepositoryGetByMeeting:
# Check ordering (category first, then text within category)
texts = [e.text for e in result]
- assert texts == sorted(texts, key=lambda t: (result[texts.index(t)].category.value, t))
+ assert texts == sorted(texts, key=lambda t: (result[texts.index(t)].category.value, t)), f"Entities should be ordered by category then text, got {texts}"
async def test_isolates_meetings(
self,
@@ -397,7 +397,7 @@ class TestEntityRepositoryGetByMeeting:
result = await entity_repo.get_by_meeting(meeting1.id)
assert len(result) == 1, "Should return only meeting1 entities"
- assert result[0].text == "Meeting 1 Entity"
+ assert result[0].text == "Meeting 1 Entity", f"Expected 'Meeting 1 Entity', got '{result[0].text}'"
# ============================================================================
@@ -475,7 +475,7 @@ class TestEntityRepositoryUpdatePinned:
assert result is True, "Should return True on success"
retrieved = await entity_repo.get(entity.id)
- assert retrieved is not None
+ assert retrieved is not None, f"Entity {entity.id} should exist after update"
assert retrieved.is_pinned is True, "Should be pinned"
async def test_unpins_entity(
@@ -502,7 +502,7 @@ class TestEntityRepositoryUpdatePinned:
assert result is True, "Should return True"
retrieved = await entity_repo.get(entity.id)
- assert retrieved is not None
+ assert retrieved is not None, f"Entity {entity.id} should exist after update"
assert retrieved.is_pinned is False, "Should be unpinned"
async def test_update_pinned_returns_false_for_nonexistent(
@@ -593,7 +593,7 @@ class TestEntityRepositoryUpdate:
result = await entity_repo.update(entity.id, text="John Smith", category="person")
await session.commit()
- assert result is not None
+ assert result is not None, f"Entity {entity.id} should exist after update"
assert result.text == "John Smith", "Text updated"
assert result.category == EntityCategory.PERSON, "Category updated"
diff --git a/tests/integration/test_error_handling.py b/tests/integration/test_error_handling.py
index 4ebfdfe..85fd75f 100644
--- a/tests/integration/test_error_handling.py
+++ b/tests/integration/test_error_handling.py
@@ -49,6 +49,10 @@ class MockContext:
raise grpc.RpcError()
+# Test constants
+LARGE_OFFSET = 100
+
+
@pytest.mark.integration
class TestInvalidInputHandling:
"""Integration tests for invalid input handling."""
@@ -65,7 +69,9 @@ class TestInvalidInputHandling:
with pytest.raises(grpc.RpcError, match=".*"):
await servicer.GetMeeting(request, context)
- assert context.abort_code == grpc.StatusCode.INVALID_ARGUMENT
+ assert context.abort_code == grpc.StatusCode.INVALID_ARGUMENT, (
+ f"expected INVALID_ARGUMENT for malformed UUID, got {context.abort_code}"
+ )
async def test_empty_meeting_id(
self, session_factory: async_sessionmaker[AsyncSession]
@@ -79,7 +85,9 @@ class TestInvalidInputHandling:
with pytest.raises(grpc.RpcError, match=".*"):
await servicer.GetMeeting(request, context)
- assert context.abort_code == grpc.StatusCode.INVALID_ARGUMENT
+ assert context.abort_code == grpc.StatusCode.INVALID_ARGUMENT, (
+ f"expected INVALID_ARGUMENT for empty meeting ID, got {context.abort_code}"
+ )
async def test_nonexistent_meeting_returns_not_found(
self, session_factory: async_sessionmaker[AsyncSession]
@@ -93,7 +101,9 @@ class TestInvalidInputHandling:
with pytest.raises(grpc.RpcError, match=".*"):
await servicer.GetMeeting(request, context)
- assert context.abort_code == grpc.StatusCode.NOT_FOUND
+ assert context.abort_code == grpc.StatusCode.NOT_FOUND, (
+ f"expected NOT_FOUND for nonexistent meeting, got {context.abort_code}"
+ )
async def test_delete_nonexistent_meeting(
self, session_factory: async_sessionmaker[AsyncSession]
@@ -107,7 +117,9 @@ class TestInvalidInputHandling:
with pytest.raises(grpc.RpcError, match=".*"):
await servicer.DeleteMeeting(request, context)
- assert context.abort_code == grpc.StatusCode.NOT_FOUND
+ assert context.abort_code == grpc.StatusCode.NOT_FOUND, (
+ f"expected NOT_FOUND when deleting nonexistent meeting, got {context.abort_code}"
+ )
@pytest.mark.integration
@@ -120,7 +132,9 @@ class TestSegmentEdgeCases:
"""Test getting segments from nonexistent meeting returns empty list."""
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
segments = await uow.segments.get_by_meeting(MeetingId(uuid4()))
- assert segments == []
+ assert segments == [], (
+ f"expected empty list for nonexistent meeting, got {len(segments)} segments"
+ )
async def test_segment_with_zero_duration(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -141,8 +155,13 @@ class TestSegmentEdgeCases:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
segments = await uow.segments.get_by_meeting(meeting.id)
- assert len(segments) == 1
- assert segments[0].start_time == segments[0].end_time
+ assert len(segments) == 1, (
+ f"expected 1 segment for zero-duration test, got {len(segments)}"
+ )
+ assert segments[0].start_time == segments[0].end_time, (
+ f"expected start_time == end_time for zero-duration segment, "
+ f"got start={segments[0].start_time}, end={segments[0].end_time}"
+ )
async def test_segment_with_large_text(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -164,8 +183,12 @@ class TestSegmentEdgeCases:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
segments = await uow.segments.get_by_meeting(meeting.id)
- assert len(segments) == 1
- assert len(segments[0].text) == len(large_text)
+ assert len(segments) == 1, (
+ f"expected 1 segment for large-text test, got {len(segments)}"
+ )
+ assert len(segments[0].text) == len(large_text), (
+ f"expected text length {len(large_text)}, got {len(segments[0].text)}"
+ )
async def test_segment_ordering_preserved(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -188,7 +211,9 @@ class TestSegmentEdgeCases:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
segments = await uow.segments.get_by_meeting(meeting.id)
segment_ids = [s.segment_id for s in segments]
- assert segment_ids == sorted(segment_ids)
+ assert segment_ids == sorted(segment_ids), (
+ f"expected segments ordered by segment_id, got {segment_ids}"
+ )
@pytest.mark.integration
@@ -235,7 +260,9 @@ class TestDiarizationJobEdgeCases:
str(uuid4()),
JOB_STATUS_COMPLETED,
)
- assert result is False
+ assert result is False, (
+ f"expected False when updating nonexistent job, got {result}"
+ )
async def test_get_nonexistent_job(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -243,7 +270,9 @@ class TestDiarizationJobEdgeCases:
"""Test getting nonexistent job returns None."""
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
result = await uow.diarization_jobs.get(str(uuid4()))
- assert result is None
+ assert result is None, (
+ f"expected None for nonexistent job, got {result}"
+ )
async def test_job_meeting_cascade_delete(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -269,7 +298,9 @@ class TestDiarizationJobEdgeCases:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
job = await uow.diarization_jobs.get(job_id)
- assert job is None
+ assert job is None, (
+ f"expected job to be cascade-deleted with meeting, got {job}"
+ )
@pytest.mark.integration
@@ -301,8 +332,11 @@ class TestSummaryEdgeCases:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
saved = await uow.summaries.get_by_meeting(meeting.id)
- assert saved is not None
- assert saved.executive_summary == "Second summary"
+ assert saved is not None, "expected summary to exist after overwrite"
+ assert saved.executive_summary == "Second summary", (
+ f"expected executive_summary to be 'Second summary', "
+ f"got '{saved.executive_summary}'"
+ )
async def test_get_summary_for_nonexistent_meeting(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -310,7 +344,9 @@ class TestSummaryEdgeCases:
"""Test getting summary for nonexistent meeting returns None."""
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
result = await uow.summaries.get_by_meeting(MeetingId(uuid4()))
- assert result is None
+ assert result is None, (
+ f"expected None for summary of nonexistent meeting, got {result}"
+ )
async def test_delete_summary_for_meeting_without_summary(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -323,7 +359,9 @@ class TestSummaryEdgeCases:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
result = await uow.summaries.delete_by_meeting(meeting.id)
- assert result is False
+ assert result is False, (
+ f"expected False when deleting nonexistent summary, got {result}"
+ )
async def test_summary_deleted_with_meeting(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -346,7 +384,9 @@ class TestSummaryEdgeCases:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
result = await uow.summaries.get_by_meeting(meeting.id)
- assert result is None
+ assert result is None, (
+ f"expected summary to be cascade-deleted with meeting, got {result}"
+ )
@pytest.mark.integration
@@ -367,7 +407,9 @@ class TestPreferencesEdgeCases:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
value = await uow.preferences.get("test_key")
- assert value == "value2"
+ assert value == "value2", (
+ f"expected preference to be overwritten to 'value2', got '{value}'"
+ )
async def test_get_nonexistent_preference(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -375,7 +417,9 @@ class TestPreferencesEdgeCases:
"""Test getting nonexistent preference returns None."""
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
value = await uow.preferences.get("nonexistent_key")
- assert value is None
+ assert value is None, (
+ f"expected None for nonexistent preference, got {value}"
+ )
async def test_delete_nonexistent_preference(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -383,7 +427,9 @@ class TestPreferencesEdgeCases:
"""Test deleting nonexistent preference returns False."""
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
result = await uow.preferences.delete("nonexistent_key")
- assert result is False
+ assert result is False, (
+ f"expected False when deleting nonexistent preference, got {result}"
+ )
async def test_preference_with_special_characters_in_key(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -396,7 +442,9 @@ class TestPreferencesEdgeCases:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
value = await uow.preferences.get(key)
- assert value == "value"
+ assert value == "value", (
+ f"expected preference with special chars in key to store 'value', got '{value}'"
+ )
@pytest.mark.integration
@@ -419,7 +467,9 @@ class TestTransactionRollback:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
result = await uow.meetings.get(meeting_id)
- assert result is None
+ assert result is None, (
+ f"expected meeting to be rolled back on exception, got {result}"
+ )
async def test_partial_commit_not_allowed(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -431,7 +481,9 @@ class TestTransactionRollback:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
result = await uow.meetings.get(meeting.id)
- assert result is None
+ assert result is None, (
+ f"expected meeting not to persist without commit, got {result}"
+ )
@pytest.mark.integration
@@ -450,10 +502,12 @@ class TestListingEdgeCases:
servicer = NoteFlowServicer(session_factory=session_factory)
- request = noteflow_pb2.ListMeetingsRequest(offset=100)
+ request = noteflow_pb2.ListMeetingsRequest(offset=LARGE_OFFSET)
result = await servicer.ListMeetings(request, MockContext())
- assert len(result.meetings) == 0
+ assert len(result.meetings) == 0, (
+ f"expected empty meetings list with large offset, got {len(result.meetings)}"
+ )
async def test_list_empty_database(
self, session_factory: async_sessionmaker[AsyncSession]
@@ -464,8 +518,12 @@ class TestListingEdgeCases:
request = noteflow_pb2.ListMeetingsRequest()
result = await servicer.ListMeetings(request, MockContext())
- assert result.total_count == 0
- assert len(result.meetings) == 0
+ assert result.total_count == 0, (
+ f"expected total_count=0 for empty database, got {result.total_count}"
+ )
+ assert len(result.meetings) == 0, (
+ f"expected empty meetings list for empty database, got {len(result.meetings)}"
+ )
@pytest.mark.integration
@@ -487,7 +545,9 @@ class TestExportErrorHandling:
with pytest.raises(grpc.RpcError, match=".*"):
await servicer.ExportTranscript(request, context)
- assert context.abort_code == grpc.StatusCode.NOT_FOUND
+ assert context.abort_code == grpc.StatusCode.NOT_FOUND, (
+ f"expected NOT_FOUND when exporting nonexistent meeting, got {context.abort_code}"
+ )
async def test_export_invalid_format(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -514,8 +574,10 @@ class TestExportErrorHandling:
)
result = await servicer.ExportTranscript(request, MockContext())
- assert result.content
- assert result.file_extension
+ assert result.content, "expected non-empty content for export with unspecified format"
+ assert result.file_extension, (
+ "expected non-empty file_extension for export with unspecified format"
+ )
@pytest.mark.integration
@@ -534,7 +596,9 @@ class TestSummarizationErrorHandling:
with pytest.raises(grpc.RpcError, match=".*"):
await servicer.GenerateSummary(request, context)
- assert context.abort_code == grpc.StatusCode.NOT_FOUND
+ assert context.abort_code == grpc.StatusCode.NOT_FOUND, (
+ f"expected NOT_FOUND when summarizing nonexistent meeting, got {context.abort_code}"
+ )
async def test_summarize_empty_meeting_returns_placeholder(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -550,7 +614,9 @@ class TestSummarizationErrorHandling:
request = noteflow_pb2.GenerateSummaryRequest(meeting_id=str(meeting.id))
result = await servicer.GenerateSummary(request, MockContext())
- assert "No transcript" in result.executive_summary
+ assert "No transcript" in result.executive_summary, (
+ f"expected placeholder summary for empty meeting, got '{result.executive_summary}'"
+ )
@pytest.mark.integration
@@ -569,7 +635,9 @@ class TestAnnotationErrorHandling:
with pytest.raises(grpc.RpcError, match=".*"):
await servicer.GetAnnotation(request, context)
- assert context.abort_code == grpc.StatusCode.NOT_FOUND
+ assert context.abort_code == grpc.StatusCode.NOT_FOUND, (
+ f"expected NOT_FOUND when getting nonexistent annotation, got {context.abort_code}"
+ )
async def test_update_nonexistent_annotation(
self, session_factory: async_sessionmaker[AsyncSession]
@@ -586,7 +654,9 @@ class TestAnnotationErrorHandling:
with pytest.raises(grpc.RpcError, match=".*"):
await servicer.UpdateAnnotation(request, context)
- assert context.abort_code == grpc.StatusCode.NOT_FOUND
+ assert context.abort_code == grpc.StatusCode.NOT_FOUND, (
+ f"expected NOT_FOUND when updating nonexistent annotation, got {context.abort_code}"
+ )
async def test_delete_nonexistent_annotation(
self, session_factory: async_sessionmaker[AsyncSession]
@@ -600,7 +670,9 @@ class TestAnnotationErrorHandling:
with pytest.raises(grpc.RpcError, match=".*"):
await servicer.DeleteAnnotation(request, context)
- assert context.abort_code == grpc.StatusCode.NOT_FOUND
+ assert context.abort_code == grpc.StatusCode.NOT_FOUND, (
+ f"expected NOT_FOUND when deleting nonexistent annotation, got {context.abort_code}"
+ )
@pytest.mark.integration
@@ -619,4 +691,6 @@ class TestDiarizationJobErrorHandling:
with pytest.raises(grpc.RpcError, match=".*"):
await servicer.GetDiarizationJobStatus(request, context)
- assert context.abort_code == grpc.StatusCode.NOT_FOUND
+ assert context.abort_code == grpc.StatusCode.NOT_FOUND, (
+ f"expected NOT_FOUND for nonexistent diarization job, got {context.abort_code}"
+ )
diff --git a/tests/integration/test_grpc_servicer_database.py b/tests/integration/test_grpc_servicer_database.py
index da8ddb1..a13abb2 100644
--- a/tests/integration/test_grpc_servicer_database.py
+++ b/tests/integration/test_grpc_servicer_database.py
@@ -93,18 +93,18 @@ class TestServicerMeetingOperationsWithDatabase:
)
result = await servicer.CreateMeeting(request, MockContext())
- assert result.id
- assert result.title == "Database Test Meeting"
- assert result.state == noteflow_pb2.MEETING_STATE_CREATED
+ assert result.id, "CreateMeeting response should include a meeting ID"
+ assert result.title == "Database Test Meeting", f"expected title 'Database Test Meeting', got '{result.title}'"
+ assert result.state == noteflow_pb2.MEETING_STATE_CREATED, f"expected state CREATED, got {result.state}"
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
from noteflow.domain.value_objects import MeetingId
meeting = await uow.meetings.get(MeetingId(uuid4().hex.replace("-", "")))
meeting = await uow.meetings.get(MeetingId(result.id))
- assert meeting is not None
- assert meeting.title == "Database Test Meeting"
- assert meeting.metadata["source"] == "integration_test"
+ assert meeting is not None, f"meeting with ID {result.id} should exist in database"
+ assert meeting.title == "Database Test Meeting", f"expected title 'Database Test Meeting', got '{meeting.title}'"
+ assert meeting.metadata["source"] == "integration_test", f"expected metadata source 'integration_test', got '{meeting.metadata.get('source')}'"
async def test_get_meeting_retrieves_from_database(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -120,8 +120,8 @@ class TestServicerMeetingOperationsWithDatabase:
request = noteflow_pb2.GetMeetingRequest(meeting_id=str(meeting.id))
result = await servicer.GetMeeting(request, MockContext())
- assert result.id == str(meeting.id)
- assert result.title == "Persisted Meeting"
+ assert result.id == str(meeting.id), f"expected meeting ID {meeting.id}, got {result.id}"
+ assert result.title == "Persisted Meeting", f"expected title 'Persisted Meeting', got '{result.title}'"
async def test_get_meeting_with_segments(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -149,10 +149,10 @@ class TestServicerMeetingOperationsWithDatabase:
)
result = await servicer.GetMeeting(request, MockContext())
- assert len(result.segments) == 3
- assert result.segments[0].text == "Segment 0"
- assert result.segments[1].text == "Segment 1"
- assert result.segments[2].text == "Segment 2"
+ assert len(result.segments) == 3, f"expected 3 segments, got {len(result.segments)}"
+ assert result.segments[0].text == "Segment 0", f"expected segment 0 text 'Segment 0', got '{result.segments[0].text}'"
+ assert result.segments[1].text == "Segment 1", f"expected segment 1 text 'Segment 1', got '{result.segments[1].text}'"
+ assert result.segments[2].text == "Segment 2", f"expected segment 2 text 'Segment 2', got '{result.segments[2].text}'"
async def test_get_nonexistent_meeting_returns_not_found(
self, session_factory: async_sessionmaker[AsyncSession]
@@ -166,7 +166,7 @@ class TestServicerMeetingOperationsWithDatabase:
with pytest.raises(grpc.RpcError, match=".*"):
await servicer.GetMeeting(request, context)
- assert context.abort_code == grpc.StatusCode.NOT_FOUND
+ assert context.abort_code == grpc.StatusCode.NOT_FOUND, f"expected NOT_FOUND status for nonexistent meeting, got {context.abort_code}"
async def test_list_meetings_queries_database(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -183,8 +183,8 @@ class TestServicerMeetingOperationsWithDatabase:
request = noteflow_pb2.ListMeetingsRequest(limit=10)
result = await servicer.ListMeetings(request, MockContext())
- assert result.total_count == 5
- assert len(result.meetings) == 5
+ assert result.total_count == 5, f"expected total_count 5, got {result.total_count}"
+ assert len(result.meetings) == 5, f"expected 5 meetings in response, got {len(result.meetings)}"
async def test_list_meetings_with_state_filter(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -212,8 +212,8 @@ class TestServicerMeetingOperationsWithDatabase:
)
result = await servicer.ListMeetings(request, MockContext())
- assert result.total_count == 1
- assert result.meetings[0].title == "Recording"
+ assert result.total_count == 1, f"expected 1 meeting with RECORDING state, got {result.total_count}"
+ assert result.meetings[0].title == "Recording", f"expected meeting title 'Recording', got '{result.meetings[0].title}'"
async def test_delete_meeting_removes_from_database(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -229,11 +229,11 @@ class TestServicerMeetingOperationsWithDatabase:
request = noteflow_pb2.DeleteMeetingRequest(meeting_id=str(meeting.id))
result = await servicer.DeleteMeeting(request, MockContext())
- assert result.success is True
+ assert result.success is True, "DeleteMeeting should return success=True"
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
deleted = await uow.meetings.get(meeting.id)
- assert deleted is None
+ assert deleted is None, f"meeting {meeting.id} should have been deleted from database"
async def test_stop_meeting_updates_database_state(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -250,12 +250,12 @@ class TestServicerMeetingOperationsWithDatabase:
request = noteflow_pb2.StopMeetingRequest(meeting_id=str(meeting.id))
result = await servicer.StopMeeting(request, MockContext())
- assert result.state == noteflow_pb2.MEETING_STATE_STOPPED
+ assert result.state == noteflow_pb2.MEETING_STATE_STOPPED, f"expected STOPPED state in response, got {result.state}"
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
stopped = await uow.meetings.get(meeting.id)
- assert stopped is not None
- assert stopped.state == MeetingState.STOPPED
+ assert stopped is not None, f"meeting {meeting.id} should exist in database after stopping"
+ assert stopped.state == MeetingState.STOPPED, f"expected STOPPED state in database, got {stopped.state}"
@pytest.mark.integration
@@ -286,14 +286,14 @@ class TestServicerDiarizationWithDatabase:
)
result = await servicer.RefineSpeakerDiarization(request, MockContext())
- assert result.job_id
- assert result.status == noteflow_pb2.JOB_STATUS_QUEUED
+ assert result.job_id, "RefineSpeakerDiarization response should include a job ID"
+ assert result.status == noteflow_pb2.JOB_STATUS_QUEUED, f"expected QUEUED status, got {result.status}"
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
job = await uow.diarization_jobs.get(result.job_id)
- assert job is not None
- assert job.meeting_id == str(meeting.id)
- assert job.status == JOB_STATUS_QUEUED
+ assert job is not None, f"diarization job {result.job_id} should exist in database"
+ assert job.meeting_id == str(meeting.id), f"expected job meeting_id {meeting.id}, got {job.meeting_id}"
+ assert job.status == JOB_STATUS_QUEUED, f"expected job status QUEUED, got {job.status}"
async def test_get_diarization_job_status_retrieves_from_database(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -318,10 +318,10 @@ class TestServicerDiarizationWithDatabase:
request = noteflow_pb2.GetDiarizationJobStatusRequest(job_id=job.job_id)
result = await servicer.GetDiarizationJobStatus(request, MockContext())
- assert result.job_id == job.job_id
- assert result.status == noteflow_pb2.JOB_STATUS_COMPLETED
- assert result.segments_updated == DIARIZATION_SEGMENTS_UPDATED
- assert list(result.speaker_ids) == ["SPEAKER_00", "SPEAKER_01"]
+ assert result.job_id == job.job_id, f"expected job_id {job.job_id}, got {result.job_id}"
+ assert result.status == noteflow_pb2.JOB_STATUS_COMPLETED, f"expected COMPLETED status, got {result.status}"
+ assert result.segments_updated == DIARIZATION_SEGMENTS_UPDATED, f"expected {DIARIZATION_SEGMENTS_UPDATED} segments_updated, got {result.segments_updated}"
+ assert list(result.speaker_ids) == ["SPEAKER_00", "SPEAKER_01"], f"expected speaker_ids ['SPEAKER_00', 'SPEAKER_01'], got {list(result.speaker_ids)}"
async def test_get_nonexistent_job_returns_not_found(
self, session_factory: async_sessionmaker[AsyncSession]
@@ -335,7 +335,7 @@ class TestServicerDiarizationWithDatabase:
with pytest.raises(grpc.RpcError, match=".*"):
await servicer.GetDiarizationJobStatus(request, context)
- assert context.abort_code == grpc.StatusCode.NOT_FOUND
+ assert context.abort_code == grpc.StatusCode.NOT_FOUND, f"expected NOT_FOUND status for nonexistent job, got {context.abort_code}"
async def test_refine_rejects_recording_meeting(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -359,8 +359,8 @@ class TestServicerDiarizationWithDatabase:
)
result = await servicer.RefineSpeakerDiarization(request, MockContext())
- assert result.status == noteflow_pb2.JOB_STATUS_FAILED
- assert "stopped" in result.error_message.lower()
+ assert result.status == noteflow_pb2.JOB_STATUS_FAILED, f"expected FAILED status for recording meeting, got {result.status}"
+ assert "stopped" in result.error_message.lower(), f"expected 'stopped' in error message, got '{result.error_message}'"
@pytest.mark.integration
@@ -397,7 +397,7 @@ class TestServicerServerInfoWithDatabase:
request = noteflow_pb2.ServerInfoRequest()
result = await servicer.GetServerInfo(request, MockContext())
- assert result.active_meetings == 3
+ assert result.active_meetings == 3, f"expected 3 active meetings (2 recording + 1 stopping), got {result.active_meetings}"
@pytest.mark.integration
@@ -440,9 +440,9 @@ class TestServicerShutdownWithDatabase:
j2 = await uow.diarization_jobs.get(job2.job_id)
j3 = await uow.diarization_jobs.get(job3.job_id)
- assert j1 is not None and j1.status == JOB_STATUS_FAILED
- assert j2 is not None and j2.status == JOB_STATUS_FAILED
- assert j3 is not None and j3.status == JOB_STATUS_COMPLETED
+ assert j1 is not None and j1.status == JOB_STATUS_FAILED, f"queued job should be marked FAILED after shutdown, got status={j1.status if j1 else 'None'}"
+ assert j2 is not None and j2.status == JOB_STATUS_FAILED, f"running job should be marked FAILED after shutdown, got status={j2.status if j2 else 'None'}"
+ assert j3 is not None and j3.status == JOB_STATUS_COMPLETED, f"completed job should remain COMPLETED after shutdown, got status={j3.status if j3 else 'None'}"
@pytest.mark.integration
@@ -477,15 +477,15 @@ class TestServicerRenameSpeakerWithDatabase:
)
result = await servicer.RenameSpeaker(request, MockContext())
- assert result.segments_updated == 3
- assert result.success is True
+ assert result.segments_updated == 3, f"expected 3 segments updated, got {result.segments_updated}"
+ assert result.success is True, "RenameSpeaker should return success=True"
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
segments = await uow.segments.get_by_meeting(meeting.id)
alice_segments = [s for s in segments if s.speaker_id == "Alice"]
other_segments = [s for s in segments if s.speaker_id == "SPEAKER_01"]
- assert len(alice_segments) == 3
- assert len(other_segments) == 2
+ assert len(alice_segments) == 3, f"expected 3 segments with speaker_id 'Alice', got {len(alice_segments)}"
+ assert len(other_segments) == 2, f"expected 2 segments with speaker_id 'SPEAKER_01', got {len(other_segments)}"
@pytest.mark.integration
@@ -505,7 +505,7 @@ class TestServicerTransactionIntegrity:
from noteflow.domain.value_objects import MeetingId
meeting = await uow.meetings.get(MeetingId(result.id))
- assert meeting is not None
+ assert meeting is not None, f"meeting {result.id} should exist in database after atomic create"
async def test_stop_meeting_clears_streaming_turns(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -532,7 +532,7 @@ class TestServicerTransactionIntegrity:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
remaining = await uow.diarization_jobs.get_streaming_turns(str(meeting.id))
- assert remaining == []
+ assert remaining == [], f"expected no streaming turns after StopMeeting, got {len(remaining)} turns"
@pytest.mark.integration
@@ -568,7 +568,7 @@ class TestServicerEntityMutationsWithDatabase:
confidence=0.95,
is_pinned=False,
)
- assert uow._session is not None
+ assert uow._session is not None, "UnitOfWork session should be initialized"
uow._session.add(entity_model)
await uow.commit()
@@ -589,11 +589,11 @@ class TestServicerEntityMutationsWithDatabase:
)
result = await servicer.UpdateEntity(request, MockContext())
- assert (result.entity.id, result.entity.text) == (entity_id, "Updated Name")
+ assert (result.entity.id, result.entity.text) == (entity_id, "Updated Name"), f"expected entity ({entity_id}, 'Updated Name'), got ({result.entity.id}, '{result.entity.text}')"
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
updated = await uow.entities.get(PyUUID(entity_id))
- assert updated is not None and updated.text == "Updated Name"
+ assert updated is not None and updated.text == "Updated Name", f"entity text in database should be 'Updated Name', got '{updated.text if updated else 'None'}'"
async def test_update_entity_category_via_grpc(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -610,11 +610,11 @@ class TestServicerEntityMutationsWithDatabase:
)
result = await servicer.UpdateEntity(request, MockContext())
- assert result.entity.category == "company"
+ assert result.entity.category == "company", f"expected category 'company' in response, got '{result.entity.category}'"
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
updated = await uow.entities.get(PyUUID(entity_id))
- assert updated is not None and updated.category.value == "company"
+ assert updated is not None and updated.category.value == "company", f"entity category in database should be 'company', got '{updated.category.value if updated else 'None'}'"
async def test_update_entity_both_fields_via_grpc(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -632,13 +632,13 @@ class TestServicerEntityMutationsWithDatabase:
)
result = await servicer.UpdateEntity(request, MockContext())
- assert (result.entity.text, result.entity.category) == ("Acme Industries", "company")
+ assert (result.entity.text, result.entity.category) == ("Acme Industries", "company"), f"expected ('Acme Industries', 'company'), got ('{result.entity.text}', '{result.entity.category}')"
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
updated = await uow.entities.get(PyUUID(entity_id))
assert updated is not None and (updated.text, updated.category.value) == (
"Acme Industries", "company"
- )
+ ), f"entity in database should have text='Acme Industries' and category='company', got text='{updated.text if updated else 'None'}', category='{updated.category.value if updated else 'None'}'"
async def test_update_nonexistent_entity_grpc_not_found(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -659,7 +659,7 @@ class TestServicerEntityMutationsWithDatabase:
with pytest.raises(grpc.RpcError, match="not found"):
await servicer.UpdateEntity(request, context)
- assert context.abort_code == grpc.StatusCode.NOT_FOUND
+ assert context.abort_code == grpc.StatusCode.NOT_FOUND, f"expected NOT_FOUND for nonexistent entity, got {context.abort_code}"
async def test_update_entity_grpc_invalid_id(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -680,7 +680,7 @@ class TestServicerEntityMutationsWithDatabase:
with pytest.raises(grpc.RpcError, match="Invalid"):
await servicer.UpdateEntity(request, context)
- assert context.abort_code == grpc.StatusCode.INVALID_ARGUMENT
+ assert context.abort_code == grpc.StatusCode.INVALID_ARGUMENT, f"expected INVALID_ARGUMENT for malformed entity_id, got {context.abort_code}"
async def test_delete_entity_grpc_removes_from_db(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -695,10 +695,10 @@ class TestServicerEntityMutationsWithDatabase:
)
result = await servicer.DeleteEntity(request, MockContext())
- assert result.success is True
+ assert result.success is True, "DeleteEntity should return success=True"
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
- assert await uow.entities.get(PyUUID(entity_id)) is None
+ assert await uow.entities.get(PyUUID(entity_id)) is None, f"entity {entity_id} should have been deleted from database"
async def test_delete_nonexistent_entity_grpc_not_found(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -719,7 +719,7 @@ class TestServicerEntityMutationsWithDatabase:
with pytest.raises(grpc.RpcError, match="not found"):
await servicer.DeleteEntity(request, context)
- assert context.abort_code == grpc.StatusCode.NOT_FOUND
+ assert context.abort_code == grpc.StatusCode.NOT_FOUND, f"expected NOT_FOUND for nonexistent entity, got {context.abort_code}"
async def test_delete_entity_grpc_invalid_id(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -740,7 +740,7 @@ class TestServicerEntityMutationsWithDatabase:
with pytest.raises(grpc.RpcError, match="Invalid"):
await servicer.DeleteEntity(request, context)
- assert context.abort_code == grpc.StatusCode.INVALID_ARGUMENT
+ assert context.abort_code == grpc.StatusCode.INVALID_ARGUMENT, f"expected INVALID_ARGUMENT for malformed entity_id, got {context.abort_code}"
async def test_grpc_delete_preserves_other_entities(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -752,7 +752,7 @@ class TestServicerEntityMutationsWithDatabase:
meeting = Meeting.create(title="Multi-Entity Meeting")
await uow.meetings.create(meeting)
entity1_id, entity2_id = uuid4(), uuid4()
- assert uow._session is not None
+ assert uow._session is not None, "UnitOfWork session should be initialized"
uow._session.add(NamedEntityModel(
id=entity1_id, meeting_id=meeting.id, text="Entity One",
normalized_text="entity one", category="company",
@@ -774,4 +774,4 @@ class TestServicerEntityMutationsWithDatabase:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
deleted, kept = await uow.entities.get(entity1_id), await uow.entities.get(entity2_id)
- assert deleted is None and kept is not None and kept.text == "Entity Two"
+ assert deleted is None and kept is not None and kept.text == "Entity Two", f"only entity1 should be deleted; entity2 should remain with text='Entity Two', got deleted={deleted is None}, kept={kept is not None}, kept_text='{kept.text if kept else 'None'}'"
diff --git a/tests/integration/test_memory_fallback.py b/tests/integration/test_memory_fallback.py
index 4de44ef..0ca31d4 100644
--- a/tests/integration/test_memory_fallback.py
+++ b/tests/integration/test_memory_fallback.py
@@ -62,9 +62,9 @@ class TestMeetingStoreBasicOperations:
meeting1 = store.create(title="Meeting 1")
meeting2 = store.create(title="Meeting 2")
- assert meeting1.id != meeting2.id
- assert meeting1.title == "Meeting 1"
- assert meeting2.title == "Meeting 2"
+ assert meeting1.id != meeting2.id, f"meeting IDs should be unique, got {meeting1.id} twice"
+ assert meeting1.title == "Meeting 1", f"expected 'Meeting 1', got {meeting1.title!r}"
+ assert meeting2.title == "Meeting 2", f"expected 'Meeting 2', got {meeting2.title!r}"
def test_insert_and_get_meeting(self) -> None:
"""Test inserting and retrieving a meeting."""
@@ -85,7 +85,7 @@ class TestMeetingStoreBasicOperations:
result = store.get(str(uuid4()))
- assert result is None
+ assert result is None, f"get should return None for nonexistent meeting, got {result}"
def test_update_meeting_in_store(self) -> None:
"""Test updating a meeting in MeetingStore."""
@@ -96,8 +96,8 @@ class TestMeetingStoreBasicOperations:
store.update(meeting)
retrieved = store.get(str(meeting.id))
- assert retrieved is not None
- assert retrieved.title == "Updated Title"
+ assert retrieved is not None, "updated meeting should exist in store"
+ assert retrieved.title == "Updated Title", f"expected 'Updated Title', got {retrieved.title!r}"
def test_delete_meeting_from_store(self) -> None:
"""Test deleting a meeting from MeetingStore."""
@@ -106,8 +106,8 @@ class TestMeetingStoreBasicOperations:
result = store.delete(str(meeting.id))
- assert result is True
- assert store.get(str(meeting.id)) is None
+ assert result is True, "delete should return True for existing meeting"
+ assert store.get(str(meeting.id)) is None, "deleted meeting should not be retrievable"
def test_delete_nonexistent_returns_false(self) -> None:
"""Test deleting nonexistent meeting returns False."""
@@ -115,7 +115,7 @@ class TestMeetingStoreBasicOperations:
result = store.delete(str(uuid4()))
- assert result is False
+ assert result is False, "delete should return False for nonexistent meeting"
@pytest.mark.integration
@@ -131,8 +131,8 @@ class TestMeetingStoreListingAndFiltering:
meetings, total = store.list_all(limit=3, offset=0)
- assert len(meetings) == 3
- assert total == 10
+ assert len(meetings) == 3, f"expected page size of meetings, got {len(meetings)}"
+ assert total == 10, f"expected all meetings in total, got {total}"
def test_list_all_with_offset(self) -> None:
"""Test listing meetings with offset."""
@@ -143,8 +143,8 @@ class TestMeetingStoreListingAndFiltering:
meetings, total = store.list_all(limit=10, offset=2)
- assert len(meetings) == 3
- assert total == 5
+ assert len(meetings) == 3, f"expected remaining meetings after offset, got {len(meetings)}"
+ assert total == 5, f"expected all meetings in total, got {total}"
def test_list_all_filter_by_state(self) -> None:
"""Test listing meetings filtered by state."""
@@ -157,9 +157,9 @@ class TestMeetingStoreListingAndFiltering:
meetings, total = store.list_all(states=[MeetingState.RECORDING])
- assert len(meetings) == 1
- assert total == 1
- assert meetings[0].id == recording.id
+ assert len(meetings) == 1, f"expected 1 recording meeting, got {len(meetings)}"
+ assert total == 1, f"expected total=1 for recording state, got {total}"
+ assert meetings[0].id == recording.id, f"expected meeting id {recording.id}, got {meetings[0].id}"
def test_list_all_filter_by_multiple_states(self) -> None:
"""Test filtering by multiple states."""
@@ -178,8 +178,8 @@ class TestMeetingStoreListingAndFiltering:
states=[MeetingState.RECORDING, MeetingState.STOPPING]
)
- assert len(meetings) == 2
- assert total == 2
+ assert len(meetings) == 2, f"expected 2 meetings (recording+stopping), got {len(meetings)}"
+ assert total == 2, f"expected total=2 for multi-state filter, got {total}"
def test_list_all_sort_order(self) -> None:
"""Test listing with sort order."""
@@ -191,8 +191,12 @@ class TestMeetingStoreListingAndFiltering:
meetings_desc, _ = store.list_all(sort_desc=True)
meetings_asc, _ = store.list_all(sort_desc=False)
- assert meetings_desc[0].created_at >= meetings_desc[-1].created_at
- assert meetings_asc[0].created_at <= meetings_asc[-1].created_at
+ assert meetings_desc[0].created_at >= meetings_desc[-1].created_at, (
+ "descending sort should have newest first"
+ )
+ assert meetings_asc[0].created_at <= meetings_asc[-1].created_at, (
+ "ascending sort should have oldest first"
+ )
def test_count_by_state_in_store(self) -> None:
"""Test counting meetings by state in MeetingStore."""
@@ -207,8 +211,8 @@ class TestMeetingStoreListingAndFiltering:
created_count = store.count_by_state(MeetingState.CREATED)
recording_count = store.count_by_state(MeetingState.RECORDING)
- assert created_count == 2
- assert recording_count == 1
+ assert created_count == 2, f"expected 2 created meetings, got {created_count}"
+ assert recording_count == 1, f"expected 1 recording meeting, got {recording_count}"
@pytest.mark.integration
@@ -238,9 +242,9 @@ class TestMeetingStoreSegments:
segments = store.fetch_segments(str(meeting.id))
- assert len(segments) == 2
- assert segments[0].text == "First segment"
- assert segments[1].text == "Second segment"
+ assert len(segments) == 2, f"expected 2 segments, got {len(segments)}"
+ assert segments[0].text == "First segment", f"first segment mismatch: {segments[0].text!r}"
+ assert segments[1].text == "Second segment", f"second segment mismatch: {segments[1].text!r}"
def test_add_segment_to_nonexistent_meeting(self) -> None:
"""Test adding segment to nonexistent meeting returns None."""
@@ -249,7 +253,7 @@ class TestMeetingStoreSegments:
result = store.add_segment(str(uuid4()), segment)
- assert result is None
+ assert result is None, f"add_segment should return None for nonexistent meeting, got {result}"
def test_get_segments_from_nonexistent_in_store(self) -> None:
"""Test getting segments from nonexistent meeting returns empty list in store."""
@@ -257,7 +261,7 @@ class TestMeetingStoreSegments:
segments = store.fetch_segments(str(uuid4()))
- assert segments == []
+ assert segments == [], f"expected empty list for nonexistent meeting, got {segments}"
def test_get_next_segment_id_in_store(self) -> None:
"""Test getting next segment ID in MeetingStore."""
@@ -265,13 +269,13 @@ class TestMeetingStoreSegments:
meeting = store.create(title="Segment ID Test")
next_id = store.compute_next_segment_id(str(meeting.id))
- assert next_id == 0
+ assert next_id == 0, f"expected next_id=0 for empty meeting, got {next_id}"
segment = Segment(segment_id=0, text="First", start_time=0.0, end_time=1.0)
store.add_segment(str(meeting.id), segment)
next_id = store.compute_next_segment_id(str(meeting.id))
- assert next_id == 1
+ assert next_id == 1, f"expected next_id=1 after adding segment, got {next_id}"
def test_get_next_segment_id_nonexistent_meeting(self) -> None:
"""Test next segment ID for nonexistent meeting is 0."""
@@ -279,7 +283,7 @@ class TestMeetingStoreSegments:
next_id = store.compute_next_segment_id(str(uuid4()))
- assert next_id == 0
+ assert next_id == 0, f"expected next_id=0 for nonexistent meeting, got {next_id}"
@pytest.mark.integration
@@ -299,8 +303,10 @@ class TestMeetingStoreSummary:
store.set_summary(str(meeting.id), summary)
retrieved = store.get_summary(str(meeting.id))
- assert retrieved is not None
- assert retrieved.executive_summary == "This is the executive summary."
+ assert retrieved is not None, "summary should be retrievable after set"
+ assert retrieved.executive_summary == "This is the executive summary.", (
+ f"summary mismatch: {retrieved.executive_summary!r}"
+ )
def test_set_summary_nonexistent_meeting(self) -> None:
"""Test setting summary on nonexistent meeting returns None."""
@@ -312,7 +318,7 @@ class TestMeetingStoreSummary:
result = store.set_summary(str(uuid4()), summary)
- assert result is None
+ assert result is None, f"set_summary should return None for nonexistent meeting, got {result}"
def test_get_summary_nonexistent_meeting(self) -> None:
"""Test getting summary from nonexistent meeting returns None."""
@@ -320,7 +326,7 @@ class TestMeetingStoreSummary:
result = store.get_summary(str(uuid4()))
- assert result is None
+ assert result is None, f"get_summary should return None for nonexistent meeting, got {result}"
def test_clear_summary(self) -> None:
"""Test clearing meeting summary."""
@@ -332,8 +338,8 @@ class TestMeetingStoreSummary:
result = store.clear_summary(str(meeting.id))
- assert result is True
- assert store.get_summary(str(meeting.id)) is None
+ assert result is True, "clear_summary should return True when summary existed"
+ assert store.get_summary(str(meeting.id)) is None, "summary should be None after clearing"
def test_clear_summary_when_none_set(self) -> None:
"""Test clearing summary when none is set returns False."""
@@ -342,7 +348,7 @@ class TestMeetingStoreSummary:
result = store.clear_summary(str(meeting.id))
- assert result is False
+ assert result is False, "clear_summary should return False when no summary exists"
def test_clear_summary_nonexistent_meeting(self) -> None:
"""Test clearing summary on nonexistent meeting returns False."""
@@ -350,7 +356,7 @@ class TestMeetingStoreSummary:
result = store.clear_summary(str(uuid4()))
- assert result is False
+ assert result is False, "clear_summary should return False for nonexistent meeting"
@pytest.mark.integration
@@ -364,10 +370,10 @@ class TestMeetingStoreAtomicUpdates:
result = store.update_state(str(meeting.id), MeetingState.RECORDING)
- assert result is True
+ assert result is True, "update_state should return True for existing meeting"
retrieved = store.get(str(meeting.id))
- assert retrieved is not None
- assert retrieved.state == MeetingState.RECORDING
+ assert retrieved is not None, "meeting should exist after state update"
+ assert retrieved.state == MeetingState.RECORDING, f"expected RECORDING state, got {retrieved.state}"
def test_update_state_nonexistent_meeting(self) -> None:
"""Test state update on nonexistent meeting returns False."""
@@ -375,7 +381,7 @@ class TestMeetingStoreAtomicUpdates:
result = store.update_state(str(uuid4()), MeetingState.RECORDING)
- assert result is False
+ assert result is False, "update_state should return False for nonexistent meeting"
def test_update_title(self) -> None:
"""Test atomic title update."""
@@ -384,10 +390,10 @@ class TestMeetingStoreAtomicUpdates:
result = store.update_title(str(meeting.id), "Updated")
- assert result is True
+ assert result is True, "update_title should return True for existing meeting"
retrieved = store.get(str(meeting.id))
- assert retrieved is not None
- assert retrieved.title == "Updated"
+ assert retrieved is not None, "meeting should exist after title update"
+ assert retrieved.title == "Updated", f"expected 'Updated', got {retrieved.title!r}"
def test_update_end_time(self) -> None:
"""Test atomic end time update."""
@@ -397,10 +403,10 @@ class TestMeetingStoreAtomicUpdates:
result = store.update_end_time(str(meeting.id), end_time)
- assert result is True
+ assert result is True, "update_end_time should return True for existing meeting"
retrieved = store.get(str(meeting.id))
- assert retrieved is not None
- assert retrieved.ended_at == end_time
+ assert retrieved is not None, "meeting should exist after end_time update"
+ assert retrieved.ended_at == end_time, f"expected {end_time}, got {retrieved.ended_at}"
def test_active_count_property(self) -> None:
"""Test active_count property."""
@@ -417,7 +423,7 @@ class TestMeetingStoreAtomicUpdates:
stopping.begin_stopping()
store.update(stopping)
- assert store.active_count == 2
+ assert store.active_count == 2, f"expected 2 active meetings, got {store.active_count}"
@pytest.mark.integration
@@ -442,8 +448,8 @@ class TestMeetingStoreThreadSafety:
for t in threads:
t.join()
- assert len(created_ids) == 10
- assert len(set(created_ids)) == 10 # All unique
+ assert len(created_ids) == 10, f"expected 10 meetings created, got {len(created_ids)}"
+ assert len(set(created_ids)) == 10, "all meeting IDs should be unique"
def test_concurrent_reads_and_writes(self) -> None:
"""Test concurrent reads and writes are thread-safe."""
@@ -493,8 +499,8 @@ class TestMemoryUnitOfWork:
store = MeetingStore()
async with MemoryUnitOfWork(store) as uow:
- assert uow is not None
- assert isinstance(uow, MemoryUnitOfWork)
+ assert uow is not None, "context manager should return non-None UoW"
+ assert isinstance(uow, MemoryUnitOfWork), f"expected MemoryUnitOfWork, got {type(uow)}"
async def test_commit_is_noop(self) -> None:
"""Test commit is a no-op (changes already applied)."""
@@ -506,7 +512,7 @@ class TestMemoryUnitOfWork:
await uow.commit()
retrieved = await uow.meetings.get(meeting.id)
- assert retrieved is not None
+ assert retrieved is not None, "meeting should exist after commit"
async def test_rollback_does_not_undo_changes(self) -> None:
"""Test rollback does not undo changes (memory doesn't support rollback)."""
@@ -517,29 +523,29 @@ class TestMemoryUnitOfWork:
await uow.meetings.create(meeting)
await uow.rollback()
- # Meeting still exists after rollback
- assert store.get(str(meeting.id)) is not None
+ # Meeting still exists after rollback (memory doesn't support rollback)
+ assert store.get(str(meeting.id)) is not None, "meeting should persist despite rollback in memory mode"
async def test_feature_flag_annotations(self) -> None:
"""Test annotations feature flag is False."""
store = MeetingStore()
uow = MemoryUnitOfWork(store)
- assert uow.supports_annotations is False
+ assert uow.supports_annotations is False, "memory UoW should not support annotations"
async def test_feature_flag_diarization_jobs(self) -> None:
"""Test diarization jobs feature flag is False."""
store = MeetingStore()
uow = MemoryUnitOfWork(store)
- assert uow.supports_diarization_jobs is False
+ assert uow.supports_diarization_jobs is False, "memory UoW should not support diarization jobs"
async def test_feature_flag_preferences(self) -> None:
"""Test preferences feature flag is False."""
store = MeetingStore()
uow = MemoryUnitOfWork(store)
- assert uow.supports_preferences is False
+ assert uow.supports_preferences is False, "memory UoW should not support preferences"
@pytest.mark.integration
@@ -556,8 +562,8 @@ class TestMemoryMeetingRepository:
retrieved = await repo.get(meeting.id)
- assert retrieved is not None
- assert retrieved.id == meeting.id
+ assert retrieved is not None, "meeting should be retrievable after create"
+ assert retrieved.id == meeting.id, f"expected id {meeting.id}, got {retrieved.id}"
async def test_update(self) -> None:
"""Test updating a meeting."""
@@ -571,8 +577,8 @@ class TestMemoryMeetingRepository:
await repo.update(meeting)
retrieved = await repo.get(meeting.id)
- assert retrieved is not None
- assert retrieved.title == "Updated"
+ assert retrieved is not None, "meeting should exist after update"
+ assert retrieved.title == "Updated", f"expected 'Updated', got {retrieved.title!r}"
async def test_delete(self) -> None:
"""Test deleting a meeting."""
@@ -584,8 +590,8 @@ class TestMemoryMeetingRepository:
result = await repo.delete(meeting.id)
- assert result is True
- assert await repo.get(meeting.id) is None
+ assert result is True, "delete should return True for existing meeting"
+ assert await repo.get(meeting.id) is None, "meeting should not exist after delete"
async def test_list_all(self) -> None:
"""Test listing all meetings."""
@@ -598,8 +604,8 @@ class TestMemoryMeetingRepository:
meetings, total = await repo.list_all()
- assert len(meetings) == 5
- assert total == 5
+ assert len(meetings) == 5, f"expected 5 meetings, got {len(meetings)}"
+ assert total == 5, f"expected total=5, got {total}"
async def test_count_by_state_via_repo(self) -> None:
"""Test counting meetings by state via MemoryMeetingRepository."""
@@ -617,8 +623,8 @@ class TestMemoryMeetingRepository:
created_count = await repo.count_by_state(MeetingState.CREATED)
recording_count = await repo.count_by_state(MeetingState.RECORDING)
- assert created_count == 3
- assert recording_count == 1
+ assert created_count == 3, f"expected 3 created meetings, got {created_count}"
+ assert recording_count == 1, f"expected 1 recording meeting, got {recording_count}"
@pytest.mark.integration
@@ -644,8 +650,8 @@ class TestMemorySegmentRepository:
segments = await segment_repo.get_by_meeting(meeting.id)
- assert len(segments) == 1
- assert segments[0].text == "Test segment"
+ assert len(segments) == 1, f"expected 1 segment, got {len(segments)}"
+ assert segments[0].text == "Test segment", f"segment text mismatch: {segments[0].text!r}"
async def test_add_batch(self) -> None:
"""Test adding segments in batch."""
@@ -664,7 +670,7 @@ class TestMemorySegmentRepository:
retrieved = await segment_repo.get_by_meeting(meeting.id)
- assert len(retrieved) == 5
+ assert len(retrieved) == 5, f"expected 5 segments after batch add, got {len(retrieved)}"
async def test_semantic_search_returns_empty(self) -> None:
"""Test semantic search returns empty (not supported)."""
@@ -673,7 +679,7 @@ class TestMemorySegmentRepository:
results = await segment_repo.search_semantic([0.1, 0.2, 0.3])
- assert results == []
+ assert results == [], f"semantic search should return empty in memory mode, got {results}"
async def test_get_next_segment_id_via_repo(self) -> None:
"""Test getting next segment ID via MemorySegmentRepository."""
@@ -685,13 +691,13 @@ class TestMemorySegmentRepository:
await meeting_repo.create(meeting)
next_id = await segment_repo.compute_next_segment_id(meeting.id)
- assert next_id == 0
+ assert next_id == 0, f"expected next_id=0 for empty meeting, got {next_id}"
segment = Segment(segment_id=0, text="First", start_time=0.0, end_time=1.0)
await segment_repo.add(meeting.id, segment)
next_id = await segment_repo.compute_next_segment_id(meeting.id)
- assert next_id == 1
+ assert next_id == 1, f"expected next_id=1 after adding segment, got {next_id}"
@pytest.mark.integration
@@ -715,8 +721,10 @@ class TestMemorySummaryRepository:
retrieved = await summary_repo.get_by_meeting(meeting.id)
- assert retrieved is not None
- assert retrieved.executive_summary == "Executive summary content"
+ assert retrieved is not None, "summary should be retrievable after save"
+ assert retrieved.executive_summary == "Executive summary content", (
+ f"summary mismatch: {retrieved.executive_summary!r}"
+ )
async def test_delete_by_meeting(self) -> None:
"""Test deleting summary by meeting ID."""
@@ -732,8 +740,8 @@ class TestMemorySummaryRepository:
result = await summary_repo.delete_by_meeting(meeting.id)
- assert result is True
- assert await summary_repo.get_by_meeting(meeting.id) is None
+ assert result is True, "delete_by_meeting should return True when summary existed"
+ assert await summary_repo.get_by_meeting(meeting.id) is None, "summary should not exist after delete"
@pytest.mark.integration
@@ -876,7 +884,7 @@ class TestGrpcServicerMemoryFallback:
"""Test servicer falls back to memory store without session factory."""
servicer = NoteFlowServicer(session_factory=None)
- assert servicer._memory_store is not None
+ assert servicer._memory_store is not None, "servicer should have memory store when no session_factory"
async def test_create_meeting_in_memory_mode(self) -> None:
"""Test creating meeting works in memory mode."""
@@ -885,8 +893,8 @@ class TestGrpcServicerMemoryFallback:
request = noteflow_pb2.CreateMeetingRequest(title="Memory Meeting")
result = await servicer.CreateMeeting(request, MockContext())
- assert result.id
- assert result.title == "Memory Meeting"
+ assert result.id, "created meeting should have an ID"
+ assert result.title == "Memory Meeting", f"expected 'Memory Meeting', got {result.title!r}"
async def test_get_meeting_in_memory_mode(self) -> None:
"""Test getting meeting works in memory mode."""
@@ -898,8 +906,8 @@ class TestGrpcServicerMemoryFallback:
get_request = noteflow_pb2.GetMeetingRequest(meeting_id=created.id)
result = await servicer.GetMeeting(get_request, MockContext())
- assert result.id == created.id
- assert result.title == "Get Memory Test"
+ assert result.id == created.id, f"expected id {created.id}, got {result.id}"
+ assert result.title == "Get Memory Test", f"expected 'Get Memory Test', got {result.title!r}"
async def test_list_meetings_in_memory_mode(self) -> None:
"""Test listing meetings works in memory mode."""
@@ -912,7 +920,7 @@ class TestGrpcServicerMemoryFallback:
list_request = noteflow_pb2.ListMeetingsRequest()
result = await servicer.ListMeetings(list_request, MockContext())
- assert len(result.meetings) == 3
+ assert len(result.meetings) == 3, f"expected 3 meetings, got {len(result.meetings)}"
async def test_delete_meeting_in_memory_mode(self) -> None:
"""Test deleting meeting works in memory mode."""
@@ -924,13 +932,15 @@ class TestGrpcServicerMemoryFallback:
delete_request = noteflow_pb2.DeleteMeetingRequest(meeting_id=created.id)
result = await servicer.DeleteMeeting(delete_request, MockContext())
- assert result.success is True
+ assert result.success is True, "delete should succeed"
get_request = noteflow_pb2.GetMeetingRequest(meeting_id=created.id)
context = MockContext()
with pytest.raises(grpc.RpcError, match=r".*"):
await servicer.GetMeeting(get_request, context)
- assert context.abort_code == grpc.StatusCode.NOT_FOUND
+ assert context.abort_code == grpc.StatusCode.NOT_FOUND, (
+ f"expected NOT_FOUND, got {context.abort_code}"
+ )
async def test_get_server_info_in_memory_mode(self) -> None:
"""Test GetServerInfo works in memory mode."""
@@ -939,7 +949,7 @@ class TestGrpcServicerMemoryFallback:
request = noteflow_pb2.ServerInfoRequest()
result = await servicer.GetServerInfo(request, MockContext())
- assert result.active_meetings >= 0
+ assert result.active_meetings >= 0, f"active_meetings should be non-negative, got {result.active_meetings}"
async def test_annotation_operations_fail_in_memory_mode(self) -> None:
"""Test annotation operations fail gracefully in memory mode."""
@@ -960,7 +970,9 @@ class TestGrpcServicerMemoryFallback:
with pytest.raises(grpc.RpcError, match=r".*"):
await servicer.AddAnnotation(add_request, context)
- assert context.abort_code == grpc.StatusCode.UNIMPLEMENTED
+ assert context.abort_code == grpc.StatusCode.UNIMPLEMENTED, (
+ f"expected UNIMPLEMENTED for annotations in memory mode, got {context.abort_code}"
+ )
@pytest.mark.integration
@@ -985,8 +997,8 @@ class TestMemoryModeConstraints:
# Verify through store directly
stored_meeting = store.get(str(meeting.id))
- assert stored_meeting is not None
- assert len(stored_meeting.segments) == 1
+ assert stored_meeting is not None, "meeting should exist in store"
+ assert len(stored_meeting.segments) == 1, f"expected 1 segment, got {len(stored_meeting.segments)}"
async def test_memory_mode_summary_persists_on_meeting(self) -> None:
"""Test summary is stored on meeting entity in memory mode."""
@@ -1004,9 +1016,11 @@ class TestMemoryModeConstraints:
# Verify through store directly
stored_meeting = store.get(str(meeting.id))
- assert stored_meeting is not None
- assert stored_meeting.summary is not None
- assert stored_meeting.summary.executive_summary == "Test summary"
+ assert stored_meeting is not None, "meeting should exist in store"
+ assert stored_meeting.summary is not None, "summary should be attached to meeting"
+ assert stored_meeting.summary.executive_summary == "Test summary", (
+ f"summary mismatch: {stored_meeting.summary.executive_summary!r}"
+ )
async def test_memory_mode_no_semantic_search(self) -> None:
"""Test semantic search is not available in memory mode."""
@@ -1026,7 +1040,7 @@ class TestMemoryModeConstraints:
results = await uow.segments.search_semantic([0.1] * 384)
- assert results == []
+ assert results == [], f"semantic search should return empty in memory mode, got {results}"
async def test_memory_mode_meetings_isolated(self) -> None:
"""Test meetings are isolated in memory mode."""
diff --git a/tests/integration/test_preferences_repository.py b/tests/integration/test_preferences_repository.py
index 615d048..848873e 100644
--- a/tests/integration/test_preferences_repository.py
+++ b/tests/integration/test_preferences_repository.py
@@ -44,7 +44,7 @@ class TestPreferencesRepositoryBasicOperations:
value = await repo.get("theme")
- assert value == "dark"
+ assert value == "dark", f"expected theme to be 'dark', got {value!r}"
async def test_set_and_get_integer_value(self, session: AsyncSession) -> None:
"""Test setting and getting an integer preference value."""
@@ -55,7 +55,7 @@ class TestPreferencesRepositoryBasicOperations:
value = await repo.get("max_recordings")
- assert value == 100
+ assert value == 100, f"expected max_recordings to be 100, got {value}"
async def test_set_and_get_boolean_value(self, session: AsyncSession) -> None:
"""Test setting and getting a boolean preference value."""
@@ -66,7 +66,7 @@ class TestPreferencesRepositoryBasicOperations:
value = await repo.get("notifications_enabled")
- assert value is True
+ assert value is True, f"expected notifications_enabled to be True, got {value}"
async def test_set_and_get_list_value(self, session: AsyncSession) -> None:
"""Test setting and getting a list preference value."""
@@ -78,7 +78,7 @@ class TestPreferencesRepositoryBasicOperations:
value = await repo.get("preferred_languages")
- assert value == languages
+ assert value == languages, f"expected preferred_languages to be {languages}, got {value}"
async def test_set_and_get_dict_value(self, session: AsyncSession) -> None:
"""Test setting and getting a dict preference value."""
@@ -90,7 +90,7 @@ class TestPreferencesRepositoryBasicOperations:
value = await repo.get("keyboard_shortcuts")
- assert value == shortcuts
+ assert value == shortcuts, f"expected keyboard_shortcuts to be {shortcuts}, got {value}"
async def test_get_nonexistent_key_returns_none(self, session: AsyncSession) -> None:
"""Test getting a key that doesn't exist returns None."""
@@ -98,7 +98,7 @@ class TestPreferencesRepositoryBasicOperations:
value = await repo.get("nonexistent_key")
- assert value is None
+ assert value is None, f"expected nonexistent key to return None, got {value!r}"
async def test_set_updates_existing_value(self, session: AsyncSession) -> None:
"""Test setting a key that already exists updates the value."""
@@ -112,7 +112,7 @@ class TestPreferencesRepositoryBasicOperations:
value = await repo.get("volume")
- assert value == VOLUME_UPDATED
+ assert value == VOLUME_UPDATED, f"expected volume to be updated to {VOLUME_UPDATED}, got {value}"
async def test_set_null_value(self, session: AsyncSession) -> None:
"""Test setting a None value explicitly."""
@@ -123,7 +123,7 @@ class TestPreferencesRepositoryBasicOperations:
value = await repo.get("optional_setting")
- assert value is None
+ assert value is None, f"expected explicitly set None value to return None, got {value!r}"
@pytest.mark.integration
@@ -139,7 +139,7 @@ class TestPreferencesRepositoryBooleanCoercion:
value = await repo.get_bool("cloud_consent_granted")
- assert value is True
+ assert value is True, f"expected get_bool to return True for truthy value, got {value}"
async def test_get_bool_with_false_value(self, session: AsyncSession) -> None:
"""Test get_bool returns False for falsy boolean value."""
@@ -150,7 +150,7 @@ class TestPreferencesRepositoryBooleanCoercion:
value = await repo.get_bool("cloud_consent_granted")
- assert value is False
+ assert value is False, f"expected get_bool to return False for falsy value, got {value}"
async def test_get_bool_with_default_when_missing(self, session: AsyncSession) -> None:
"""Test get_bool returns default when key doesn't exist."""
@@ -159,8 +159,8 @@ class TestPreferencesRepositoryBooleanCoercion:
value_default_false = await repo.get_bool("nonexistent")
value_default_true = await repo.get_bool("nonexistent", default=True)
- assert value_default_false is False
- assert value_default_true is True
+ assert value_default_false is False, f"expected default False when key missing, got {value_default_false}"
+ assert value_default_true is True, f"expected default True when explicitly passed, got {value_default_true}"
async def test_get_bool_coerces_truthy_integer(self, session: AsyncSession) -> None:
"""Test get_bool coerces non-zero integer to True."""
@@ -171,7 +171,7 @@ class TestPreferencesRepositoryBooleanCoercion:
value = await repo.get_bool("numeric_flag")
- assert value is True
+ assert value is True, f"expected non-zero integer to coerce to True, got {value}"
async def test_get_bool_coerces_falsy_integer(self, session: AsyncSession) -> None:
"""Test get_bool coerces zero to False."""
@@ -182,7 +182,7 @@ class TestPreferencesRepositoryBooleanCoercion:
value = await repo.get_bool("numeric_flag")
- assert value is False
+ assert value is False, f"expected zero to coerce to False, got {value}"
async def test_get_bool_coerces_truthy_string(self, session: AsyncSession) -> None:
"""Test get_bool coerces non-empty string to True."""
@@ -193,7 +193,7 @@ class TestPreferencesRepositoryBooleanCoercion:
value = await repo.get_bool("string_flag")
- assert value is True
+ assert value is True, f"expected non-empty string to coerce to True, got {value}"
async def test_get_bool_coerces_empty_string(self, session: AsyncSession) -> None:
"""Test get_bool coerces empty string to False."""
@@ -204,7 +204,7 @@ class TestPreferencesRepositoryBooleanCoercion:
value = await repo.get_bool("string_flag")
- assert value is False
+ assert value is False, f"expected empty string to coerce to False, got {value}"
@pytest.mark.integration
@@ -221,10 +221,10 @@ class TestPreferencesRepositoryDelete:
result = await repo.delete("to_delete")
await session.commit()
- assert result is True
+ assert result is True, f"expected delete to return True for existing key, got {result}"
value = await repo.get("to_delete")
- assert value is None
+ assert value is None, f"expected deleted key to return None, got {value!r}"
async def test_delete_nonexistent_key(self, session: AsyncSession) -> None:
"""Test deleting a key that doesn't exist returns False."""
@@ -233,7 +233,7 @@ class TestPreferencesRepositoryDelete:
result = await repo.delete("nonexistent")
await session.commit()
- assert result is False
+ assert result is False, f"expected delete to return False for nonexistent key, got {result}"
async def test_delete_then_recreate(self, session: AsyncSession) -> None:
"""Test that a deleted key can be recreated."""
@@ -250,7 +250,7 @@ class TestPreferencesRepositoryDelete:
value = await repo.get("recyclable")
- assert value == "new"
+ assert value == "new", f"expected recreated key to have value 'new', got {value!r}"
@pytest.mark.integration
@@ -266,9 +266,9 @@ class TestPreferencesRepositoryIsolation:
await repo.set("key3", "value3")
await session.commit()
- assert await repo.get("key1") == "value1"
- assert await repo.get("key2") == "value2"
- assert await repo.get("key3") == "value3"
+ assert await repo.get("key1") == "value1", "key1 should have independent value 'value1'"
+ assert await repo.get("key2") == "value2", "key2 should have independent value 'value2'"
+ assert await repo.get("key3") == "value3", "key3 should have independent value 'value3'"
async def test_updating_one_key_doesnt_affect_others(self, session: AsyncSession) -> None:
"""Test updating one key doesn't affect other keys."""
@@ -281,8 +281,8 @@ class TestPreferencesRepositoryIsolation:
await repo.set("changing", "updated")
await session.commit()
- assert await repo.get("stable") == "unchanged"
- assert await repo.get("changing") == "updated"
+ assert await repo.get("stable") == "unchanged", "stable key should remain unchanged"
+ assert await repo.get("changing") == "updated", "changing key should be updated"
async def test_deleting_one_key_doesnt_affect_others(self, session: AsyncSession) -> None:
"""Test deleting one key doesn't affect other keys."""
@@ -295,8 +295,8 @@ class TestPreferencesRepositoryIsolation:
await repo.delete("goner")
await session.commit()
- assert await repo.get("keeper") == "kept"
- assert await repo.get("goner") is None
+ assert await repo.get("keeper") == "kept", "keeper key should remain after deleting another key"
+ assert await repo.get("goner") is None, "goner key should be deleted"
@pytest.mark.integration
@@ -312,7 +312,7 @@ class TestPreferencesRepositoryEdgeCases:
value = await repo.get("")
- assert value == "empty_key_value"
+ assert value == "empty_key_value", f"expected empty string key to store value correctly, got {value!r}"
async def test_special_characters_in_key(self, session: AsyncSession) -> None:
"""Test keys with special characters work correctly."""
@@ -324,7 +324,7 @@ class TestPreferencesRepositoryEdgeCases:
value = await repo.get(special_key)
- assert value == "high"
+ assert value == "high", f"expected special character key to work, got {value!r}"
async def test_unicode_value(self, session: AsyncSession) -> None:
"""Test storing unicode values works correctly."""
@@ -335,7 +335,7 @@ class TestPreferencesRepositoryEdgeCases:
value = await repo.get("greeting")
- assert value == "Hello, World!"
+ assert value == "Hello, World!", f"expected unicode value to be preserved, got {value!r}"
async def test_nested_dict_value(self, session: AsyncSession) -> None:
"""Test storing nested dictionaries works correctly."""
@@ -371,9 +371,9 @@ class TestPreferencesRepositoryEdgeCases:
value = await repo.get("large_data")
- assert value == large_list
- assert isinstance(value, list)
- assert len(value) == 1000
+ assert value == large_list, "expected large list to be preserved exactly"
+ assert isinstance(value, list), f"expected list type, got {type(value)}"
+ assert len(value) == 1000, f"expected list length of 1000, got {len(value)}"
async def test_float_value_precision(self, session: AsyncSession) -> None:
"""Test float values maintain precision through JSONB storage."""
@@ -384,7 +384,7 @@ class TestPreferencesRepositoryEdgeCases:
value = await repo.get("audio_gain")
- assert value == pytest.approx(0.123456789)
+ assert value == pytest.approx(0.123456789), f"expected float precision to be maintained, got {value}"
@pytest.mark.integration
@@ -397,7 +397,7 @@ class TestPreferencesRepositoryCloudConsent:
consent = await repo.get_bool("cloud_consent_granted", False)
- assert consent is False
+ assert consent is False, f"expected cloud consent to default to False, got {consent}"
async def test_cloud_consent_workflow_grant_consent(self, session: AsyncSession) -> None:
"""Test granting cloud consent persists correctly."""
@@ -408,7 +408,7 @@ class TestPreferencesRepositoryCloudConsent:
consent = await repo.get_bool("cloud_consent_granted", False)
- assert consent is True
+ assert consent is True, f"expected cloud consent to be granted, got {consent}"
async def test_cloud_consent_workflow_revoke_consent(self, session: AsyncSession) -> None:
"""Test revoking cloud consent after granting."""
@@ -422,7 +422,7 @@ class TestPreferencesRepositoryCloudConsent:
consent = await repo.get_bool("cloud_consent_granted", False)
- assert consent is False
+ assert consent is False, f"expected cloud consent to be revoked, got {consent}"
async def test_cloud_consent_survives_session_restart(
self, session: AsyncSession
@@ -440,7 +440,7 @@ class TestPreferencesRepositoryCloudConsent:
consent = await repo.get_bool("cloud_consent_granted", False)
- assert consent is True
+ assert consent is True, f"expected cloud consent to survive session restart, got {consent}"
@pytest.mark.integration
@@ -472,8 +472,8 @@ class TestPreferencesRepositoryBulkOperations:
result = await repo.get_all(keys=["include"])
- assert "include" in result
- assert "exclude" not in result
+ assert "include" in result, "filtered key should be included in result"
+ assert "exclude" not in result, "non-filtered key should not be in result"
async def test_get_all_returns_empty_dict_when_no_prefs(
self, session: AsyncSession
@@ -483,7 +483,7 @@ class TestPreferencesRepositoryBulkOperations:
result = await repo.get_all()
- assert result == {}
+ assert result == {}, f"expected empty dict when no preferences exist, got {result}"
async def test_get_all_with_metadata_includes_timestamps(
self, session: AsyncSession
@@ -514,7 +514,7 @@ class TestPreferencesRepositoryBulkOperations:
max_ts = await repo.get_max_updated_at()
- assert max_ts is not None
+ assert max_ts is not None, "expected max_updated_at to return a timestamp when preferences exist"
async def test_get_max_updated_at_returns_none_when_empty(
self, session: AsyncSession
@@ -524,7 +524,7 @@ class TestPreferencesRepositoryBulkOperations:
max_ts = await repo.get_max_updated_at()
- assert max_ts is None
+ assert max_ts is None, f"expected max_updated_at to return None when no preferences exist, got {max_ts}"
async def test_set_bulk_creates_multiple_preferences(
self, session: AsyncSession
@@ -537,10 +537,10 @@ class TestPreferencesRepositoryBulkOperations:
result = await repo.get_all()
- assert len(result) == 3
- assert result["key1"] == "value1"
- assert result["key2"] == "value2"
- assert result["key3"] == "value3"
+ assert len(result) == 3, f"expected 3 preferences from bulk set, got {len(result)}"
+ assert result["key1"] == "value1", f"expected key1 to be 'value1', got {result.get('key1')!r}"
+ assert result["key2"] == "value2", f"expected key2 to be 'value2', got {result.get('key2')!r}"
+ assert result["key3"] == "value3", f"expected key3 to be 'value3', got {result.get('key3')!r}"
async def test_set_bulk_updates_existing_preferences(
self, session: AsyncSession
@@ -556,5 +556,5 @@ class TestPreferencesRepositoryBulkOperations:
result = await repo.get_all()
- assert result["theme"] == "dark"
- assert result["new_key"] == "new_value"
+ assert result["theme"] == "dark", f"expected bulk set to update existing key, got {result.get('theme')!r}"
+ assert result["new_key"] == "new_value", f"expected bulk set to create new key, got {result.get('new_key')!r}"
diff --git a/tests/integration/test_project_repository.py b/tests/integration/test_project_repository.py
index 1c6610b..e9c3e5b 100644
--- a/tests/integration/test_project_repository.py
+++ b/tests/integration/test_project_repository.py
@@ -87,7 +87,7 @@ class TestProjectRepository:
result = await repo.get(uuid4())
- assert result is None
+ assert result is None, "non-existent project should return None"
async def test_create_project_with_settings_repository(self, session: AsyncSession) -> None:
"""Test creating project with full settings."""
@@ -158,7 +158,7 @@ class TestProjectRepository:
result = await repo.get_by_slug(workspace.id, "nonexistent")
- assert result is None
+ assert result is None, "non-existent project should return None"
async def test_get_default_for_workspace(self, session: AsyncSession) -> None:
"""Test retrieving default project for workspace."""
@@ -193,7 +193,7 @@ class TestProjectRepository:
result = await repo.get_default_for_workspace(workspace.id)
- assert result is None
+ assert result is None, "non-existent project should return None"
async def test_update_project(self, session: AsyncSession) -> None:
"""Test updating a project."""
@@ -209,7 +209,7 @@ class TestProjectRepository:
# Update via domain entity
retrieved = await repo.get(project.id)
- assert retrieved is not None
+ assert retrieved is not None, "project should exist for update"
retrieved.update_name("Updated Name")
retrieved.update_description("New description")
retrieved.update_settings(ProjectSettings(rag_enabled=True))
@@ -271,7 +271,7 @@ class TestProjectRepository:
# Get fresh reference
default = await repo.get_default_for_workspace(workspace.id)
- assert default is not None
+ assert default is not None, "default project should exist before archive attempt"
with pytest.raises(CannotArchiveDefaultProjectError, match="Cannot archive"):
await repo.archive(default.id)
@@ -282,7 +282,7 @@ class TestProjectRepository:
result = await repo.archive(uuid4())
- assert result is None
+ assert result is None, "non-existent project should return None"
async def test_restore_project(self, session: AsyncSession) -> None:
"""Test restoring an archived project."""
@@ -312,7 +312,7 @@ class TestProjectRepository:
result = await repo.restore(uuid4())
- assert result is None
+ assert result is None, "non-existent project should return None"
async def test_delete_project(self, session: AsyncSession) -> None:
"""Test deleting a project."""
@@ -329,10 +329,10 @@ class TestProjectRepository:
result = await repo.delete(project.id)
await session.commit()
- assert result is True
+ assert result is True, "delete should return True for existing project"
retrieved = await repo.get(project.id)
- assert retrieved is None
+ assert retrieved is None, "deleted project should not be retrievable"
async def test_delete_project_not_found(self, session: AsyncSession) -> None:
"""Test deleting non-existent project returns False."""
@@ -340,7 +340,7 @@ class TestProjectRepository:
result = await repo.delete(uuid4())
- assert result is False
+ assert result is False, "delete should return False for non-existent project"
async def test_list_for_workspace(self, session: AsyncSession) -> None:
"""Test listing projects in a workspace."""
@@ -368,10 +368,10 @@ class TestProjectRepository:
result = await repo.list_for_workspace(workspace.id)
- assert len(result) == 3
+ assert len(result) == 3, f"expected 3 projects, got {len(result)}" # noqa: PLR2004
# Default project should come first, then alphabetically
- assert result[0].is_default is True
- assert result[0].name == "Default Project"
+ assert result[0].is_default is True, "first project should be default"
+ assert result[0].name == "Default Project", f"first project should be 'Default Project', got '{result[0].name}'"
async def test_list_for_workspace_excludes_archived(self, session: AsyncSession) -> None:
"""Test list_for_workspace excludes archived by default."""
@@ -395,8 +395,8 @@ class TestProjectRepository:
result = await repo.list_for_workspace(workspace.id, include_archived=False)
- assert len(result) == 1
- assert result[0].id == active.id
+ assert len(result) == 1, f"expected 1 active project, got {len(result)}"
+ assert result[0].id == active.id, f"expected active project ID {active.id}, got {result[0].id}"
async def test_list_for_workspace_includes_archived(self, session: AsyncSession) -> None:
"""Test list_for_workspace can include archived projects."""
@@ -420,7 +420,7 @@ class TestProjectRepository:
result = await repo.list_for_workspace(workspace.id, include_archived=True)
- assert len(result) == 2
+ assert len(result) == 2, f"expected 2 projects (including archived), got {len(result)}" # noqa: PLR2004
async def test_list_for_workspace_pagination(self, session: AsyncSession) -> None:
"""Test list_for_workspace with pagination."""
@@ -428,7 +428,7 @@ class TestProjectRepository:
repo = SqlAlchemyProjectRepository(session)
# Create 5 projects
- for i in range(5):
+ for i in range(5): # noqa: PLR2004
await repo.create(
project_id=uuid4(),
workspace_id=workspace.id,
@@ -436,14 +436,14 @@ class TestProjectRepository:
)
await session.commit()
- result = await repo.list_for_workspace(workspace.id, limit=2, offset=0)
- assert len(result) == 2
+ result = await repo.list_for_workspace(workspace.id, limit=2, offset=0) # noqa: PLR2004
+ assert len(result) == 2, "first page should be full" # noqa: PLR2004
- result = await repo.list_for_workspace(workspace.id, limit=2, offset=2)
- assert len(result) == 2
+ result = await repo.list_for_workspace(workspace.id, limit=2, offset=2) # noqa: PLR2004
+ assert len(result) == 2, "second page should be full" # noqa: PLR2004
- result = await repo.list_for_workspace(workspace.id, limit=2, offset=4)
- assert len(result) == 1
+ result = await repo.list_for_workspace(workspace.id, limit=2, offset=4) # noqa: PLR2004
+ assert len(result) == 1, "last page has remainder"
async def test_count_for_workspace(self, session: AsyncSession) -> None:
"""Test counting projects in a workspace."""
@@ -451,7 +451,7 @@ class TestProjectRepository:
repo = SqlAlchemyProjectRepository(session)
# Create 3 active + 1 archived
- for _ in range(3):
+ for _ in range(3): # noqa: PLR2004
await repo.create(
project_id=uuid4(),
workspace_id=workspace.id,
@@ -470,8 +470,8 @@ class TestProjectRepository:
count_active = await repo.count_for_workspace(workspace.id, include_archived=False)
count_all = await repo.count_for_workspace(workspace.id, include_archived=True)
- assert count_active == 3
- assert count_all == 4
+ assert count_active == 3, f"expected 3 active projects, got {count_active}" # noqa: PLR2004
+ assert count_all == 4, f"expected 4 total projects, got {count_all}" # noqa: PLR2004
@pytest.mark.integration
@@ -516,7 +516,7 @@ class TestProjectMembershipRepository:
result = await repo.get(uuid4(), uuid4())
- assert result is None
+ assert result is None, "non-existent membership should return None"
async def test_update_role(self, session: AsyncSession) -> None:
"""Test updating a member's role."""
@@ -537,8 +537,8 @@ class TestProjectMembershipRepository:
# Verify persistence
retrieved = await repo.get(project.id, user.id)
- assert retrieved is not None
- assert retrieved.role == ProjectRole.ADMIN
+ assert retrieved is not None, "membership should exist after update"
+ assert retrieved.role == ProjectRole.ADMIN, "persisted role should be ADMIN"
async def test_update_role_not_found(self, session: AsyncSession) -> None:
"""Test update_role returns None for non-existent membership."""
@@ -546,7 +546,7 @@ class TestProjectMembershipRepository:
result = await repo.update_role(uuid4(), uuid4(), ProjectRole.ADMIN)
- assert result is None
+ assert result is None, "non-existent membership should return None"
async def test_remove_membership(self, session: AsyncSession) -> None:
"""Test removing a project membership."""
@@ -562,10 +562,10 @@ class TestProjectMembershipRepository:
result = await repo.remove(project.id, user.id)
await session.commit()
- assert result is True
+ assert result is True, "remove should return True for existing membership"
retrieved = await repo.get(project.id, user.id)
- assert retrieved is None
+ assert retrieved is None, "removed membership should not be retrievable"
async def test_remove_membership_not_found(self, session: AsyncSession) -> None:
"""Test remove returns False for non-existent membership."""
@@ -573,7 +573,7 @@ class TestProjectMembershipRepository:
result = await repo.remove(uuid4(), uuid4())
- assert result is False
+ assert result is False, "remove should return False for non-existent membership"
async def test_list_for_project(self, session: AsyncSession) -> None:
"""Test listing all members of a project."""
@@ -583,7 +583,7 @@ class TestProjectMembershipRepository:
# Create multiple users
users = []
- for i in range(3):
+ for i in range(3): # noqa: PLR2004
user = UserModel(
id=uuid4(),
display_name=f"User {i}",
@@ -602,9 +602,9 @@ class TestProjectMembershipRepository:
result = await repo.list_for_project(project.id)
- assert len(result) == 3
+ assert len(result) == 3, f"expected 3 members, got {len(result)}" # noqa: PLR2004
roles_found = {m.role for m in result}
- assert roles_found == {ProjectRole.ADMIN, ProjectRole.EDITOR, ProjectRole.VIEWER}
+ assert roles_found == {ProjectRole.ADMIN, ProjectRole.EDITOR, ProjectRole.VIEWER}, "all roles should be present"
async def test_list_for_project_pagination(self, session: AsyncSession) -> None:
"""Test list_for_project with pagination."""
@@ -614,7 +614,7 @@ class TestProjectMembershipRepository:
# Create 5 users and memberships
repo = SqlAlchemyProjectMembershipRepository(session)
- for i in range(5):
+ for i in range(5): # noqa: PLR2004
user = UserModel(
id=uuid4(),
display_name=f"User {i}",
@@ -626,11 +626,11 @@ class TestProjectMembershipRepository:
await repo.add(project.id, user.id, ProjectRole.VIEWER)
await session.commit()
- result = await repo.list_for_project(project.id, limit=2, offset=0)
- assert len(result) == 2
+ result = await repo.list_for_project(project.id, limit=2, offset=0) # noqa: PLR2004
+ assert len(result) == 2, "first page should have 2 members" # noqa: PLR2004
- result = await repo.list_for_project(project.id, limit=2, offset=4)
- assert len(result) == 1
+ result = await repo.list_for_project(project.id, limit=2, offset=4) # noqa: PLR2004
+ assert len(result) == 1, "last page should have 1 member"
async def test_list_for_user(self, session: AsyncSession) -> None:
"""Test listing all projects a user is a member of."""
@@ -642,7 +642,7 @@ class TestProjectMembershipRepository:
project_repo = SqlAlchemyProjectRepository(session)
membership_repo = SqlAlchemyProjectMembershipRepository(session)
- for i in range(3):
+ for i in range(3): # noqa: PLR2004
project = await project_repo.create(
project_id=uuid4(),
workspace_id=workspace.id,
@@ -653,8 +653,8 @@ class TestProjectMembershipRepository:
result = await membership_repo.list_for_user(user.id)
- assert len(result) == 3
- assert all(m.user_id == user.id for m in result)
+ assert len(result) == 3, f"expected 3 memberships, got {len(result)}" # noqa: PLR2004
+ assert all(m.user_id == user.id for m in result), "all memberships should belong to user"
async def test_list_for_user_filtered_by_workspace(self, session: AsyncSession) -> None:
"""Test list_for_user can filter by workspace."""
@@ -674,7 +674,7 @@ class TestProjectMembershipRepository:
membership_repo = SqlAlchemyProjectMembershipRepository(session)
# Projects in workspace1
- for i in range(2):
+ for i in range(2): # noqa: PLR2004
project = await project_repo.create(
project_id=uuid4(),
workspace_id=workspace1.id,
@@ -693,12 +693,12 @@ class TestProjectMembershipRepository:
# Filter by workspace1
result = await membership_repo.list_for_user(user.id, workspace_id=workspace1.id)
- assert len(result) == 2
+ assert len(result) == 2, f"expected 2 memberships in workspace1, got {len(result)}" # noqa: PLR2004
# Filter by workspace2
result = await membership_repo.list_for_user(user.id, workspace_id=workspace2.id)
- assert len(result) == 1
- assert result[0].role == ProjectRole.ADMIN
+ assert len(result) == 1, f"expected 1 membership in workspace2, got {len(result)}"
+ assert result[0].role == ProjectRole.ADMIN, "workspace2 membership should have ADMIN role"
async def test_bulk_add_memberships(self, session: AsyncSession) -> None:
"""Test bulk adding memberships."""
@@ -708,7 +708,7 @@ class TestProjectMembershipRepository:
# Create users
users = []
- for i in range(3):
+ for i in range(3): # noqa: PLR2004
user = UserModel(
id=uuid4(),
display_name=f"User {i}",
@@ -728,9 +728,9 @@ class TestProjectMembershipRepository:
result = await repo.bulk_add(project.id, memberships)
await session.commit()
- assert len(result) == 3
+ assert len(result) == 3, f"expected 3 memberships created, got {len(result)}" # noqa: PLR2004
roles = {m.role for m in result}
- assert roles == {ProjectRole.ADMIN, ProjectRole.EDITOR, ProjectRole.VIEWER}
+ assert roles == {ProjectRole.ADMIN, ProjectRole.EDITOR, ProjectRole.VIEWER}, "all roles should be present"
async def test_count_for_project(self, session: AsyncSession) -> None:
"""Test counting members in a project."""
@@ -740,7 +740,7 @@ class TestProjectMembershipRepository:
# Create users and add memberships
repo = SqlAlchemyProjectMembershipRepository(session)
- for i in range(5):
+ for i in range(5): # noqa: PLR2004
user = UserModel(
id=uuid4(),
display_name=f"User {i}",
@@ -754,7 +754,7 @@ class TestProjectMembershipRepository:
count = await repo.count_for_project(project.id)
- assert count == 5, "Should count all members in project"
+ assert count == 5, "Should count all members in project" # noqa: PLR2004
async def test_count_for_project_empty(self, session: AsyncSession) -> None:
"""Test counting members returns 0 for empty project."""
diff --git a/tests/integration/test_recovery_service.py b/tests/integration/test_recovery_service.py
index efb3bea..cf7baff 100644
--- a/tests/integration/test_recovery_service.py
+++ b/tests/integration/test_recovery_service.py
@@ -70,9 +70,9 @@ class TestRecoveryServiceMeetingRecovery:
recovery_service = RecoveryService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
recovered, _ = await recovery_service.recover_crashed_meetings()
- assert len(recovered) == 1
- assert recovered[0].state == MeetingState.ERROR
- assert recovered[0].metadata["crash_previous_state"] == "STOPPING"
+ assert len(recovered) == 1, "should recover exactly one crashed meeting"
+ assert recovered[0].state == MeetingState.ERROR, "recovered meeting should be in ERROR state"
+ assert recovered[0].metadata["crash_previous_state"] == "STOPPING", "crash_previous_state should be 'STOPPING'"
async def test_ignores_created_meetings(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -86,12 +86,12 @@ class TestRecoveryServiceMeetingRecovery:
recovery_service = RecoveryService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
recovered, _ = await recovery_service.recover_crashed_meetings()
- assert len(recovered) == 0
+ assert len(recovered) == 0, "should not recover any meetings when only CREATED state exists"
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
m = await uow.meetings.get(meeting.id)
- assert m is not None
- assert m.state == MeetingState.CREATED
+ assert m is not None, f"meeting {meeting.id} should exist in database"
+ assert m.state == MeetingState.CREATED, f"meeting should remain in CREATED state, got {m.state}"
async def test_ignores_stopped_meetings(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -108,7 +108,7 @@ class TestRecoveryServiceMeetingRecovery:
recovery_service = RecoveryService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
recovered, _ = await recovery_service.recover_crashed_meetings()
- assert len(recovered) == 0
+ assert len(recovered) == 0, "should not recover any meetings when only STOPPED state exists"
async def test_ignores_error_meetings(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -124,29 +124,46 @@ class TestRecoveryServiceMeetingRecovery:
recovery_service = RecoveryService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
recovered, _ = await recovery_service.recover_crashed_meetings()
- assert len(recovered) == 0
+ assert len(recovered) == 0, "should not recover any meetings when already in ERROR state"
async def test_recovers_multiple_meetings(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
) -> None:
"""Test recovering multiple crashed meetings at once."""
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
- for i in range(5):
- meeting = Meeting.create(title=f"Crashed Meeting {i}")
- meeting.start_recording()
- await uow.meetings.create(meeting)
+ meeting0 = Meeting.create(title="Crashed Meeting 0")
+ meeting0.start_recording()
+ await uow.meetings.create(meeting0)
+
+ meeting1 = Meeting.create(title="Crashed Meeting 1")
+ meeting1.start_recording()
+ await uow.meetings.create(meeting1)
+
+ meeting2 = Meeting.create(title="Crashed Meeting 2")
+ meeting2.start_recording()
+ await uow.meetings.create(meeting2)
+
+ meeting3 = Meeting.create(title="Crashed Meeting 3")
+ meeting3.start_recording()
+ await uow.meetings.create(meeting3)
+
+ meeting4 = Meeting.create(title="Crashed Meeting 4")
+ meeting4.start_recording()
+ await uow.meetings.create(meeting4)
+
await uow.commit()
+ expected_ids = {meeting0.id, meeting1.id, meeting2.id, meeting3.id, meeting4.id}
recovery_service = RecoveryService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
recovered, _ = await recovery_service.recover_crashed_meetings()
- assert len(recovered) == 5
+ assert len(recovered) == len(expected_ids), f"should recover all crashed meetings, got {len(recovered)}"
- async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
- for m in recovered:
- retrieved = await uow.meetings.get(m.id)
- assert retrieved is not None
- assert retrieved.state == MeetingState.ERROR
+ recovered_ids = {m.id for m in recovered}
+ assert recovered_ids == expected_ids, f"recovered meeting IDs should match expected: {expected_ids}"
+
+ recovered_states = {m.state for m in recovered}
+ assert recovered_states == {MeetingState.ERROR}, f"all recovered meetings should be in ERROR state, got {recovered_states}"
async def test_recovery_metadata_includes_timestamp(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -161,9 +178,9 @@ class TestRecoveryServiceMeetingRecovery:
recovery_service = RecoveryService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
recovered, _ = await recovery_service.recover_crashed_meetings()
- assert len(recovered) == 1
- assert "crash_recovery_time" in recovered[0].metadata
- assert recovered[0].metadata["crash_recovery_time"]
+ assert len(recovered) == 1, "should recover exactly one crashed meeting"
+ assert "crash_recovery_time" in recovered[0].metadata, "recovered meeting metadata should include crash_recovery_time"
+ assert recovered[0].metadata["crash_recovery_time"], "crash_recovery_time should have a non-empty value"
@pytest.mark.integration
@@ -216,7 +233,7 @@ class TestRecoveryServiceDiarizationJobRecovery:
recovery_service = RecoveryService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
failed_count = await recovery_service.recover_crashed_diarization_jobs()
- assert failed_count == 1
+ assert failed_count == 1, "should fail exactly one running diarization job"
async def test_ignores_completed_diarization_jobs(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -238,12 +255,12 @@ class TestRecoveryServiceDiarizationJobRecovery:
recovery_service = RecoveryService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
failed_count = await recovery_service.recover_crashed_diarization_jobs()
- assert failed_count == 0
+ assert failed_count == 0, "should not fail any completed diarization jobs"
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
retrieved = await uow.diarization_jobs.get(job.job_id)
- assert retrieved is not None
- assert retrieved.status == JOB_STATUS_COMPLETED
+ assert retrieved is not None, f"diarization job {job.job_id} should exist in database"
+ assert retrieved.status == JOB_STATUS_COMPLETED, f"completed job should remain COMPLETED, got {retrieved.status}"
async def test_ignores_already_failed_diarization_jobs(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -265,12 +282,12 @@ class TestRecoveryServiceDiarizationJobRecovery:
recovery_service = RecoveryService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
failed_count = await recovery_service.recover_crashed_diarization_jobs()
- assert failed_count == 0
+ assert failed_count == 0, "should not fail any already-failed diarization jobs"
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
retrieved = await uow.diarization_jobs.get(job.job_id)
- assert retrieved is not None
- assert retrieved.error_message == "Original failure"
+ assert retrieved is not None, f"diarization job {job.job_id} should exist in database"
+ assert retrieved.error_message == "Original failure", f"original error message should be preserved, got '{retrieved.error_message}'"
@pytest.mark.integration
@@ -324,9 +341,9 @@ class TestRecoveryServiceFullRecovery:
recovery_service = RecoveryService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
result = await recovery_service.recover_all()
- assert result.meetings_recovered == 0
- assert result.diarization_jobs_failed == 0
- assert result.total_recovered == 0
+ assert result.meetings_recovered == 0, "should not recover any meetings when none crashed"
+ assert result.diarization_jobs_failed == 0, "should not fail any diarization jobs when none crashed"
+ assert result.total_recovered == 0, "total_recovered should be 0 when nothing to recover"
@pytest.mark.integration
@@ -338,30 +355,39 @@ class TestRecoveryServiceCounting:
) -> None:
"""Test count_crashed_meetings returns accurate count."""
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
- for state, title in [
- (MeetingState.CREATED, "Created"),
- (MeetingState.RECORDING, "Recording 1"),
- (MeetingState.RECORDING, "Recording 2"),
- (MeetingState.STOPPING, "Stopping"),
- (MeetingState.STOPPED, "Stopped"),
- ]:
- meeting = Meeting.create(title=title)
- if state == MeetingState.RECORDING:
- meeting.start_recording()
- elif state == MeetingState.STOPPING:
- meeting.start_recording()
- meeting.begin_stopping()
- elif state == MeetingState.STOPPED:
- meeting.start_recording()
- meeting.begin_stopping()
- meeting.stop_recording()
- await uow.meetings.create(meeting)
+ # CREATED state meeting
+ meeting_created = Meeting.create(title="Created")
+ await uow.meetings.create(meeting_created)
+
+ # First RECORDING state meeting
+ meeting_recording1 = Meeting.create(title="Recording 1")
+ meeting_recording1.start_recording()
+ await uow.meetings.create(meeting_recording1)
+
+ # Second RECORDING state meeting
+ meeting_recording2 = Meeting.create(title="Recording 2")
+ meeting_recording2.start_recording()
+ await uow.meetings.create(meeting_recording2)
+
+ # STOPPING state meeting
+ meeting_stopping = Meeting.create(title="Stopping")
+ meeting_stopping.start_recording()
+ meeting_stopping.begin_stopping()
+ await uow.meetings.create(meeting_stopping)
+
+ # STOPPED state meeting
+ meeting_stopped = Meeting.create(title="Stopped")
+ meeting_stopped.start_recording()
+ meeting_stopped.begin_stopping()
+ meeting_stopped.stop_recording()
+ await uow.meetings.create(meeting_stopped)
+
await uow.commit()
recovery_service = RecoveryService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
count = await recovery_service.count_crashed_meetings()
- assert count == 3
+ assert count == 3, f"should count 3 crashed meetings (2 RECORDING + 1 STOPPING), got {count}"
@pytest.mark.integration
@@ -393,9 +419,9 @@ class TestRecoveryServiceAudioValidation:
)
recovered, audio_failures = await recovery_service.recover_crashed_meetings()
- assert len(recovered) == 1
- assert audio_failures == 0
- assert recovered[0].metadata["audio_valid"] == "true"
+ assert len(recovered) == 1, "should recover exactly one meeting"
+ assert audio_failures == 0, "should report no audio validation failures when files exist"
+ assert recovered[0].metadata["audio_valid"] == "true", "audio_valid should be 'true' when manifest and audio.enc exist"
async def test_audio_validation_with_missing_audio(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -475,9 +501,9 @@ class TestRecoveryServiceAudioValidation:
)
recovered, audio_failures = await recovery_service.recover_crashed_meetings()
- assert len(recovered) == 1
- assert audio_failures == 1
- assert recovered[0].metadata["audio_valid"] == "false"
+ assert len(recovered) == 1, "should recover exactly one meeting"
+ assert audio_failures == 1, "should report one audio validation failure when directory missing"
+ assert recovered[0].metadata["audio_valid"] == "false", "audio_valid should be 'false' when meeting directory missing"
async def test_audio_validation_skipped_without_meetings_dir(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -492,9 +518,9 @@ class TestRecoveryServiceAudioValidation:
recovery_service = RecoveryService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
recovered, audio_failures = await recovery_service.recover_crashed_meetings()
- assert len(recovered) == 1
- assert audio_failures == 0
- assert recovered[0].metadata["audio_valid"] == "true"
+ assert len(recovered) == 1, "should recover exactly one meeting"
+ assert audio_failures == 0, "should report no audio validation failures when meetings_dir not provided"
+ assert recovered[0].metadata["audio_valid"] == "true", "audio_valid should default to 'true' when validation skipped"
async def test_audio_validation_uses_asset_path(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -522,6 +548,6 @@ class TestRecoveryServiceAudioValidation:
)
recovered, audio_failures = await recovery_service.recover_crashed_meetings()
- assert len(recovered) == 1
- assert audio_failures == 0
- assert recovered[0].metadata["audio_valid"] == "true"
+ assert len(recovered) == 1, "should recover exactly one meeting"
+ assert audio_failures == 0, "should report no audio validation failures when custom path files exist"
+ assert recovered[0].metadata["audio_valid"] == "true", "audio_valid should be 'true' when files exist at custom asset_path"
diff --git a/tests/integration/test_repositories.py b/tests/integration/test_repositories.py
index d5ecbcb..0cd75dc 100644
--- a/tests/integration/test_repositories.py
+++ b/tests/integration/test_repositories.py
@@ -51,7 +51,7 @@ class TestMeetingRepository:
result = await repo.get(meeting_id)
- assert result is None
+ assert result is None, f"expected None for non-existent meeting, got {result}"
async def test_update_meeting(self, session: AsyncSession) -> None:
"""Test updating a meeting."""
@@ -67,9 +67,9 @@ class TestMeetingRepository:
# Verify
retrieved = await repo.get(meeting.id)
- assert retrieved is not None
- assert retrieved.state == MeetingState.RECORDING
- assert retrieved.started_at is not None
+ assert retrieved is not None, "updated meeting should exist"
+ assert retrieved.state == MeetingState.RECORDING, f"expected RECORDING state, got {retrieved.state}"
+ assert retrieved.started_at is not None, "started_at should be set after start_recording()"
async def test_delete_meeting(self, session: AsyncSession) -> None:
"""Test deleting a meeting."""
@@ -82,11 +82,11 @@ class TestMeetingRepository:
result = await repo.delete(meeting.id)
await session.commit()
- assert result is True
+ assert result is True, "delete should return True for existing meeting"
# Verify deleted
retrieved = await repo.get(meeting.id)
- assert retrieved is None
+ assert retrieved is None, "meeting should not exist after deletion"
async def test_delete_meeting_not_found_repository(self, session: AsyncSession) -> None:
"""Test deleting non-existent meeting returns False."""
@@ -95,7 +95,7 @@ class TestMeetingRepository:
result = await repo.delete(meeting_id)
- assert result is False
+ assert result is False, "delete should return False for non-existent meeting"
async def test_list_all_meetings(self, session: AsyncSession) -> None:
"""Test listing all meetings with pagination."""
@@ -108,10 +108,12 @@ class TestMeetingRepository:
await session.commit()
# List with pagination
- result, total = await repo.list_all(limit=3, offset=0)
+ page_limit = 3
+ result, total = await repo.list_all(limit=page_limit, offset=0)
- assert len(result) == 3
- assert total == 5
+ assert len(result) == page_limit, f"expected {page_limit} meetings, got {len(result)}"
+ expected_total = len(meetings)
+ assert total == expected_total, f"expected total {expected_total}, got {total}"
async def test_list_meetings_filter_by_state(self, session: AsyncSession) -> None:
"""Test filtering meetings by state."""
@@ -129,8 +131,8 @@ class TestMeetingRepository:
# Filter by RECORDING state
result, _ = await repo.list_all(states=[MeetingState.RECORDING])
- assert len(result) == 1
- assert result[0].title == "Recording"
+ assert len(result) == 1, f"expected 1 meeting in RECORDING state, got {len(result)}"
+ assert result[0].title == "Recording", f"expected 'Recording' title, got '{result[0].title}'"
async def test_count_by_state(self, session: AsyncSession) -> None:
"""Test counting meetings by state."""
@@ -143,7 +145,7 @@ class TestMeetingRepository:
count = await repo.count_by_state(MeetingState.CREATED)
- assert count == 3
+ assert count == 3, f"expected count of 3 CREATED meetings, got {count}"
@pytest.mark.integration
@@ -175,9 +177,9 @@ class TestSegmentRepository:
# Get segments
result = await segment_repo.get_by_meeting(meeting.id)
- assert len(result) == 1
- assert result[0].text == "Hello world"
- assert result[0].db_id is not None
+ assert len(result) == 1, f"expected 1 segment, got {len(result)}"
+ assert result[0].text == "Hello world", f"expected text 'Hello world', got '{result[0].text}'"
+ assert result[0].db_id is not None, "segment db_id should be set after persistence"
async def test_add_segment_with_words(self, session: AsyncSession) -> None:
"""Test adding segment with word-level timing."""
@@ -205,8 +207,8 @@ class TestSegmentRepository:
result = await segment_repo.get_by_meeting(meeting.id, include_words=True)
- assert len(result[0].words) == 2
- assert result[0].words[0].word == "Hello"
+ assert len(result[0].words) == 2, f"expected 2 word timings, got {len(result[0].words)}"
+ assert result[0].words[0].word == "Hello", f"expected first word 'Hello', got '{result[0].words[0].word}'"
async def test_add_batch_segments(self, session: AsyncSession) -> None:
"""Test batch adding segments."""
@@ -226,7 +228,7 @@ class TestSegmentRepository:
result = await segment_repo.get_by_meeting(meeting.id)
- assert len(result) == 3
+ assert len(result) == 3, f"expected 3 batch-added segments, got {len(result)}"
async def test_compute_next_segment_id(self, session: AsyncSession) -> None:
"""Test compute_next_segment_id returns max + 1 or 0 when empty."""
@@ -237,7 +239,8 @@ class TestSegmentRepository:
await meeting_repo.create(meeting)
await session.commit()
- assert await segment_repo.compute_next_segment_id(meeting.id) == 0
+ next_id = await segment_repo.compute_next_segment_id(meeting.id)
+ assert next_id == 0, f"expected next segment_id 0 for empty meeting, got {next_id}"
segments = [
Segment(segment_id=0, text="Segment 0", start_time=0.0, end_time=1.0),
@@ -246,7 +249,8 @@ class TestSegmentRepository:
await segment_repo.add_batch(meeting.id, segments)
await session.commit()
- assert await segment_repo.compute_next_segment_id(meeting.id) == 6
+ next_id = await segment_repo.compute_next_segment_id(meeting.id)
+ assert next_id == 6, f"expected next segment_id 6 (max 5 + 1), got {next_id}"
async def test_update_embedding_and_retrieve(self, session: AsyncSession) -> None:
"""Test updating a segment embedding persists to the database."""
@@ -261,13 +265,13 @@ class TestSegmentRepository:
await segment_repo.add(meeting.id, segment)
await session.commit()
- assert segment.db_id is not None
+ assert segment.db_id is not None, "segment db_id should be set after add"
embedding = [0.1] * 1536
await segment_repo.update_embedding(segment.db_id, embedding)
await session.commit()
result = await segment_repo.get_by_meeting(meeting.id)
- assert result[0].embedding == pytest.approx(embedding)
+ assert result[0].embedding == pytest.approx(embedding), "retrieved embedding should match saved embedding"
async def test_search_semantic_orders_by_similarity(self, session: AsyncSession) -> None:
"""Test semantic search returns closest matches first."""
@@ -298,10 +302,11 @@ class TestSegmentRepository:
await segment_repo.add_batch(meeting.id, [segment1, segment2])
await session.commit()
- results = await segment_repo.search_semantic(query_embedding=emb1, limit=2)
- assert len(results) == 2
- assert results[0][0].segment_id == 0
- assert results[0][1] >= results[1][1]
+ search_limit = 2
+ results = await segment_repo.search_semantic(query_embedding=emb1, limit=search_limit)
+ assert len(results) == search_limit, f"expected {search_limit} semantic search results, got {len(results)}"
+ assert results[0][0].segment_id == 0, f"expected closest match segment_id 0, got {results[0][0].segment_id}"
+ assert results[0][1] >= results[1][1], f"results should be ordered by similarity: {results[0][1]} >= {results[1][1]}"
@pytest.mark.integration
@@ -329,9 +334,9 @@ class TestSummaryRepository:
result = await summary_repo.get_by_meeting(meeting.id)
- assert result is not None
- assert result.executive_summary == "This was a productive meeting."
- assert result.model_version == "test/v1"
+ assert result is not None, "summary should exist after save"
+ assert result.executive_summary == "This was a productive meeting.", f"expected executive_summary to match, got '{result.executive_summary}'"
+ assert result.model_version == "test/v1", f"expected model_version 'test/v1', got '{result.model_version}'"
async def test_save_summary_with_key_points(self, session: AsyncSession) -> None:
"""Test saving summary with key points."""
@@ -356,9 +361,9 @@ class TestSummaryRepository:
result = await summary_repo.get_by_meeting(meeting.id)
- assert result is not None
- assert len(result.key_points) == 2
- assert result.key_points[0].text == "Point 1"
+ assert result is not None, "summary with key points should exist"
+ assert len(result.key_points) == 2, f"expected 2 key points, got {len(result.key_points)}"
+ assert result.key_points[0].text == "Point 1", f"expected first key point 'Point 1', got '{result.key_points[0].text}'"
async def test_save_summary_with_action_items(self, session: AsyncSession) -> None:
"""Test saving summary with action items."""
@@ -403,10 +408,10 @@ class TestSummaryRepository:
result = await summary_repo.delete_by_meeting(meeting.id)
await session.commit()
- assert result is True
+ assert result is True, "delete should return True for existing summary"
retrieved = await summary_repo.get_by_meeting(meeting.id)
- assert retrieved is None
+ assert retrieved is None, "summary should not exist after deletion"
async def test_update_summary_replaces_items(self, session: AsyncSession) -> None:
"""Test saving a summary twice replaces key points and action items."""
@@ -470,9 +475,9 @@ class TestAnnotationRepository:
retrieved = await annotation_repo.get(annotation.id)
- assert retrieved is not None
- assert retrieved.text == "Decision made"
- assert retrieved.segment_ids == [0]
+ assert retrieved is not None, "annotation should exist after add"
+ assert retrieved.text == "Decision made", f"expected text 'Decision made', got '{retrieved.text}'"
+ assert retrieved.segment_ids == [0], f"expected segment_ids [0], got {retrieved.segment_ids}"
async def test_get_by_meeting_ordered(self, session: AsyncSession) -> None:
"""Test annotations returned in start_time order."""
@@ -505,7 +510,8 @@ class TestAnnotationRepository:
result = await annotation_repo.get_by_meeting(meeting.id)
- assert [a.text for a in result] == ["First", "Second"]
+ texts = [a.text for a in result]
+ assert texts == ["First", "Second"], f"expected annotations ordered by start_time, got {texts}"
async def test_get_by_time_range_inclusive(self, session: AsyncSession) -> None:
"""Test time range query includes boundary overlaps."""
@@ -538,7 +544,9 @@ class TestAnnotationRepository:
result = await annotation_repo.get_by_time_range(meeting.id, start_time=1.0, end_time=1.0)
- assert {a.text for a in result} == {"Ends at boundary", "Starts at boundary"}
+ texts = {a.text for a in result}
+ expected = {"Ends at boundary", "Starts at boundary"}
+ assert texts == expected, f"expected both boundary annotations, got {texts}"
async def test_update_annotation_not_found_raises_repository(self, session: AsyncSession) -> None:
"""Test update raises when annotation does not exist."""
@@ -562,4 +570,4 @@ class TestAnnotationRepository:
result = await annotation_repo.delete(AnnotationId(uuid4()))
- assert result is False
+ assert result is False, "delete should return False for non-existent annotation"
diff --git a/tests/integration/test_server_initialization.py b/tests/integration/test_server_initialization.py
index 10b0815..2bf8ca9 100644
--- a/tests/integration/test_server_initialization.py
+++ b/tests/integration/test_server_initialization.py
@@ -44,7 +44,7 @@ class TestServerStartupPreferences:
"""Test servicer can be initialized with database session factory."""
servicer = NoteFlowServicer(session_factory=session_factory)
- assert servicer._session_factory is not None
+ assert servicer._session_factory is not None, "Servicer should have session factory set"
async def test_preferences_loaded_on_startup(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -59,8 +59,8 @@ class TestServerStartupPreferences:
consent = await uow.preferences.get_bool("cloud_consent_granted", False)
language = await uow.preferences.get("default_language")
- assert consent is True
- assert language == "en"
+ assert consent is True, "Cloud consent preference should be True after being set"
+ assert language == "en", f"Default language should be 'en', got {language!r}"
async def test_preferences_default_when_not_set(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -69,7 +69,7 @@ class TestServerStartupPreferences:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
consent = await uow.preferences.get_bool("cloud_consent_granted", False)
- assert consent is False
+ assert consent is False, "Cloud consent should default to False when not set"
@pytest.mark.integration
@@ -127,8 +127,12 @@ class TestServerStartupRecovery:
recovery_service = RecoveryService(SqlAlchemyUnitOfWork(session_factory, meetings_dir))
result = await recovery_service.recover_all()
- assert result.meetings_recovered == 0
- assert result.diarization_jobs_failed == 0
+ assert result.meetings_recovered == 0, (
+ f"No meetings should be recovered for clean state, got {result.meetings_recovered}"
+ )
+ assert result.diarization_jobs_failed == 0, (
+ f"No jobs should be failed for clean state, got {result.diarization_jobs_failed}"
+ )
@pytest.mark.integration
@@ -164,8 +168,14 @@ class TestServerGracefulShutdown:
j1 = await uow.diarization_jobs.get(job1.job_id)
j2 = await uow.diarization_jobs.get(job2.job_id)
- assert j1 is not None and j1.status == JOB_STATUS_FAILED
- assert j2 is not None and j2.status == JOB_STATUS_FAILED
+ assert j1 is not None, f"Job {job1.job_id} should exist after shutdown"
+ assert j1.status == JOB_STATUS_FAILED, (
+ f"Queued job should be marked failed on shutdown, got {j1.status}"
+ )
+ assert j2 is not None, f"Job {job2.job_id} should exist after shutdown"
+ assert j2.status == JOB_STATUS_FAILED, (
+ f"Running job should be marked failed on shutdown, got {j2.status}"
+ )
async def test_shutdown_preserves_completed_jobs(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -190,9 +200,13 @@ class TestServerGracefulShutdown:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
job = await uow.diarization_jobs.get(completed_job.job_id)
- assert job is not None
- assert job.status == JOB_STATUS_COMPLETED
- assert job.segments_updated == 10
+ assert job is not None, f"Completed job {completed_job.job_id} should exist after shutdown"
+ assert job.status == JOB_STATUS_COMPLETED, (
+ f"Completed job should remain completed after shutdown, got {job.status}"
+ )
+ assert job.segments_updated == 10, (
+ f"Completed job segments_updated should be preserved, expected 10, got {job.segments_updated}"
+ )
@pytest.mark.integration
@@ -228,7 +242,9 @@ class TestServerDatabaseOperations:
request = noteflow_pb2.ServerInfoRequest()
result = await servicer.GetServerInfo(request, MockContext())
- assert result.active_meetings == 2
+ assert result.active_meetings == 2, (
+ f"ServerInfo should report 2 active meetings, got {result.active_meetings}"
+ )
async def test_multiple_servicer_instances_share_database(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -251,8 +267,12 @@ class TestServerDatabaseOperations:
result1 = await servicer1.GetMeeting(request, MockContext())
result2 = await servicer2.GetMeeting(request, MockContext())
- assert result1.id == result2.id
- assert result1.title == "Shared Meeting"
+ assert result1.id == result2.id, (
+ f"Both servicers should return same meeting ID, got {result1.id} vs {result2.id}"
+ )
+ assert result1.title == "Shared Meeting", (
+ f"Meeting title should be 'Shared Meeting', got {result1.title!r}"
+ )
@pytest.mark.integration
@@ -279,8 +299,12 @@ class TestServerDatabasePersistence:
get_request = noteflow_pb2.GetMeetingRequest(meeting_id=meeting_id)
result = await servicer2.GetMeeting(get_request, MockContext())
- assert result.id == meeting_id
- assert result.title == "Persistent Meeting"
+ assert result.id == meeting_id, (
+ f"Meeting ID should persist across servicer restart, expected {meeting_id}, got {result.id}"
+ )
+ assert result.title == "Persistent Meeting", (
+ f"Meeting title should persist across restart, got {result.title!r}"
+ )
async def test_preferences_survive_servicer_restart(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -295,4 +319,6 @@ class TestServerDatabasePersistence:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
value = await uow.preferences.get("test_setting")
- assert value == "test_value"
+ assert value == "test_value", (
+ f"Preference should persist across servicer restart, expected 'test_value', got {value!r}"
+ )
diff --git a/tests/integration/test_signal_handling.py b/tests/integration/test_signal_handling.py
index aa6c994..cd047a8 100644
--- a/tests/integration/test_signal_handling.py
+++ b/tests/integration/test_signal_handling.py
@@ -29,14 +29,20 @@ class TestServicerShutdown:
) -> None:
"""Verify shutdown works when no streams are active."""
# Empty state
- assert len(memory_servicer._active_streams) == 0
- assert len(memory_servicer._diarization_tasks) == 0
+ assert len(memory_servicer._active_streams) == 0, (
+ f"expected no active streams, got {len(memory_servicer._active_streams)}"
+ )
+ assert len(memory_servicer._diarization_tasks) == 0, (
+ f"expected no diarization tasks, got {len(memory_servicer._diarization_tasks)}"
+ )
# Shutdown should complete without error
await memory_servicer.shutdown()
# State should still be empty
- assert len(memory_servicer._active_streams) == 0
+ assert len(memory_servicer._active_streams) == 0, (
+ f"expected no active streams after shutdown, got {len(memory_servicer._active_streams)}"
+ )
@pytest.mark.asyncio
async def test_shutdown_cleans_active_streams(
@@ -54,14 +60,20 @@ class TestServicerShutdown:
mock_session.close = MagicMock()
memory_servicer._diarization_sessions[meeting_id] = mock_session
- assert len(memory_servicer._active_streams) == 5
- assert len(memory_servicer._diarization_sessions) == 5
+ assert len(memory_servicer._active_streams) == 5, (
+ f"expected 5 active streams, got {len(memory_servicer._active_streams)}"
+ )
+ assert len(memory_servicer._diarization_sessions) == 5, (
+ f"expected 5 diarization sessions, got {len(memory_servicer._diarization_sessions)}"
+ )
# Shutdown
await memory_servicer.shutdown()
# Verify all sessions closed
- assert len(memory_servicer._diarization_sessions) == 0
+ assert len(memory_servicer._diarization_sessions) == 0, (
+ f"expected all diarization sessions closed, got {len(memory_servicer._diarization_sessions)}"
+ )
@pytest.mark.asyncio
async def test_shutdown_cancels_diarization_tasks(
@@ -75,17 +87,22 @@ class TestServicerShutdown:
memory_servicer._diarization_tasks[f"job-{i}"] = task
tasks_created.append(task)
- assert len(memory_servicer._diarization_tasks) == 3
+ assert len(memory_servicer._diarization_tasks) == 3, (
+ f"expected 3 diarization tasks, got {len(memory_servicer._diarization_tasks)}"
+ )
# Shutdown should cancel all
await memory_servicer.shutdown()
- assert len(memory_servicer._diarization_tasks) == 0
+ assert len(memory_servicer._diarization_tasks) == 0, (
+ f"expected no diarization tasks after shutdown, got {len(memory_servicer._diarization_tasks)}"
+ )
- # Verify tasks are cancelled
- for task in tasks_created:
- assert task.done()
- assert task.cancelled()
+ # Verify tasks are cancelled - collect non-done/non-cancelled tasks
+ not_done = [t for t in tasks_created if not t.done()]
+ not_cancelled = [t for t in tasks_created if t.done() and not t.cancelled()]
+ assert not not_done, f"all tasks should be done after shutdown, {len(not_done)} still running"
+ assert not not_cancelled, f"all tasks should be cancelled, {len(not_cancelled)} completed instead"
@pytest.mark.asyncio
async def test_shutdown_marks_cancelled_jobs_failed(
@@ -111,8 +128,12 @@ class TestServicerShutdown:
await memory_servicer.shutdown()
# Verify job marked as failed
- assert job.status == noteflow_pb2.JOB_STATUS_FAILED
- assert job.error_message == "ERR_TASK_CANCELLED"
+ assert job.status == noteflow_pb2.JOB_STATUS_FAILED, (
+ f"expected job status FAILED, got {job.status}"
+ )
+ assert job.error_message == "ERR_TASK_CANCELLED", (
+ f"expected error message 'ERR_TASK_CANCELLED', got '{job.error_message}'"
+ )
@pytest.mark.asyncio
async def test_shutdown_idempotent(
@@ -147,14 +168,16 @@ class TestServicerShutdown:
writer.open(meeting_id, dek, wrapped_dek, sample_rate=DEFAULT_SAMPLE_RATE)
memory_servicer._audio_writers[meeting_id] = writer
- assert writer.is_recording
+ assert writer.is_recording, "expected audio writer to be recording after open"
# Shutdown
await memory_servicer.shutdown()
# Verify writer was closed and removed
- assert meeting_id not in memory_servicer._audio_writers
- assert not writer.is_recording
+ assert meeting_id not in memory_servicer._audio_writers, (
+ f"expected meeting '{meeting_id}' removed from audio writers after shutdown"
+ )
+ assert not writer.is_recording, "expected audio writer to stop recording after shutdown"
class TestStreamingStateCleanup:
@@ -174,9 +197,15 @@ class TestStreamingStateCleanup:
memory_servicer._active_streams.add(meeting_id)
# Verify state exists
- assert len(memory_servicer._active_streams) == 10
- assert len(memory_servicer._vad_instances) == 10
- assert len(memory_servicer._segmenters) == 10
+ assert len(memory_servicer._active_streams) == 10, (
+ f"expected 10 active streams, got {len(memory_servicer._active_streams)}"
+ )
+ assert len(memory_servicer._vad_instances) == 10, (
+ f"expected 10 VAD instances, got {len(memory_servicer._vad_instances)}"
+ )
+ assert len(memory_servicer._segmenters) == 10, (
+ f"expected 10 segmenters, got {len(memory_servicer._segmenters)}"
+ )
# Clean up all streams
for meeting_id in meeting_ids:
@@ -184,10 +213,18 @@ class TestStreamingStateCleanup:
memory_servicer._active_streams.discard(meeting_id)
# Verify all state cleaned
- assert len(memory_servicer._active_streams) == 0
- assert len(memory_servicer._vad_instances) == 0
- assert len(memory_servicer._segmenters) == 0
- assert len(memory_servicer._partial_buffers) == 0
+ assert len(memory_servicer._active_streams) == 0, (
+ f"expected no active streams after cleanup, got {len(memory_servicer._active_streams)}"
+ )
+ assert len(memory_servicer._vad_instances) == 0, (
+ f"expected no VAD instances after cleanup, got {len(memory_servicer._vad_instances)}"
+ )
+ assert len(memory_servicer._segmenters) == 0, (
+ f"expected no segmenters after cleanup, got {len(memory_servicer._segmenters)}"
+ )
+ assert len(memory_servicer._partial_buffers) == 0, (
+ f"expected no partial buffers after cleanup, got {len(memory_servicer._partial_buffers)}"
+ )
@pytest.mark.asyncio
async def test_cleanup_with_diarization_sessions(
@@ -209,7 +246,9 @@ class TestStreamingStateCleanup:
# Verify session was closed
mock_session.close.assert_called_once()
- assert meeting_id not in memory_servicer._diarization_sessions
+ assert meeting_id not in memory_servicer._diarization_sessions, (
+ f"expected meeting '{meeting_id}' removed from diarization sessions after cleanup"
+ )
class TestTaskCancellation:
@@ -228,8 +267,8 @@ class TestTaskCancellation:
await memory_servicer.shutdown()
# Task should be cancelled and done
- assert task.done()
- assert task.cancelled()
+ assert task.done(), "expected long-running task to be done after shutdown"
+ assert task.cancelled(), "expected long-running task to be cancelled after shutdown"
@pytest.mark.asyncio
async def test_task_with_exception_handling(
@@ -249,11 +288,13 @@ class TestTaskCancellation:
await task
# Verify task is done (not stuck)
- assert task.done()
+ assert task.done(), "expected failing task to be done after exception"
# Cleanup should still work
await memory_servicer.shutdown()
- assert len(memory_servicer._diarization_tasks) == 0
+ assert len(memory_servicer._diarization_tasks) == 0, (
+ f"expected no diarization tasks after shutdown, got {len(memory_servicer._diarization_tasks)}"
+ )
@pytest.mark.asyncio
async def test_mixed_task_states_on_shutdown(
@@ -277,7 +318,9 @@ class TestTaskCancellation:
# Shutdown should handle all states
await memory_servicer.shutdown()
- assert len(memory_servicer._diarization_tasks) == 0
+ assert len(memory_servicer._diarization_tasks) == 0, (
+ f"expected no diarization tasks after shutdown, got {len(memory_servicer._diarization_tasks)}"
+ )
class TestResourceCleanupOrder:
@@ -316,8 +359,12 @@ class TestResourceCleanupOrder:
await memory_servicer.shutdown()
# Diarization should be closed before audio (based on shutdown() order)
- assert "diarization" in cleanup_order
- assert "audio" in cleanup_order
+ assert "diarization" in cleanup_order, (
+ f"expected 'diarization' in cleanup order, got {cleanup_order}"
+ )
+ assert "audio" in cleanup_order, (
+ f"expected 'audio' in cleanup order, got {cleanup_order}"
+ )
@pytest.mark.asyncio
async def test_tasks_cancelled_before_sessions_closed(
@@ -337,7 +384,7 @@ class TestResourceCleanupOrder:
await memory_servicer.shutdown()
# Both should be cleaned up
- assert task.done()
+ assert task.done(), "expected task to be done after shutdown"
mock_session.close.assert_called_once()
@@ -362,7 +409,9 @@ class TestConcurrentShutdown:
)
# Should be clean
- assert len(memory_servicer._diarization_tasks) == 0
+ assert len(memory_servicer._diarization_tasks) == 0, (
+ f"expected no diarization tasks after concurrent shutdowns, got {len(memory_servicer._diarization_tasks)}"
+ )
@pytest.mark.asyncio
async def test_new_operations_during_shutdown(
diff --git a/tests/integration/test_trigger_settings.py b/tests/integration/test_trigger_settings.py
index 761d5a7..4d916e4 100644
--- a/tests/integration/test_trigger_settings.py
+++ b/tests/integration/test_trigger_settings.py
@@ -32,14 +32,16 @@ def test_trigger_settings_env_parsing(
monkeypatch.setenv("NOTEFLOW_TRIGGER_AUDIO_MIN_SAMPLES", "5")
monkeypatch.setenv("NOTEFLOW_TRIGGER_POLL_INTERVAL_SECONDS", "1.5")
settings = get_trigger_settings()
- assert getattr(settings, attr) == expected
+ actual = getattr(settings, attr)
+ assert actual == expected, f"TriggerSettings.{attr}: expected {expected!r}, got {actual!r}"
def test_trigger_settings_poll_interval_parsing(monkeypatch: pytest.MonkeyPatch) -> None:
"""TriggerSettings parses poll interval as float."""
monkeypatch.setenv("NOTEFLOW_TRIGGER_POLL_INTERVAL_SECONDS", "1.5")
settings = get_trigger_settings()
- assert settings.trigger_poll_interval_seconds == pytest.approx(1.5)
+ actual = settings.trigger_poll_interval_seconds
+ assert actual == pytest.approx(1.5), f"poll_interval_seconds: expected 1.5, got {actual}"
class TestRetentionSettings:
@@ -50,9 +52,19 @@ class TestRetentionSettings:
# Access via class to check field defaults without loading from env
expected_retention_days_default = 90
expected_check_interval_default = 24
- assert Settings.model_fields["retention_enabled"].default is False
- assert Settings.model_fields["retention_days"].default == expected_retention_days_default
- assert Settings.model_fields["retention_check_interval_hours"].default == expected_check_interval_default
+ retention_enabled_default = Settings.model_fields["retention_enabled"].default
+ retention_days_default = Settings.model_fields["retention_days"].default
+ check_interval_default = Settings.model_fields["retention_check_interval_hours"].default
+ assert retention_enabled_default is False, (
+ f"retention_enabled default: expected False, got {retention_enabled_default!r}"
+ )
+ assert retention_days_default == expected_retention_days_default, (
+ f"retention_days default: expected {expected_retention_days_default}, got {retention_days_default}"
+ )
+ assert check_interval_default == expected_check_interval_default, (
+ f"retention_check_interval_hours default: expected {expected_check_interval_default}, "
+ f"got {check_interval_default}"
+ )
def test_retention_env_parsing(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Retention settings should parse from environment variables."""
@@ -65,9 +77,16 @@ class TestRetentionSettings:
settings = get_settings()
- assert settings.retention_enabled is True
- assert settings.retention_days == expected_retention_days
- assert settings.retention_check_interval_hours == expected_check_interval
+ assert settings.retention_enabled is True, (
+ f"retention_enabled: expected True, got {settings.retention_enabled!r}"
+ )
+ assert settings.retention_days == expected_retention_days, (
+ f"retention_days: expected {expected_retention_days}, got {settings.retention_days}"
+ )
+ assert settings.retention_check_interval_hours == expected_check_interval, (
+ f"retention_check_interval_hours: expected {expected_check_interval}, "
+ f"got {settings.retention_check_interval_hours}"
+ )
def test_retention_days_validation(self) -> None:
"""Retention days should be validated within range."""
diff --git a/tests/integration/test_unit_of_work.py b/tests/integration/test_unit_of_work.py
index a797571..28f7d53 100644
--- a/tests/integration/test_unit_of_work.py
+++ b/tests/integration/test_unit_of_work.py
@@ -25,9 +25,9 @@ class TestUnitOfWork:
) -> None:
"""Test UoW works as async context manager."""
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
- assert uow.meetings is not None
- assert uow.segments is not None
- assert uow.summaries is not None
+ assert uow.meetings is not None, "meetings repository should be initialized"
+ assert uow.segments is not None, "segments repository should be initialized"
+ assert uow.summaries is not None, "summaries repository should be initialized"
async def test_uow_commit(self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path) -> None:
"""Test UoW commit persists changes."""
@@ -40,8 +40,8 @@ class TestUnitOfWork:
# Verify in new UoW
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
retrieved = await uow.meetings.get(meeting.id)
- assert retrieved is not None
- assert retrieved.title == "Commit Test"
+ assert retrieved is not None, f"meeting {meeting.id} should exist after commit"
+ assert retrieved.title == "Commit Test", f"expected title 'Commit Test', got '{retrieved.title}'"
async def test_uow_rollback(self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path) -> None:
"""Test UoW rollback discards changes."""
@@ -54,7 +54,7 @@ class TestUnitOfWork:
# Verify not persisted
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
retrieved = await uow.meetings.get(meeting.id)
- assert retrieved is None
+ assert retrieved is None, f"meeting {meeting.id} should not exist after rollback"
async def test_uow_auto_rollback_on_exception(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -70,7 +70,7 @@ class TestUnitOfWork:
# Verify not persisted
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
retrieved = await uow.meetings.get(meeting.id)
- assert retrieved is None
+ assert retrieved is None, f"meeting {meeting.id} should not exist after exception rollback"
async def test_uow_transactional_consistency(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -103,9 +103,9 @@ class TestUnitOfWork:
segs = await uow.segments.get_by_meeting(meeting.id)
s = await uow.summaries.get_by_meeting(meeting.id)
- assert m is not None
- assert len(segs) == 1
- assert s is not None
+ assert m is not None, f"meeting {meeting.id} should exist after transactional commit"
+ assert len(segs) == 1, f"expected 1 segment, got {len(segs)}"
+ assert s is not None, f"summary for meeting {meeting.id} should exist after commit"
async def test_uow_repository_caching(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -114,11 +114,11 @@ class TestUnitOfWork:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
meetings1 = uow.meetings
meetings2 = uow.meetings
- assert meetings1 is meetings2
+ assert meetings1 is meetings2, "meetings repository should be cached (same instance)"
segments1 = uow.segments
segments2 = uow.segments
- assert segments1 is segments2
+ assert segments1 is segments2, "segments repository should be cached (same instance)"
async def test_uow_multiple_operations(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -146,6 +146,6 @@ class TestUnitOfWork:
m = await uow.meetings.get(meeting.id)
segs = await uow.segments.get_by_meeting(meeting.id)
- assert m is not None
- assert m.state == MeetingState.RECORDING
- assert len(segs) == 1
+ assert m is not None, f"meeting {meeting.id} should exist after multiple operations"
+ assert m.state == MeetingState.RECORDING, f"expected state RECORDING, got {m.state}"
+ assert len(segs) == 1, f"expected 1 segment after operations, got {len(segs)}"
diff --git a/tests/integration/test_unit_of_work_advanced.py b/tests/integration/test_unit_of_work_advanced.py
index a69f9bb..800bbbb 100644
--- a/tests/integration/test_unit_of_work_advanced.py
+++ b/tests/integration/test_unit_of_work_advanced.py
@@ -40,21 +40,21 @@ class TestUnitOfWorkFeatureFlags:
) -> None:
"""Test database UoW supports annotations."""
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
- assert uow.supports_annotations is True
+ assert uow.supports_annotations is True, "database UoW should support annotations"
async def test_supports_diarization_jobs_true(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
) -> None:
"""Test database UoW supports diarization jobs."""
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
- assert uow.supports_diarization_jobs is True
+ assert uow.supports_diarization_jobs is True, "database UoW should support diarization jobs"
async def test_supports_preferences_true(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
) -> None:
"""Test database UoW supports preferences."""
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
- assert uow.supports_preferences is True
+ assert uow.supports_preferences is True, "database UoW should support preferences"
@pytest.mark.integration
@@ -115,9 +115,9 @@ class TestUnitOfWorkCrossRepositoryOperations:
await uow.commit()
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
- assert await uow.meetings.get(meeting.id) is None
- assert await uow.segments.get_by_meeting(meeting.id) == []
- assert await uow.summaries.get_by_meeting(meeting.id) is None
+ assert await uow.meetings.get(meeting.id) is None, "meeting should be deleted"
+ assert await uow.segments.get_by_meeting(meeting.id) == [], "segments should cascade delete"
+ assert await uow.summaries.get_by_meeting(meeting.id) is None, "summary should cascade delete"
async def test_meeting_deletion_cascades_to_diarization_jobs(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -144,8 +144,8 @@ class TestUnitOfWorkCrossRepositoryOperations:
await uow.commit()
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
- assert await uow.diarization_jobs.get(job.job_id) is None
- assert await uow.diarization_jobs.get_streaming_turns(str(meeting.id)) == []
+ assert await uow.diarization_jobs.get(job.job_id) is None, "diarization job should cascade delete"
+ assert await uow.diarization_jobs.get_streaming_turns(str(meeting.id)) == [], "streaming turns should cascade delete"
@pytest.mark.integration
@@ -238,7 +238,7 @@ class TestUnitOfWorkRollbackScenarios:
await uow.rollback()
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
- assert await uow.meetings.get(meeting.id) is None
+ assert await uow.meetings.get(meeting.id) is None, "meeting should not exist after rollback"
async def test_exception_during_segment_add_rolls_back_meeting(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -259,7 +259,7 @@ class TestUnitOfWorkRollbackScenarios:
raise TestError("Simulated failure")
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
- assert await uow.meetings.get(meeting.id) is None
+ assert await uow.meetings.get(meeting.id) is None, "meeting should not exist after exception rollback"
async def test_rollback_then_new_work_in_same_context(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -276,10 +276,10 @@ class TestUnitOfWorkRollbackScenarios:
await uow.commit()
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
- assert await uow.meetings.get(meeting1.id) is None
+ assert await uow.meetings.get(meeting1.id) is None, "rolled back meeting should not exist"
m2 = await uow.meetings.get(meeting2.id)
- assert m2 is not None
- assert m2.title == "Committed"
+ assert m2 is not None, "committed meeting should be retrievable"
+ assert m2.title == "Committed", f"expected title 'Committed', got {m2.title!r}"
@pytest.mark.integration
@@ -304,12 +304,12 @@ class TestUnitOfWorkRepositoryCaching:
jobs1 = uow.diarization_jobs
jobs2 = uow.diarization_jobs
- assert meetings1 is meetings2
- assert segments1 is segments2
- assert summaries1 is summaries2
- assert annotations1 is annotations2
- assert preferences1 is preferences2
- assert jobs1 is jobs2
+ assert meetings1 is meetings2, "meetings repository should be cached"
+ assert segments1 is segments2, "segments repository should be cached"
+ assert summaries1 is summaries2, "summaries repository should be cached"
+ assert annotations1 is annotations2, "annotations repository should be cached"
+ assert preferences1 is preferences2, "preferences repository should be cached"
+ assert jobs1 is jobs2, "diarization_jobs repository should be cached"
async def test_repository_instances_new_per_context(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -321,7 +321,7 @@ class TestUnitOfWorkRepositoryCaching:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow2:
meetings2 = uow2.meetings
- assert meetings1 is not meetings2
+ assert meetings1 is not meetings2, "repository instances should differ across UoW contexts"
@pytest.mark.integration
@@ -371,16 +371,16 @@ class TestUnitOfWorkComplexWorkflows:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
meeting = await uow.meetings.get(meeting.id)
- assert meeting is not None
- assert meeting.state == MeetingState.CREATED
+ assert meeting is not None, "meeting should be retrievable after creation"
+ assert meeting.state == MeetingState.CREATED, f"expected CREATED state, got {meeting.state}"
meeting.start_recording()
await uow.meetings.update(meeting)
await uow.commit()
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
meeting = await uow.meetings.get(meeting.id)
- assert meeting is not None
- assert meeting.state == MeetingState.RECORDING
+ assert meeting is not None, "meeting should be retrievable for segment addition"
+ assert meeting.state == MeetingState.RECORDING, f"expected RECORDING state, got {meeting.state}"
for i in range(5):
segment = Segment(
segment_id=i,
@@ -393,7 +393,7 @@ class TestUnitOfWorkComplexWorkflows:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
meeting = await uow.meetings.get(meeting.id)
- assert meeting is not None
+ assert meeting is not None, "meeting should be retrievable for stopping"
meeting.begin_stopping()
meeting.stop_recording()
await uow.meetings.update(meeting)
@@ -401,8 +401,8 @@ class TestUnitOfWorkComplexWorkflows:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
meeting = await uow.meetings.get(meeting.id)
- assert meeting is not None
- assert meeting.state == MeetingState.STOPPED
+ assert meeting is not None, "meeting should be retrievable for summary creation"
+ assert meeting.state == MeetingState.STOPPED, f"expected STOPPED state, got {meeting.state}"
summary = Summary(
meeting_id=meeting.id,
@@ -419,10 +419,10 @@ class TestUnitOfWorkComplexWorkflows:
segments = await uow.segments.get_by_meeting(meeting_id)
summary = await uow.summaries.get_by_meeting(meeting_id)
- assert final_meeting is not None
- assert final_meeting.state == MeetingState.STOPPED
- assert len(segments) == 5
- assert summary is not None
+ assert final_meeting is not None, "final meeting should be retrievable"
+ assert final_meeting.state == MeetingState.STOPPED, f"expected STOPPED state, got {final_meeting.state}"
+ assert len(segments) == 5, f"expected 5 segments, got {len(segments)}"
+ assert summary is not None, "summary should be retrievable"
async def test_diarization_job_workflow(
self, session_factory: async_sessionmaker[AsyncSession], meetings_dir: Path
@@ -461,10 +461,10 @@ class TestUnitOfWorkComplexWorkflows:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
final_job = await uow.diarization_jobs.get(job.job_id)
- assert final_job is not None
- assert final_job.status == JOB_STATUS_COMPLETED
- assert final_job.segments_updated == 10
- assert final_job.speaker_ids == ["SPEAKER_00", "SPEAKER_01"]
+ assert final_job is not None, "completed job should be retrievable"
+ assert final_job.status == JOB_STATUS_COMPLETED, f"expected COMPLETED status, got {final_job.status}"
+ assert final_job.segments_updated == 10, f"expected 10 segments updated, got {final_job.segments_updated}"
+ assert final_job.speaker_ids == ["SPEAKER_00", "SPEAKER_01"], f"unexpected speaker_ids: {final_job.speaker_ids}"
@pytest.mark.integration
@@ -477,7 +477,7 @@ class TestUnitOfWorkPreferencesWorkflow:
"""Test cloud consent workflow as used by server startup."""
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
consent = await uow.preferences.get_bool("cloud_consent_granted", False)
- assert consent is False
+ assert consent is False, "initial consent should be False"
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
await uow.preferences.set("cloud_consent_granted", True)
@@ -485,7 +485,7 @@ class TestUnitOfWorkPreferencesWorkflow:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
consent = await uow.preferences.get_bool("cloud_consent_granted", False)
- assert consent is True
+ assert consent is True, "consent should be True after granting"
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
await uow.preferences.set("cloud_consent_granted", False)
@@ -493,4 +493,4 @@ class TestUnitOfWorkPreferencesWorkflow:
async with SqlAlchemyUnitOfWork(session_factory, meetings_dir) as uow:
consent = await uow.preferences.get_bool("cloud_consent_granted", False)
- assert consent is False
+ assert consent is False, "consent should be False after revoking"
diff --git a/tests/integration/test_webhook_integration.py b/tests/integration/test_webhook_integration.py
index db09c6b..b54de0c 100644
--- a/tests/integration/test_webhook_integration.py
+++ b/tests/integration/test_webhook_integration.py
@@ -132,22 +132,34 @@ class TestStopMeetingTriggersWebhook:
result = await servicer.StopMeeting(request, MockGrpcContext())
# StopMeeting returns Meeting proto directly - state should be STOPPED
- assert result.state == noteflow_pb2.MEETING_STATE_STOPPED
+ assert result.state == noteflow_pb2.MEETING_STATE_STOPPED, (
+ f"expected meeting state STOPPED, got {result.state}"
+ )
# Verify webhooks were triggered (recording.stopped + meeting.completed)
- assert len(captured_webhook_calls) == 2
+ assert len(captured_webhook_calls) == 2, (
+ f"expected 2 webhook calls (recording.stopped + meeting.completed), got {len(captured_webhook_calls)}"
+ )
event_types = {call["event_type"] for call in captured_webhook_calls}
- assert WebhookEventType.RECORDING_STOPPED in event_types
- assert WebhookEventType.MEETING_COMPLETED in event_types
+ assert WebhookEventType.RECORDING_STOPPED in event_types, (
+ f"expected RECORDING_STOPPED event in {event_types}"
+ )
+ assert WebhookEventType.MEETING_COMPLETED in event_types, (
+ f"expected MEETING_COMPLETED event in {event_types}"
+ )
# Verify meeting.completed payload
completed_call = next(
c for c in captured_webhook_calls
if c["event_type"] == WebhookEventType.MEETING_COMPLETED
)
- assert completed_call["payload"]["meeting_id"] == meeting_id
- assert completed_call["payload"]["title"] == "Webhook Integration Test"
+ assert completed_call["payload"]["meeting_id"] == meeting_id, (
+ f"expected meeting_id {meeting_id}, got {completed_call['payload']['meeting_id']}"
+ )
+ assert completed_call["payload"]["title"] == "Webhook Integration Test", (
+ f"expected title 'Webhook Integration Test', got {completed_call['payload']['title']}"
+ )
async def test_stop_meeting_with_failed_webhook_still_succeeds(
self,
@@ -185,7 +197,9 @@ class TestStopMeetingTriggersWebhook:
result = await servicer.StopMeeting(request, MockGrpcContext())
# Meeting stop succeeds despite webhook failure
- assert result.state == noteflow_pb2.MEETING_STATE_STOPPED
+ assert result.state == noteflow_pb2.MEETING_STATE_STOPPED, (
+ f"expected meeting state STOPPED despite webhook failure, got {result.state}"
+ )
@pytest.mark.integration
@@ -213,4 +227,6 @@ class TestNoWebhookServiceGracefulDegradation:
request = noteflow_pb2.StopMeetingRequest(meeting_id=meeting_id)
result = await servicer.StopMeeting(request, MockGrpcContext())
- assert result.state == noteflow_pb2.MEETING_STATE_STOPPED
+ assert result.state == noteflow_pb2.MEETING_STATE_STOPPED, (
+ f"expected meeting state STOPPED without webhook service, got {result.state}"
+ )
diff --git a/tests/integration/test_webhook_repository.py b/tests/integration/test_webhook_repository.py
index 60e1602..8c438f3 100644
--- a/tests/integration/test_webhook_repository.py
+++ b/tests/integration/test_webhook_repository.py
@@ -113,9 +113,15 @@ class TestWebhookRepositoryCreate:
await session.commit()
assert len(created.events) == 3, "Should have 3 events"
- assert WebhookEventType.MEETING_COMPLETED in created.events
- assert WebhookEventType.SUMMARY_GENERATED in created.events
- assert WebhookEventType.RECORDING_STARTED in created.events
+ assert (
+ WebhookEventType.MEETING_COMPLETED in created.events
+ ), "MEETING_COMPLETED event should be in events"
+ assert (
+ WebhookEventType.SUMMARY_GENERATED in created.events
+ ), "SUMMARY_GENERATED event should be in events"
+ assert (
+ WebhookEventType.RECORDING_STARTED in created.events
+ ), "RECORDING_STARTED event should be in events"
async def test_creates_webhook_with_optional_fields(
self,
@@ -221,10 +227,14 @@ class TestWebhookRepositoryGetById:
retrieved = await webhook_repo.get_by_id(config.id)
- assert retrieved is not None
+ assert retrieved is not None, "Should retrieve webhook by ID"
assert isinstance(retrieved.events, frozenset), "Events should be frozenset"
- assert WebhookEventType.MEETING_COMPLETED in retrieved.events
- assert WebhookEventType.RECORDING_STOPPED in retrieved.events
+ assert (
+ WebhookEventType.MEETING_COMPLETED in retrieved.events
+ ), "MEETING_COMPLETED event should be in retrieved events"
+ assert (
+ WebhookEventType.RECORDING_STOPPED in retrieved.events
+ ), "RECORDING_STOPPED event should be in retrieved events"
# ============================================================================
@@ -271,9 +281,13 @@ class TestWebhookRepositoryGetAll:
result = await webhook_repo.get_all()
assert len(result) == 2, "Should return both webhooks"
- urls = {w.url for w in result}
- assert "https://example.com/hook1" in urls
- assert "https://example.com/hook2" in urls
+ result_urls = (result[0].url, result[1].url)
+ assert (
+ "https://example.com/hook1" in result_urls
+ ), "hook1 URL should be in results"
+ assert (
+ "https://example.com/hook2" in result_urls
+ ), "hook2 URL should be in results"
async def test_filters_by_workspace(
self,
@@ -306,7 +320,9 @@ class TestWebhookRepositoryGetAll:
result = await webhook_repo.get_all(workspace_id=workspace1)
assert len(result) == 1, "Should return only workspace1 webhooks"
- assert result[0].url == "https://workspace1.com/hook"
+ assert (
+ result[0].url == "https://workspace1.com/hook"
+ ), f"URL should be workspace1 hook, got {result[0].url}"
# ============================================================================
@@ -344,8 +360,10 @@ class TestWebhookRepositoryGetAllEnabled:
result = await webhook_repo.get_all_enabled()
assert len(result) == 1, "Should return only enabled webhook"
- assert result[0].url == "https://example.com/enabled"
- assert result[0].enabled is True
+ assert (
+ result[0].url == "https://example.com/enabled"
+ ), f"URL should be enabled webhook, got {result[0].url}"
+ assert result[0].enabled is True, "Webhook should be enabled"
async def test_filters_enabled_by_workspace(
self,
@@ -377,7 +395,9 @@ class TestWebhookRepositoryGetAllEnabled:
result = await webhook_repo.get_all_enabled(workspace_id=workspace1)
assert len(result) == 1, "Should return only workspace1 enabled webhooks"
- assert result[0].workspace_id == workspace1
+ assert (
+ result[0].workspace_id == workspace1
+ ), f"Workspace ID should be {workspace1}, got {result[0].workspace_id}"
async def test_returns_empty_when_all_disabled(
self,
@@ -503,8 +523,12 @@ class TestWebhookRepositoryUpdate:
await session.commit()
assert len(result.events) == 1, "Should have only 1 event"
- assert WebhookEventType.RECORDING_STARTED in result.events
- assert WebhookEventType.MEETING_COMPLETED not in result.events
+ assert (
+ WebhookEventType.RECORDING_STARTED in result.events
+ ), "RECORDING_STARTED should be in updated events"
+ assert (
+ WebhookEventType.MEETING_COMPLETED not in result.events
+ ), "MEETING_COMPLETED should not be in updated events"
async def test_update_raises_for_nonexistent_webhook(
self,
diff --git a/tests/quality/baselines.json b/tests/quality/baselines.json
index a65767c..54a3b6e 100644
--- a/tests/quality/baselines.json
+++ b/tests/quality/baselines.json
@@ -1,101 +1,15 @@
{
- "generated_at": "2025-12-31T15:28:38.066948+00:00",
+ "generated_at": "2025-12-31T21:15:56.514631+00:00",
"rules": {
"alias_import": [
"alias_import|src/noteflow/domain/auth/oidc.py|cc2f0972|datetime->dt",
"alias_import|src/noteflow/grpc/service.py|d8a43a4a|__version__->NOTEFLOW_VERSION"
],
- "assertion_roulette": [
- "assertion_roulette|tests/domain/test_meeting.py|test_immediate_stop_after_start_zero_duration|assertions=5",
- "assertion_roulette|tests/domain/test_meeting.py|test_state_transition_does_not_modify_segments|assertions=4",
- "assertion_roulette|tests/domain/test_project.py|test_default_values|assertions=4",
- "assertion_roulette|tests/domain/test_project.py|test_default_values|assertions=4",
- "assertion_roulette|tests/domain/test_project.py|test_settings_with_full_configuration|assertions=4",
- "assertion_roulette|tests/domain/test_project.py|test_timestamps_are_set|assertions=4",
- "assertion_roulette|tests/domain/test_project.py|test_with_nested_rules|assertions=4",
- "assertion_roulette|tests/domain/test_project.py|test_with_values|assertions=4",
- "assertion_roulette|tests/domain/test_summary.py|test_action_item_with_all_fields|assertions=4",
- "assertion_roulette|tests/grpc/test_entities_mixin.py|test_returns_extracted_entities|assertions=4",
- "assertion_roulette|tests/infrastructure/asr/test_engine.py|test_load_model_with_stub_sets_state|assertions=4",
- "assertion_roulette|tests/infrastructure/asr/test_engine.py|test_transcribe_with_stubbed_model|assertions=5",
- "assertion_roulette|tests/infrastructure/audio/test_writer.py|test_flush_writes_buffered_data|assertions=5",
- "assertion_roulette|tests/infrastructure/audio/test_writer.py|test_manifest_contains_correct_metadata|assertions=6",
- "assertion_roulette|tests/infrastructure/audio/test_writer.py|test_periodic_flush_thread_starts_on_open|assertions=4",
- "assertion_roulette|tests/infrastructure/audio/test_writer.py|test_write_chunk_converts_float32_to_pcm16|assertions=4",
- "assertion_roulette|tests/infrastructure/calendar/test_google_adapter.py|test_list_events_returns_calendar_events|assertions=7",
- "assertion_roulette|tests/infrastructure/calendar/test_oauth_manager.py|test_initiate_google_auth_returns_url_and_state|assertions=6",
- "assertion_roulette|tests/infrastructure/export/test_html.py|test_export_escapes_html|assertions=5",
- "assertion_roulette|tests/infrastructure/export/test_markdown.py|test_export_includes_sections|assertions=10",
- "assertion_roulette|tests/infrastructure/summarization/test_ollama_provider.py|test_ollama_summarize_returns_result|assertions=8",
- "assertion_roulette|tests/infrastructure/test_calendar_converters.py|test_calendar_event_info_to_orm_kwargs|assertions=5",
- "assertion_roulette|tests/infrastructure/test_observability.py|test_collect_now_returns_metrics|assertions=5",
- "assertion_roulette|tests/infrastructure/test_orm_converters.py|test_converts_orm_to_domain_annotation|assertions=5",
- "assertion_roulette|tests/infrastructure/triggers/test_foreground_app.py|test_foreground_app_settings_lowercases_apps|assertions=7",
- "assertion_roulette|tests/integration/test_e2e_annotations.py|test_add_annotation_persists_to_database|assertions=8",
- "assertion_roulette|tests/integration/test_e2e_annotations.py|test_update_annotation_modifies_database|assertions=4",
- "assertion_roulette|tests/integration/test_e2e_ner.py|test_delete_does_not_affect_other_entities|assertions=4",
- "assertion_roulette|tests/integration/test_grpc_servicer_database.py|test_create_meeting_persists_to_database|assertions=6",
- "assertion_roulette|tests/integration/test_grpc_servicer_database.py|test_get_diarization_job_status_retrieves_from_database|assertions=4",
- "assertion_roulette|tests/integration/test_grpc_servicer_database.py|test_get_meeting_with_segments|assertions=4",
- "assertion_roulette|tests/integration/test_grpc_servicer_database.py|test_refine_speaker_diarization_creates_job_in_database|assertions=5",
- "assertion_roulette|tests/integration/test_grpc_servicer_database.py|test_rename_speaker_updates_segments_in_database|assertions=4",
- "assertion_roulette|tests/integration/test_preferences_repository.py|test_set_bulk_creates_multiple_preferences|assertions=4",
- "assertion_roulette|tests/integration/test_signal_handling.py|test_cleanup_all_active_streams|assertions=7",
- "assertion_roulette|tests/integration/test_signal_handling.py|test_shutdown_cancels_diarization_tasks|assertions=4",
- "assertion_roulette|tests/integration/test_unit_of_work_advanced.py|test_diarization_job_workflow|assertions=4",
- "assertion_roulette|tests/integration/test_unit_of_work_advanced.py|test_meeting_lifecycle_workflow|assertions=11",
- "assertion_roulette|tests/integration/test_unit_of_work_advanced.py|test_repository_instances_cached_within_context|assertions=6",
- "assertion_roulette|tests/integration/test_webhook_integration.py|test_stop_meeting_triggers_meeting_completed_webhook|assertions=6",
- "assertion_roulette|tests/scripts/test_migrate_logging.py|test_adds_get_logger_to_existing_import|assertions=4",
- "assertion_roulette|tests/scripts/test_migrate_logging.py|test_grpc_service_pattern|assertions=4",
- "assertion_roulette|tests/scripts/test_migrate_logging.py|test_keeps_import_logging_when_constants_used|assertions=5",
- "assertion_roulette|tests/scripts/test_migrate_logging.py|test_transforms_complex_module|assertions=5",
- "assertion_roulette|tests/scripts/test_migrate_logging.py|test_transforms_simple_module|assertions=4",
- "assertion_roulette|tests/scripts/test_migrate_logging.py|test_uses_logging_constants_detection|assertions=4"
- ],
"conditional_test_logic": [
"conditional_test_logic|tests/application/test_meeting_service.py|test_meeting_state_transitions|if@122",
- "conditional_test_logic|tests/grpc/test_sprint_15_1_critical_bugs.py|test_no_datetime_now_in_diarization_mixin|for@234",
- "conditional_test_logic|tests/grpc/test_stream_lifecycle.py|test_double_start_same_meeting_id_detected|if@454",
- "conditional_test_logic|tests/infrastructure/audio/test_capture.py|test_get_default_device_returns_device_or_none|if@42",
- "conditional_test_logic|tests/infrastructure/audio/test_ring_buffer.py|test_chunk_count_property|for@173",
- "conditional_test_logic|tests/infrastructure/audio/test_ring_buffer.py|test_get_window_chronological_order|for@132",
- "conditional_test_logic|tests/infrastructure/audio/test_ring_buffer.py|test_ring_buffer_duration_property|for@164",
- "conditional_test_logic|tests/infrastructure/ner/test_engine.py|test_confidence_is_set|for@126",
- "conditional_test_logic|tests/infrastructure/ner/test_engine.py|test_normalized_text_is_lowercase|for@119",
- "conditional_test_logic|tests/infrastructure/persistence/test_migrations.py|test_all_migrations_have_down_revision|for@54",
- "conditional_test_logic|tests/infrastructure/persistence/test_migrations.py|test_all_migrations_have_downgrade_function|for@85",
- "conditional_test_logic|tests/infrastructure/persistence/test_migrations.py|test_all_migrations_have_revision|for@34",
- "conditional_test_logic|tests/infrastructure/persistence/test_migrations.py|test_all_migrations_have_upgrade_function|for@74",
- "conditional_test_logic|tests/infrastructure/test_observability.py|test_rapid_collection_maintains_order|for@403",
- "conditional_test_logic|tests/infrastructure/test_observability.py|test_rapid_sequential_logging|for@356",
- "conditional_test_logic|tests/infrastructure/triggers/test_calendar.py|test_datetime_parsing_formats|if@315",
- "conditional_test_logic|tests/infrastructure/triggers/test_calendar.py|test_overlap_scenarios|if@177",
- "conditional_test_logic|tests/integration/test_crash_scenarios.py|test_concurrent_recovery_calls|for@359",
- "conditional_test_logic|tests/integration/test_database_resilience.py|test_concurrent_creates_unique_ids|for@235",
- "conditional_test_logic|tests/integration/test_entity_repository.py|test_saves_multiple_entities|for@193",
- "conditional_test_logic|tests/integration/test_recovery_service.py|test_recovers_multiple_meetings|for@146",
- "conditional_test_logic|tests/integration/test_signal_handling.py|test_shutdown_cancels_diarization_tasks|for@85"
- ],
- "deprecated_pattern": [
- "deprecated_pattern|src/noteflow/infrastructure/export/html.py|b089eb78|str.format()"
- ],
- "duplicate_test_name": [
- "duplicate_test_name|tests/application/test_recovery_service.py|test_audio_validation_skipped_without_meetings_dir|count=2",
- "duplicate_test_name|tests/config/test_feature_flags.py|test_default_values|count=4",
- "duplicate_test_name|tests/domain/test_project.py|test_is_frozen|count=2",
- "duplicate_test_name|tests/domain/test_project.py|test_with_values|count=2",
- "duplicate_test_name|tests/grpc/test_annotation_mixin.py|test_aborts_on_invalid_annotation_id|count=3",
- "duplicate_test_name|tests/grpc/test_annotation_mixin.py|test_aborts_on_invalid_meeting_id|count=2",
- "duplicate_test_name|tests/grpc/test_annotation_mixin.py|test_aborts_when_annotation_not_found|count=3",
- "duplicate_test_name|tests/grpc/test_entities_mixin.py|test_aborts_when_entity_belongs_to_different_meeting|count=2",
- "duplicate_test_name|tests/grpc/test_entities_mixin.py|test_aborts_when_entity_not_found|count=2",
- "duplicate_test_name|tests/grpc/test_entities_mixin.py|test_aborts_with_invalid_entity_id_format|count=2",
- "duplicate_test_name|tests/grpc/test_entities_mixin.py|test_aborts_with_invalid_meeting_id_format|count=2",
- "duplicate_test_name|tests/grpc/test_entities_mixin.py|test_aborts_with_invalid_meeting_id|count=2",
- "duplicate_test_name|tests/grpc/test_project_mixin.py|test_delete_project_not_found|count=2",
- "duplicate_test_name|tests/infrastructure/summarization/test_cloud_provider.py|test_raises_invalid_response_on_empty_content|count=2",
- "duplicate_test_name|tests/infrastructure/summarization/test_cloud_provider.py|test_summarize_returns_result|count=2"
+ "conditional_test_logic|tests/grpc/test_stream_lifecycle.py|test_double_start_same_meeting_id_detected|if@456",
+ "conditional_test_logic|tests/infrastructure/audio/test_capture.py|test_get_default_device_returns_device_or_none|if@46",
+ "conditional_test_logic|tests/infrastructure/triggers/test_calendar.py|test_overlap_scenarios|if@181"
],
"eager_test": [
"eager_test|tests/infrastructure/audio/test_writer.py|test_audio_roundtrip_encryption_decryption|methods=15",
@@ -107,9 +21,6 @@
"exception_handling|tests/integration/test_memory_fallback.py|test_concurrent_reads_and_writes|catches_Exception",
"exception_handling|tests/integration/test_memory_fallback.py|test_concurrent_reads_and_writes|catches_Exception"
],
- "high_complexity": [
- "high_complexity|src/noteflow/infrastructure/observability/usage.py|record|complexity=20"
- ],
"long_method": [
"long_method|src/noteflow/application/services/summarization_service.py|summarize|lines=104",
"long_method|src/noteflow/grpc/_mixins/oidc.py|RegisterOidcProvider|lines=79",
@@ -138,106 +49,21 @@
],
"long_test": [
"long_test|tests/infrastructure/audio/test_capture.py|test_start_with_stubbed_stream_invokes_callback|lines=54",
- "long_test|tests/integration/test_e2e_streaming.py|test_segments_persisted_to_database|lines=70",
- "long_test|tests/integration/test_unit_of_work_advanced.py|test_meeting_lifecycle_workflow|lines=63"
- ],
- "magic_number_test": [
- "magic_number_test|tests/domain/test_annotation.py|test_annotation_very_long_duration|value=7200.0",
- "magic_number_test|tests/domain/test_meeting.py|test_duration_seconds_with_times|value=1800.0",
- "magic_number_test|tests/domain/test_segment.py|test_segment_very_long_duration|value=36000.0",
- "magic_number_test|tests/domain/test_summary.py|test_key_point_with_many_segment_ids|value=50",
- "magic_number_test|tests/domain/test_summary.py|test_key_point_with_timing|value=10.5",
- "magic_number_test|tests/domain/test_summary.py|test_key_point_with_timing|value=25.0",
- "magic_number_test|tests/domain/test_summary.py|test_summary_very_long_executive_summary|value=10000",
- "magic_number_test|tests/grpc/test_annotation_mixin.py|test_returns_annotation_when_found|value=120.0",
- "magic_number_test|tests/grpc/test_annotation_mixin.py|test_updates_annotation_successfully|value=15.0",
- "magic_number_test|tests/grpc/test_annotation_mixin.py|test_updates_annotation_successfully|value=25.0",
- "magic_number_test|tests/grpc/test_annotation_mixin.py|test_updates_text_only|value=20.0",
- "magic_number_test|tests/grpc/test_diarization_cancel.py|test_progress_percent_running|value=50.0",
- "magic_number_test|tests/grpc/test_diarization_mixin.py|test_status_progress_running_is_time_based|value=50.0",
- "magic_number_test|tests/grpc/test_meeting_mixin.py|test_list_meetings_respects_limit|value=25",
- "magic_number_test|tests/grpc/test_meeting_mixin.py|test_list_meetings_respects_offset|value=50",
- "magic_number_test|tests/grpc/test_preferences_mixin.py|test_computes_deterministic_etag|value=32",
- "magic_number_test|tests/grpc/test_stream_lifecycle.py|test_partial_buffers_cleared_on_cleanup|value=3200",
- "magic_number_test|tests/grpc/test_timestamp_converters.py|test_epoch_seconds_to_datetime_returns_utc|value=2024",
- "magic_number_test|tests/grpc/test_timestamp_converters.py|test_iso_string_with_z_suffix_parsed_as_utc|value=14",
- "magic_number_test|tests/grpc/test_timestamp_converters.py|test_iso_string_with_z_suffix_parsed_as_utc|value=15",
- "magic_number_test|tests/grpc/test_timestamp_converters.py|test_iso_string_with_z_suffix_parsed_as_utc|value=2024",
- "magic_number_test|tests/grpc/test_timestamp_converters.py|test_iso_string_with_z_suffix_parsed_as_utc|value=30",
- "magic_number_test|tests/grpc/test_timestamp_converters.py|test_iso_string_with_z_suffix_parsed_as_utc|value=45",
- "magic_number_test|tests/infrastructure/asr/test_segmenter.py|test_custom_config|value=44100",
- "magic_number_test|tests/infrastructure/asr/test_segmenter.py|test_custom_config|value=60.0",
- "magic_number_test|tests/infrastructure/audio/test_capture.py|test_properties_after_start|value=44100",
- "magic_number_test|tests/infrastructure/audio/test_dto.py|test_timestamped_audio_creation|value=1600",
- "magic_number_test|tests/infrastructure/audio/test_reader.py|test_reader_uses_manifest_sample_rate|value=1600",
- "magic_number_test|tests/infrastructure/audio/test_reader.py|test_reader_uses_manifest_sample_rate|value=48000",
- "magic_number_test|tests/infrastructure/audio/test_reader.py|test_reader_uses_manifest_sample_rate|value=48000",
- "magic_number_test|tests/infrastructure/audio/test_ring_buffer.py|test_init_with_default_duration|value=30.0",
- "magic_number_test|tests/infrastructure/audio/test_ring_buffer.py|test_max_duration_property|value=15.0",
- "magic_number_test|tests/infrastructure/audio/test_writer.py|test_write_chunk_converts_float32_to_pcm16|value=3200",
- "magic_number_test|tests/infrastructure/summarization/test_cloud_provider.py|test_summarize_returns_result|value=150",
- "magic_number_test|tests/infrastructure/summarization/test_cloud_provider.py|test_summarize_returns_result|value=150",
- "magic_number_test|tests/infrastructure/test_diarization.py|test_overlap_duration_full_overlap|value=15.0",
- "magic_number_test|tests/infrastructure/test_diarization.py|test_overlap_duration_no_overlap|value=12.0",
- "magic_number_test|tests/infrastructure/test_diarization.py|test_overlap_duration_no_overlap|value=20.0",
- "magic_number_test|tests/infrastructure/test_diarization.py|test_overlap_duration_partial_overlap_right|value=15.0",
- "magic_number_test|tests/infrastructure/test_integration_converters.py|test_converts_stats_dict|value=15",
- "magic_number_test|tests/infrastructure/test_integration_converters.py|test_sync_run_orm_to_domain|value=5000",
- "magic_number_test|tests/infrastructure/test_integration_converters.py|test_sync_run_to_orm_kwargs|value=10000",
- "magic_number_test|tests/infrastructure/test_integration_converters.py|test_sync_run_to_orm_kwargs|value=25",
- "magic_number_test|tests/infrastructure/test_observability.py|test_log_with_large_details|value=50",
- "magic_number_test|tests/infrastructure/triggers/test_calendar.py|test_non_iterable_returns_empty|value=12345",
- "magic_number_test|tests/integration/test_e2e_annotations.py|test_add_annotation_persists_to_database|value=15.0"
+ "long_test|tests/integration/test_e2e_streaming.py|test_segments_persisted_to_database|lines=74",
+ "long_test|tests/integration/test_e2e_streaming.py|test_stream_init_recovers_streaming_turns|lines=51",
+ "long_test|tests/integration/test_e2e_summarization.py|test_generate_summary_regenerates_with_force_flag|lines=52",
+ "long_test|tests/integration/test_e2e_summarization.py|test_summary_with_key_points_persisted|lines=52",
+ "long_test|tests/integration/test_unit_of_work_advanced.py|test_meeting_lifecycle_workflow|lines=63",
+ "long_test|tests/integration/test_webhook_integration.py|test_stop_meeting_triggers_meeting_completed_webhook|lines=61"
],
"module_size_soft": [
- "module_size_soft|src/noteflow/config/settings.py|module|lines=566",
+ "module_size_soft|src/noteflow/config/settings.py|module|lines=579",
"module_size_soft|src/noteflow/domain/ports/repositories/identity.py|module|lines=599",
- "module_size_soft|src/noteflow/grpc/server.py|module|lines=534"
+ "module_size_soft|src/noteflow/grpc/server.py|module|lines=537"
],
"orphaned_import": [
"orphaned_import|src/noteflow/infrastructure/observability/otel.py|opentelemetry"
],
- "raises_without_match": [
- "raises_without_match|tests/domain/test_project.py|test_archive_default_project_raises|line=261",
- "raises_without_match|tests/domain/test_project.py|test_default_project_cannot_be_archived|line=544",
- "raises_without_match|tests/grpc/test_stream_lifecycle.py|test_cancelled_error_propagation_in_stream|line=647",
- "raises_without_match|tests/infrastructure/asr/test_dto.py|test_word_timing_frozen|line=41",
- "raises_without_match|tests/infrastructure/audio/test_dto.py|test_audio_device_info_frozen|line=42",
- "raises_without_match|tests/infrastructure/auth/test_oidc_registry.py|test_create_provider_discovery_failure|line=154",
- "raises_without_match|tests/integration/test_e2e_annotations.py|test_add_annotation_invalid_meeting_id|line=341",
- "raises_without_match|tests/integration/test_e2e_annotations.py|test_annotations_deleted_with_meeting|line=460",
- "raises_without_match|tests/integration/test_e2e_annotations.py|test_delete_annotation_not_found_e2e|line=386",
- "raises_without_match|tests/integration/test_e2e_annotations.py|test_get_annotation_not_found|line=355",
- "raises_without_match|tests/integration/test_e2e_annotations.py|test_update_annotation_not_found|line=372",
- "raises_without_match|tests/integration/test_e2e_export.py|test_export_transcript_invalid_meeting_id|line=427",
- "raises_without_match|tests/integration/test_e2e_export.py|test_export_transcript_nonexistent_meeting|line=410",
- "raises_without_match|tests/integration/test_e2e_streaming.py|test_concurrent_streams_rejected|line=356",
- "raises_without_match|tests/integration/test_e2e_streaming.py|test_stream_init_fails_for_nonexistent_meeting|line=191",
- "raises_without_match|tests/integration/test_e2e_streaming.py|test_stream_rejects_invalid_meeting_id|line=213",
- "raises_without_match|tests/integration/test_e2e_summarization.py|test_generate_summary_invalid_meeting_id|line=499",
- "raises_without_match|tests/integration/test_e2e_summarization.py|test_generate_summary_nonexistent_meeting|line=485",
- "raises_without_match|tests/integration/test_error_handling.py|test_delete_nonexistent_annotation|line=600",
- "raises_without_match|tests/integration/test_error_handling.py|test_delete_nonexistent_meeting|line=107",
- "raises_without_match|tests/integration/test_error_handling.py|test_duplicate_job_id_rejected|line=225",
- "raises_without_match|tests/integration/test_error_handling.py|test_empty_meeting_id|line=79",
- "raises_without_match|tests/integration/test_error_handling.py|test_export_nonexistent_meeting|line=487",
- "raises_without_match|tests/integration/test_error_handling.py|test_get_nonexistent_annotation|line=569",
- "raises_without_match|tests/integration/test_error_handling.py|test_get_status_nonexistent_job|line=619",
- "raises_without_match|tests/integration/test_error_handling.py|test_invalid_uuid_format_for_meeting_id|line=65",
- "raises_without_match|tests/integration/test_error_handling.py|test_nonexistent_meeting_returns_not_found|line=93",
- "raises_without_match|tests/integration/test_error_handling.py|test_summarize_nonexistent_meeting|line=534",
- "raises_without_match|tests/integration/test_error_handling.py|test_update_nonexistent_annotation|line=586",
- "raises_without_match|tests/integration/test_grpc_servicer_database.py|test_get_nonexistent_job_returns_not_found|line=335",
- "raises_without_match|tests/integration/test_grpc_servicer_database.py|test_get_nonexistent_meeting_returns_not_found|line=166",
- "raises_without_match|tests/integration/test_project_repository.py|test_archive_default_project_raises_repository|line=276",
- "raises_without_match|tests/integration/test_trigger_settings.py|test_retention_check_interval_validation|line=91",
- "raises_without_match|tests/integration/test_trigger_settings.py|test_retention_check_interval_validation|line=98",
- "raises_without_match|tests/integration/test_trigger_settings.py|test_retention_days_validation|line=77",
- "raises_without_match|tests/integration/test_trigger_settings.py|test_retention_days_validation|line=81",
- "raises_without_match|tests/integration/test_unit_of_work_advanced.py|test_exception_during_segment_add_rolls_back_meeting|line=252",
- "raises_without_match|tests/stress/test_transaction_boundaries.py|test_batch_segment_add_rollback|line=196",
- "raises_without_match|tests/stress/test_transaction_boundaries.py|test_exception_type_does_not_matter|line=78"
- ],
"sensitive_equality": [
"sensitive_equality|tests/domain/test_project.py|test_error_message_includes_project_id|str",
"sensitive_equality|tests/integration/test_e2e_streaming.py|test_active_stream_removed_on_completion|str",
@@ -245,7 +71,7 @@
"sensitive_equality|tests/integration/test_grpc_servicer_database.py|test_refine_speaker_diarization_creates_job_in_database|str"
],
"sleepy_test": [
- "sleepy_test|tests/integration/test_e2e_streaming.py|test_stop_request_exits_stream_gracefully|line=456",
+ "sleepy_test|tests/integration/test_e2e_streaming.py|test_stop_request_exits_stream_gracefully|line=481",
"sleepy_test|tests/integration/test_unit_of_work_advanced.py|test_concurrent_uow_instances_isolated|line=165",
"sleepy_test|tests/integration/test_unit_of_work_advanced.py|test_concurrent_uow_instances_isolated|line=171"
],
@@ -292,12 +118,6 @@
"thin_wrapper|src/noteflow/infrastructure/persistence/memory/repositories/integration.py|get_sync_run|get",
"thin_wrapper|src/noteflow/infrastructure/persistence/memory/repositories/webhook.py|get_by_id|get",
"thin_wrapper|src/noteflow/infrastructure/security/crypto.py|generate_dek|token_bytes"
- ],
- "unused_fixture": [
- "unused_fixture|tests/grpc/test_export_mixin.py|test_export_aborts_when_meeting_not_found|mock_meetings_repo",
- "unused_fixture|tests/grpc/test_export_mixin.py|test_export_aborts_when_meeting_not_found|mock_segments_repo",
- "unused_fixture|tests/grpc/test_stream_lifecycle.py|test_audio_writer_closed_on_cleanup|crypto",
- "unused_fixture|tests/grpc/test_stream_lifecycle.py|test_context_cancelled_check_pattern|memory_servicer"
]
},
"schema_version": 1
diff --git a/tests/scripts/test_migrate_logging.py b/tests/scripts/test_migrate_logging.py
index 55c7242..36830b3 100644
--- a/tests/scripts/test_migrate_logging.py
+++ b/tests/scripts/test_migrate_logging.py
@@ -129,30 +129,30 @@ class TestNeedsMigration:
def test_detects_stdlib_pattern(self) -> None:
"""Detects standard stdlib logging pattern."""
- assert needs_migration(SAMPLE_STDLIB_LOGGER) is True
+ assert needs_migration(SAMPLE_STDLIB_LOGGER) is True, "stdlib logging pattern should require migration"
def test_ignores_already_migrated(self) -> None:
"""Already migrated files don't need migration."""
# File has get_logger instead of logging.getLogger, so pattern won't match
- assert needs_migration(SAMPLE_ALREADY_MIGRATED) is False
+ assert needs_migration(SAMPLE_ALREADY_MIGRATED) is False, "already migrated file should not need migration"
def test_ignores_no_logging(self) -> None:
"""Files without logging don't need migration."""
- assert needs_migration(SAMPLE_NO_LOGGING) is False
+ assert needs_migration(SAMPLE_NO_LOGGING) is False, "file without logging should not need migration"
def test_detects_complex_module(self) -> None:
"""Detects logging in complex modules."""
- assert needs_migration(SAMPLE_COMPLEX_MODULE) is True
+ assert needs_migration(SAMPLE_COMPLEX_MODULE) is True, "complex module with logging should require migration"
def test_requires_both_import_and_getlogger(self) -> None:
"""Requires both import logging AND getLogger call."""
# Only import, no getLogger
only_import = "import logging\n\ndef foo(): pass"
- assert needs_migration(only_import) is False
+ assert needs_migration(only_import) is False, "file with only import (no getLogger) should not need migration"
# Only getLogger, no import (invalid but should handle)
only_getlogger = "logger = logging.getLogger(__name__)"
- assert needs_migration(only_getlogger) is False
+ assert needs_migration(only_getlogger) is False, "file with only getLogger (no import) should not need migration"
class TestHasInfrastructureImport:
@@ -161,17 +161,17 @@ class TestHasInfrastructureImport:
def test_detects_get_logger_import(self) -> None:
"""Detects get_logger import."""
content = "from noteflow.infrastructure.logging import get_logger"
- assert has_infrastructure_import(content) is True
+ assert has_infrastructure_import(content) is True, "should detect get_logger import from infrastructure.logging"
def test_detects_multi_import(self) -> None:
"""Detects multi-symbol import."""
content = "from noteflow.infrastructure.logging import get_logger, configure_logging"
- assert has_infrastructure_import(content) is True
+ assert has_infrastructure_import(content) is True, "should detect multi-symbol import from infrastructure.logging"
def test_no_import(self) -> None:
"""No import returns False."""
content = "import logging"
- assert has_infrastructure_import(content) is False
+ assert has_infrastructure_import(content) is False, "stdlib logging import should not be detected as infrastructure import"
class TestShouldSkipFile:
@@ -179,22 +179,22 @@ class TestShouldSkipFile:
def test_skips_proto_files(self) -> None:
"""Skips protobuf generated files."""
- assert should_skip_file(Path('foo_pb2.py')) is True
- assert should_skip_file(Path('foo_pb2_grpc.py')) is True
+ assert should_skip_file(Path('foo_pb2.py')) is True, "should skip *_pb2.py protobuf files"
+ assert should_skip_file(Path('foo_pb2_grpc.py')) is True, "should skip *_pb2_grpc.py protobuf files"
def test_skips_logging_module_files(self) -> None:
"""Skips files in logging module itself."""
assert should_skip_file(
Path('src/noteflow/infrastructure/logging/config.py')
- ) is True
+ ) is True, "should skip files within infrastructure/logging module"
assert should_skip_file(
Path('src/noteflow/infrastructure/logging/__init__.py')
- ) is True
+ ) is True, "should skip __init__.py within infrastructure/logging module"
def test_allows_normal_files(self) -> None:
"""Allows normal Python files."""
- assert should_skip_file(Path('src/noteflow/grpc/client.py')) is False
- assert should_skip_file(Path('src/noteflow/application/services/foo.py')) is False
+ assert should_skip_file(Path('src/noteflow/grpc/client.py')) is False, "should not skip normal grpc files"
+ assert should_skip_file(Path('src/noteflow/application/services/foo.py')) is False, "should not skip normal application service files"
class TestTransformFile:
@@ -207,10 +207,10 @@ class TestTransformFile:
result = transform_file(test_file)
- assert result.has_changes is True
- assert 'from noteflow.infrastructure.logging import get_logger' in result.transformed
- assert 'logger = get_logger(__name__)' in result.transformed
- assert 'logging.getLogger(__name__)' not in result.transformed
+ assert result.has_changes is True, "simple module with logging should have changes"
+ assert 'from noteflow.infrastructure.logging import get_logger' in result.transformed, "transformed code should include infrastructure import"
+ assert 'logger = get_logger(__name__)' in result.transformed, "transformed code should use get_logger(__name__)"
+ assert 'logging.getLogger(__name__)' not in result.transformed, "transformed code should not contain logging.getLogger"
def test_transforms_complex_module(self, tmp_path: Path) -> None:
"""Transforms complex module preserving structure, removes unused import logging."""
@@ -219,15 +219,15 @@ class TestTransformFile:
result = transform_file(test_file)
- assert result.has_changes is True
+ assert result.has_changes is True, "complex module with logging should have changes"
# import logging should be removed (not using constants)
- assert 'import logging' not in result.transformed
+ assert 'import logging' not in result.transformed, "unused import logging should be removed"
# Infrastructure import should be present
- assert 'from noteflow.infrastructure.logging import get_logger' in result.transformed
+ assert 'from noteflow.infrastructure.logging import get_logger' in result.transformed, "transformed code should include infrastructure import"
# Check getLogger replaced
- assert 'logger = get_logger(__name__)' in result.transformed
+ assert 'logger = get_logger(__name__)' in result.transformed, "transformed code should use get_logger(__name__)"
# Changes should include removal
- assert 'Removed unused import logging' in result.changes
+ assert 'Removed unused import logging' in result.changes, "changes should document removal of unused import"
def test_handles_multiple_loggers(self, tmp_path: Path) -> None:
"""Only transforms main __name__ logger, not child loggers."""
@@ -236,11 +236,11 @@ class TestTransformFile:
result = transform_file(test_file)
- assert result.has_changes is True
+ assert result.has_changes is True, "module with multiple loggers should have changes"
# Main logger transformed
- assert 'logger = get_logger(__name__)' in result.transformed
+ assert 'logger = get_logger(__name__)' in result.transformed, "main logger should be transformed to get_logger"
# Child logger NOT transformed (different pattern)
- assert 'child_logger = logging.getLogger(__name__ + ".child")' in result.transformed
+ assert 'child_logger = logging.getLogger(__name__ + ".child")' in result.transformed, "child logger with custom name should not be transformed"
def test_no_change_for_already_migrated(self, tmp_path: Path) -> None:
"""No changes for already migrated files."""
@@ -249,7 +249,7 @@ class TestTransformFile:
result = transform_file(test_file)
- assert result.has_changes is False
+ assert result.has_changes is False, "already migrated file should have no changes"
def test_no_change_for_no_logging(self, tmp_path: Path) -> None:
"""No changes for files without logging."""
@@ -258,7 +258,7 @@ class TestTransformFile:
result = transform_file(test_file)
- assert result.has_changes is False
+ assert result.has_changes is False, "file without logging should have no changes"
def test_preserves_file_structure(self, tmp_path: Path) -> None:
"""Preserves docstrings, imports order, and code structure."""
@@ -268,11 +268,11 @@ class TestTransformFile:
result = transform_file(test_file)
# Docstring preserved
- assert '"""Complex module with various patterns."""' in result.transformed
+ assert '"""Complex module with various patterns."""' in result.transformed, "module docstring should be preserved"
# Class preserved
- assert 'class MyService:' in result.transformed
+ assert 'class MyService:' in result.transformed, "class definition should be preserved"
# Type checking block preserved
- assert 'if TYPE_CHECKING:' in result.transformed
+ assert 'if TYPE_CHECKING:' in result.transformed, "TYPE_CHECKING block should be preserved"
class TestValidateTransformation:
@@ -301,7 +301,7 @@ class TestValidateTransformation:
errors = validate_transformation(result)
- assert any('Missing infrastructure.logging import' in e for e in errors)
+ assert any('Missing infrastructure.logging import' in e for e in errors), f"should detect missing import error, got: {errors}"
def test_detects_syntax_error(self, tmp_path: Path) -> None:
"""Detects syntax errors in transformed code."""
@@ -313,7 +313,7 @@ class TestValidateTransformation:
errors = validate_transformation(result)
- assert any('Syntax error' in e for e in errors)
+ assert any('Syntax error' in e for e in errors), f"should detect syntax error, got: {errors}"
def test_no_errors_for_unchanged(self, tmp_path: Path) -> None:
"""No errors for unchanged files."""
@@ -325,7 +325,7 @@ class TestValidateTransformation:
errors = validate_transformation(result)
- assert errors == []
+ assert errors == [], f"unchanged file should have no validation errors, got: {errors}"
class TestTransformResultDiff:
@@ -339,9 +339,9 @@ class TestTransformResultDiff:
result = transform_file(test_file)
diff = result.get_diff()
- assert '-logger = logging.getLogger(__name__)' in diff
- assert '+logger = get_logger(__name__)' in diff
- assert '+from noteflow.infrastructure.logging import get_logger' in diff
+ assert '-logger = logging.getLogger(__name__)' in diff, "diff should show removed logging.getLogger line"
+ assert '+logger = get_logger(__name__)' in diff, "diff should show added get_logger line"
+ assert '+from noteflow.infrastructure.logging import get_logger' in diff, "diff should show added infrastructure import"
def test_no_diff_for_unchanged(self, tmp_path: Path) -> None:
"""No diff for unchanged files."""
@@ -351,7 +351,7 @@ class TestTransformResultDiff:
result = transform_file(test_file)
diff = result.get_diff()
- assert diff == ''
+ assert diff == '', f"unchanged file should have empty diff, got: {diff!r}"
class TestEndToEndScenarios:
@@ -390,10 +390,10 @@ class TestEndToEndScenarios:
result = transform_file(test_file)
errors = validate_transformation(result)
- assert result.has_changes is True
- assert errors == []
- assert 'from noteflow.infrastructure.logging import get_logger' in result.transformed
- assert 'logger = get_logger(__name__)' in result.transformed
+ assert result.has_changes is True, "gRPC service file should have changes"
+ assert errors == [], f"gRPC service transformation should have no errors, got: {errors}"
+ assert 'from noteflow.infrastructure.logging import get_logger' in result.transformed, "transformed gRPC service should include infrastructure import"
+ assert 'logger = get_logger(__name__)' in result.transformed, "transformed gRPC service should use get_logger"
def test_application_service_pattern(self, tmp_path: Path) -> None:
"""Transforms typical application service file."""
@@ -428,10 +428,10 @@ class TestEndToEndScenarios:
result = transform_file(test_file)
errors = validate_transformation(result)
- assert result.has_changes is True
- assert errors == []
+ assert result.has_changes is True, "application service file should have changes"
+ assert errors == [], f"application service transformation should have no errors, got: {errors}"
# Percent-style formatting should be preserved
- assert 'logger.info("Creating meeting: %s", title)' in result.transformed
+ assert 'logger.info("Creating meeting: %s", title)' in result.transformed, "percent-style formatting should be preserved"
def test_infrastructure_adapter_pattern(self, tmp_path: Path) -> None:
"""Transforms typical infrastructure adapter file."""
@@ -467,10 +467,10 @@ class TestEndToEndScenarios:
result = transform_file(test_file)
errors = validate_transformation(result)
- assert result.has_changes is True
- assert errors == []
+ assert result.has_changes is True, "infrastructure adapter file should have changes"
+ assert errors == [], f"infrastructure adapter transformation should have no errors, got: {errors}"
# Exception logging preserved
- assert 'logger.exception("Query failed")' in result.transformed
+ assert 'logger.exception("Query failed")' in result.transformed, "exception logging should be preserved"
class TestEdgeCases:
@@ -491,7 +491,7 @@ class TestEdgeCases:
result = transform_file(test_file)
# Custom name pattern doesn't match, so no migration needed
- assert result.has_changes is False
+ assert result.has_changes is False, "logger with custom name should not be transformed"
def test_multiline_getlogger(self, tmp_path: Path) -> None:
"""Handles multiline getLogger call."""
@@ -514,7 +514,7 @@ class TestEdgeCases:
# Our regex is simple and won't match multiline
# This is acceptable - user can migrate manually
# The key is we don't break anything
- assert 'logging' in result.transformed # File should still work
+ assert 'logging' in result.transformed, "multiline getLogger should be preserved (not broken)"
def test_empty_file(self, tmp_path: Path) -> None:
"""Handles empty file gracefully."""
@@ -523,8 +523,8 @@ class TestEdgeCases:
result = transform_file(test_file)
- assert result.has_changes is False
- assert result.transformed == ''
+ assert result.has_changes is False, "empty file should have no changes"
+ assert result.transformed == '', "empty file transformation should produce empty string"
def test_file_with_only_comments(self, tmp_path: Path) -> None:
"""Handles file with only comments."""
@@ -535,7 +535,7 @@ class TestEdgeCases:
result = transform_file(test_file)
- assert result.has_changes is False
+ assert result.has_changes is False, "file with only comments should have no changes"
def test_keeps_import_logging_when_constants_used(self, tmp_path: Path) -> None:
"""Keeps import logging when logging constants are used."""
@@ -544,20 +544,20 @@ class TestEdgeCases:
result = transform_file(test_file)
- assert result.has_changes is True
+ assert result.has_changes is True, "file with logging constants should have changes"
# import logging should be KEPT (logging.DEBUG is used)
- assert 'import logging' in result.transformed
- assert 'from noteflow.infrastructure.logging import get_logger' in result.transformed
- assert 'logger = get_logger(__name__)' in result.transformed
+ assert 'import logging' in result.transformed, "import logging should be kept when constants are used"
+ assert 'from noteflow.infrastructure.logging import get_logger' in result.transformed, "infrastructure import should be added"
+ assert 'logger = get_logger(__name__)' in result.transformed, "logger should use get_logger"
# Should NOT have the removal message
- assert 'Removed unused import logging' not in result.changes
+ assert 'Removed unused import logging' not in result.changes, "should not remove import logging when constants are used"
def test_uses_logging_constants_detection(self) -> None:
"""Tests uses_logging_constants function directly."""
- assert uses_logging_constants("logging.DEBUG") is True
- assert uses_logging_constants("logging.StreamHandler()") is True
- assert uses_logging_constants("logging.basicConfig()") is True
- assert uses_logging_constants("logger.info('hello')") is False
+ assert uses_logging_constants("logging.DEBUG") is True, "should detect logging.DEBUG constant"
+ assert uses_logging_constants("logging.StreamHandler()") is True, "should detect logging.StreamHandler usage"
+ assert uses_logging_constants("logging.basicConfig()") is True, "should detect logging.basicConfig usage"
+ assert uses_logging_constants("logger.info('hello')") is False, "logger.info should not be detected as constant usage"
def test_adds_get_logger_to_existing_import(self, tmp_path: Path) -> None:
"""Adds get_logger to existing infrastructure import."""
@@ -578,7 +578,7 @@ class TestEdgeCases:
result = transform_file(test_file)
- assert result.has_changes is True
- assert 'get_logger, configure_logging' in result.transformed
- assert 'logger = get_logger(__name__)' in result.transformed
- assert 'Added get_logger to existing import' in result.changes
+ assert result.has_changes is True, "file with partial infrastructure import should have changes"
+ assert 'get_logger, configure_logging' in result.transformed, "get_logger should be added to existing import"
+ assert 'logger = get_logger(__name__)' in result.transformed, "logger should use get_logger"
+ assert 'Added get_logger to existing import' in result.changes, "changes should document adding get_logger to existing import"
diff --git a/tests/test_constants_sync.py b/tests/test_constants_sync.py
index ff11331..ec77a72 100644
--- a/tests/test_constants_sync.py
+++ b/tests/test_constants_sync.py
@@ -22,6 +22,11 @@ from noteflow.config.constants import (
# Path to Rust constants file
RUST_CONSTANTS_PATH = Path(__file__).parent.parent / "client/src-tauri/src/constants.rs"
+# Expected constant values for validation tests
+EXPECTED_SAMPLE_RATE_HZ = 16000 # 16kHz standard for speech recognition
+EXPECTED_GRPC_PORT = 50051 # Standard gRPC port convention
+EXPECTED_SECONDS_PER_HOUR = 3600 # 60 * 60
+
def _parse_rust_constant(content: str, module: str, name: str) -> str | None:
"""Extract a constant value from Rust source.
@@ -109,12 +114,18 @@ class TestConstantValues:
def test_sample_rate_is_16khz(self) -> None:
"""Standard sample rate for speech recognition is 16kHz."""
- assert DEFAULT_SAMPLE_RATE == 16000, "DEFAULT_SAMPLE_RATE should be 16000 Hz"
+ assert DEFAULT_SAMPLE_RATE == EXPECTED_SAMPLE_RATE_HZ, (
+ f"DEFAULT_SAMPLE_RATE should be {EXPECTED_SAMPLE_RATE_HZ} Hz"
+ )
def test_grpc_port_is_standard(self) -> None:
"""Default gRPC port follows convention (50051)."""
- assert DEFAULT_GRPC_PORT == 50051, "DEFAULT_GRPC_PORT should be 50051"
+ assert DEFAULT_GRPC_PORT == EXPECTED_GRPC_PORT, (
+ f"DEFAULT_GRPC_PORT should be {EXPECTED_GRPC_PORT}"
+ )
def test_seconds_per_hour_is_correct(self) -> None:
"""Seconds per hour must be 3600."""
- assert SECONDS_PER_HOUR == 3600, "SECONDS_PER_HOUR should be 3600"
+ assert SECONDS_PER_HOUR == EXPECTED_SECONDS_PER_HOUR, (
+ f"SECONDS_PER_HOUR should be {EXPECTED_SECONDS_PER_HOUR}"
+ )