"""실시간 LocalAgreement 단위 테스트 — 버퍼 절단 불변식, 단조 타임스탬프.""" from __future__ import annotations from luke_scribe.pipeline.realtime import LocalAgreement from luke_scribe.results.models import Segment def _segs(texts: list[str], start: float = 0.0) -> list[Segment]: out = [] t = start for i, text in enumerate(texts): out.append(Segment(index=i, start=t, end=t + 1.0, text=text)) t += 1.0 return out class TestAgreement: def test_no_confirmation_until_agreement_count(self): la = LocalAgreement(agreement_count=2) e1 = la.feed(_segs(["첫 번째 문장"]), 2.0) assert e1["confirmed"] == [] # 1회만으로는 미확정 def test_stable_prefix_confirmed(self): la = LocalAgreement(agreement_count=2) la.feed(_segs(["오늘 API 서버"]), 2.0) e2 = la.feed(_segs(["오늘 API 서버에서 vLLM"]), 2.0) confirmed = e2["confirmed"] assert confirmed and confirmed[0].text == "오늘 API 서버" assert e2["truncated"] is True def test_confirmed_immutable(self): """확정 세그먼트는 이후 가설에 의해 절대 변경되지 않는다 (불변식 1).""" la = LocalAgreement(agreement_count=2) la.feed(_segs(["오늘 API"]), 2.0) e2 = la.feed(_segs(["오늘 API 서버"]), 2.0) first_confirmed = [s.text for s in e2["confirmed"]] la.feed(_segs(["오늘 API 서버에서 vLLM 사용"]), 2.0) # 확정분은 그대로 assert [s.text for s in la.state.confirmed_segments][ : len(first_confirmed) ] == first_confirmed def test_no_duplicate_confirmation(self): """이미 확정된 텍스트는 다음 가설에서 재발행되지 않는다 (Eng P18).""" la = LocalAgreement(agreement_count=2) la.feed(_segs(["안정된 발화"]), 2.0) e2 = la.feed(_segs(["안정된 발화", "다음 문장"]), 2.0) assert [s.text for s in e2["confirmed"]] == ["안정된 발화"] # 다음 피드: "안정된 발화"는 재발행 금지, 새 텍스트만 확정 e3 = la.feed(_segs(["안정된 발화", "다음 문장", "셋째 문장"]), 2.0) assert [s.text for s in e3["confirmed"]] == ["다음 문장"] assert [s.text for s in la.state.confirmed_segments] == ["안정된 발화", "다음 문장"] def test_monotonic_timestamps(self): """확정 세그먼트 시간은 단조 증가 (불변식 3).""" la = LocalAgreement(agreement_count=2) la.feed(_segs(["첫 문장", "둘째 문장"]), 4.0) la.feed(_segs(["첫 문장", "둘째 문장", "셋째 문장"]), 4.0) ends = [s.end for s in la.state.confirmed_segments] assert all(b >= a for a, b in zip(ends, ends[1:], strict=False)) def test_buffer_truncation_invariant(self): """확정 방출 후 보류 버퍼는 left_context 이하로 절단 (불변식 2).""" la = LocalAgreement(agreement_count=2, retained_left_context_sec=1.0) la.feed(_segs(["a", "b", "c"]), 3.0) e2 = la.feed(_segs(["a", "b", "c", "d", "e", "f"]), 3.0) assert len(e2["confirmed"]) >= 1 # audio offset은 마지막 확정 end - left_context assert la.state.audio_offset_sec >= 0 def test_flush_confirms_pending(self): la = LocalAgreement(agreement_count=2) la.feed(_segs(["마지막 발화"]), 2.0) flushed = la.flush() assert flushed["confirmed"] assert "마지막 발화" in flushed["new_text"]