"""fence.py DTO 单元测试。 覆盖 ``FenceVerdict`` / ``FenceGeneration`` / ``FenceContext`` 的字段赋值、 默认值、不可变语义与 ``__post_init__`` 校验逻辑。 """ from __future__ import annotations import dataclasses from datetime import datetime import pytest from yuxi.channels.contract.dtos.fence import ( FenceContext, FenceGeneration, FenceVerdict, ) from yuxi.channels.contract.errors import ValidationError pytestmark = pytest.mark.unit @pytest.mark.unit class TestFenceVerdict: """栅栏裁决 DTO 测试。""" def test_fields_are_assigned(self): # Arrange # Act verdict = FenceVerdict(is_stale=True, current_generation=3, stale_generation=1) # Assert assert verdict.is_stale is True assert verdict.current_generation == 3 assert verdict.stale_generation == 1 def test_stale_generation_defaults_to_none(self): # Arrange # Act verdict = FenceVerdict(is_stale=False, current_generation=2) # Assert assert verdict.stale_generation is None def test_is_frozen(self): # Arrange verdict = FenceVerdict(is_stale=False, current_generation=0) # Act / Assert with pytest.raises(dataclasses.FrozenInstanceError): verdict.is_stale = True # type: ignore[misc] @pytest.mark.unit class TestFenceGeneration: """栅栏代际 DTO 测试。""" def test_fields_are_assigned(self): # Arrange # Act gen = FenceGeneration( conversation_id="conv-1", current_generation=3, visible_generation=2, ) # Assert assert gen.conversation_id == "conv-1" assert gen.current_generation == 3 assert gen.visible_generation == 2 def test_zero_generations_are_valid(self): # Arrange # Act gen = FenceGeneration( conversation_id="conv-1", current_generation=0, visible_generation=0, ) # Assert - 代际为 0 是合法的(非负) assert gen.current_generation == 0 assert gen.visible_generation == 0 def test_is_frozen(self): # Arrange gen = FenceGeneration( conversation_id="conv-1", current_generation=0, visible_generation=0 ) # Act / Assert with pytest.raises(dataclasses.FrozenInstanceError): gen.current_generation = 1 # type: ignore[misc] def test_empty_conversation_id_raises_validation_error(self): # Arrange # Act / Assert with pytest.raises(ValidationError) as exc_info: FenceGeneration(conversation_id="", current_generation=0, visible_generation=0) assert exc_info.value.field == "conversation_id" def test_negative_current_generation_raises_validation_error(self): # Arrange # Act / Assert with pytest.raises(ValidationError) as exc_info: FenceGeneration(conversation_id="conv-1", current_generation=-1, visible_generation=0) assert exc_info.value.field == "current_generation" def test_negative_visible_generation_raises_validation_error(self): # Arrange # Act / Assert with pytest.raises(ValidationError) as exc_info: FenceGeneration(conversation_id="conv-1", current_generation=0, visible_generation=-1) assert exc_info.value.field == "visible_generation" @pytest.mark.unit class TestFenceContext: """栅栏上下文 DTO 测试。""" def test_fields_are_assigned(self): # Arrange now = datetime(2026, 1, 1, 12, 0, 0) # Act ctx = FenceContext( conversation_id="conv-1", generation=1, started_at=now, ) # Assert assert ctx.conversation_id == "conv-1" assert ctx.generation == 1 assert ctx.started_at is now assert ctx.ttl_seconds == 120 def test_custom_ttl(self): # Arrange now = datetime(2026, 1, 1, 12, 0, 0) # Act ctx = FenceContext( conversation_id="conv-1", generation=1, started_at=now, ttl_seconds=300, ) # Assert assert ctx.ttl_seconds == 300 def test_zero_generation_is_valid(self): # Arrange now = datetime(2026, 1, 1, 12, 0, 0) # Act ctx = FenceContext(conversation_id="conv-1", generation=0, started_at=now) # Assert assert ctx.generation == 0 def test_is_frozen(self): # Arrange now = datetime(2026, 1, 1, 12, 0, 0) ctx = FenceContext(conversation_id="conv-1", generation=0, started_at=now) # Act / Assert with pytest.raises(dataclasses.FrozenInstanceError): ctx.generation = 1 # type: ignore[misc] def test_empty_conversation_id_raises_validation_error(self): # Arrange now = datetime(2026, 1, 1, 12, 0, 0) # Act / Assert with pytest.raises(ValidationError) as exc_info: FenceContext(conversation_id="", generation=0, started_at=now) assert exc_info.value.field == "conversation_id" def test_negative_generation_raises_validation_error(self): # Arrange now = datetime(2026, 1, 1, 12, 0, 0) # Act / Assert with pytest.raises(ValidationError) as exc_info: FenceContext(conversation_id="conv-1", generation=-1, started_at=now) assert exc_info.value.field == "generation" def test_non_positive_ttl_raises_validation_error(self): # Arrange now = datetime(2026, 1, 1, 12, 0, 0) # Act / Assert with pytest.raises(ValidationError) as exc_info: FenceContext(conversation_id="conv-1", generation=0, started_at=now, ttl_seconds=0) assert exc_info.value.field == "ttl_seconds"