- Moved all hookify configuration files from `.claude/` to `.claude/hooks/` subdirectory for better organization - Added four new blocking hooks to prevent common error handling anti-patterns: - `block-broad-exception-handler`: Prevents catching generic `Exception` with only logging - `block-datetime-now-fallback`: Blocks returning `datetime.now()` as fallback on parse failures to prevent data corruption - `block-default
145 lines
5.6 KiB
Python
145 lines
5.6 KiB
Python
"""Tests for Annotation entity."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from uuid import uuid4
|
|
|
|
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."""
|
|
|
|
def test_annotation_valid(self, sample_annotation: Annotation) -> None:
|
|
"""Annotation fixture has expected properties."""
|
|
assert sample_annotation.text == "Sample annotation text", "text should match fixture"
|
|
assert sample_annotation.duration == 1.0, "duration should be end - start"
|
|
assert sample_annotation.has_segments() is False, "no segment_ids means has_segments is False"
|
|
|
|
def test_annotation_invalid_times_raises(self) -> None:
|
|
"""Annotation raises when end_time < start_time."""
|
|
with pytest.raises(ValueError, match=r"end_time .* must be >= start_time"):
|
|
Annotation(
|
|
id=AnnotationId(uuid4()),
|
|
meeting_id=MeetingId(uuid4()),
|
|
annotation_type=AnnotationType.DECISION,
|
|
text="Bad timing",
|
|
start_time=5.0,
|
|
end_time=2.0,
|
|
)
|
|
|
|
def test_annotation_has_segments(self) -> None:
|
|
"""has_segments reflects segment_ids list."""
|
|
annotation = Annotation(
|
|
id=AnnotationId(uuid4()),
|
|
meeting_id=MeetingId(uuid4()),
|
|
annotation_type=AnnotationType.ACTION_ITEM,
|
|
text="Follow up",
|
|
start_time=0.0,
|
|
end_time=1.0,
|
|
segment_ids=[1, 2],
|
|
)
|
|
|
|
assert annotation.has_segments() is True, "has_segments should be True when segment_ids provided"
|
|
assert annotation.duration == 1.0, "duration should be end - start"
|
|
|
|
|
|
class TestAnnotationEdgeCases:
|
|
"""Edge case tests for Annotation entity."""
|
|
|
|
def test_annotation_zero_duration(self) -> None:
|
|
"""Test annotation with zero duration is valid."""
|
|
annotation = Annotation(
|
|
id=AnnotationId(uuid4()),
|
|
meeting_id=MeetingId(uuid4()),
|
|
annotation_type=AnnotationType.NOTE,
|
|
text="Instant note",
|
|
start_time=5.0,
|
|
end_time=5.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."""
|
|
annotation = Annotation(
|
|
id=AnnotationId(uuid4()),
|
|
meeting_id=MeetingId(uuid4()),
|
|
annotation_type=AnnotationType.NOTE,
|
|
text="",
|
|
start_time=0.0,
|
|
end_time=1.0,
|
|
)
|
|
assert annotation.text == "", "empty text should be preserved as empty string"
|
|
|
|
def test_annotation_unicode_text(self) -> None:
|
|
"""Test annotation with unicode text."""
|
|
annotation = Annotation(
|
|
id=AnnotationId(uuid4()),
|
|
meeting_id=MeetingId(uuid4()),
|
|
annotation_type=AnnotationType.NOTE,
|
|
text="重要な点 🎯 café",
|
|
start_time=0.0,
|
|
end_time=1.0,
|
|
)
|
|
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."""
|
|
many_segments = list(range(100))
|
|
annotation = Annotation(
|
|
id=AnnotationId(uuid4()),
|
|
meeting_id=MeetingId(uuid4()),
|
|
annotation_type=AnnotationType.DECISION,
|
|
text="Major decision",
|
|
start_time=0.0,
|
|
end_time=60.0,
|
|
segment_ids=many_segments,
|
|
)
|
|
assert annotation.has_segments() is True, "has_segments should be True with many segments"
|
|
assert len(annotation.segment_ids) == 100, "all segment_ids should be preserved"
|
|
|
|
def test_annotation_very_long_duration(self) -> None:
|
|
"""Test annotation spanning long duration."""
|
|
annotation = Annotation(
|
|
id=AnnotationId(uuid4()),
|
|
meeting_id=MeetingId(uuid4()),
|
|
annotation_type=AnnotationType.NOTE,
|
|
text="Long meeting note",
|
|
start_time=0.0,
|
|
end_time=TWO_HOURS_SECONDS,
|
|
)
|
|
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:
|
|
"""Test all annotation types are valid."""
|
|
meeting_id = MeetingId(uuid4())
|
|
annotation = Annotation(
|
|
id=AnnotationId(uuid4()),
|
|
meeting_id=meeting_id,
|
|
annotation_type=annotation_type,
|
|
text=f"Test {annotation_type.name}",
|
|
start_time=0.0,
|
|
end_time=1.0,
|
|
)
|
|
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."""
|
|
annotation = Annotation(
|
|
id=AnnotationId(uuid4()),
|
|
meeting_id=MeetingId(uuid4()),
|
|
annotation_type=AnnotationType.NOTE,
|
|
text="No segments",
|
|
start_time=0.0,
|
|
end_time=1.0,
|
|
segment_ids=[],
|
|
)
|
|
assert annotation.has_segments() is False, "empty segment_ids list should result in has_segments() returning False"
|