c2077b3108
plan ds-presegment-mapreduce-2. TRIGGER(25K tok) 이하 = 기존 단일콜 byte-불변 무회귀. 초과 시 3-way over% 게이트: auto=유닛별 map(26B)→reduce(26B, p3c_deep_summary_reduce 변형) → ai_detail_summary 동일 기록(불일치=reduce+map 합본 dedup) / hybrid·whole= HOLD(payload.presegment.awaiting_split + StageDeferred 24h, 맥미니 미전송 — 알람· 클로드 유인 분할은 PR3). - 유닛 단위 멱등 재개: 성공 유닛 즉시 payload.map_results commit — 502/defer/재시작 후 완료 유닛 skip, 실패 유닛만 raise→기존 attempts/백오프 재사용 - 모든 LLM 콜 캡(12K tok) 이하 — map=greedy-pack 보장, reduce=build_reduce_units_block 비례 절단 보장, est_tokens 로그로 단정 가능 - 콜 사이 gate 해제 → 짧은 인터랙티브 요청 interleave (허브 굶김 해소 본체) - fix: summarize_units 의 `from app.services...` 절대 import — 컨테이너(빌드 컨텍스트 ./app)에 app 패키지가 없어 배선 시 ModuleNotFoundError 나는 PR1 잠복 버그 → 상대 import 로 수정 (컨테이너/repo-root 테스트 양쪽 동작) - tests: 헬퍼 6 + worker seam 5 (map-reduce e2e·재개·유닛실패·drain 보류·HOLD) — PR1 15 포함 26 passed, 인접 policy/hier_decomp/fair_share 123 passed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
81 lines
2.9 KiB
Python
81 lines
2.9 KiB
Python
"""summarize_units PR2 헬퍼 단위테스트 — map/reduce 프롬프트 조립 순수함수.
|
||
|
||
핵심 불변식:
|
||
- render_map_slice: 유닛 위치(1-based)/섹션 라벨 + 본문 그대로 (손실 0).
|
||
- build_reduce_units_block: 어떤 입력에도 반환 블록 est_tokens <= budget (캡 초과 0
|
||
검증 게이트의 reduce 측). 절단은 detail 만 — 라벨/TLDR/불일치/순서 보존.
|
||
|
||
pytest + 단독 실행 양쪽 지원:
|
||
PYTHONPATH=. pytest tests/summarize_units/ -q
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from app.services.summarize_units import (
|
||
SummarizeUnit,
|
||
build_reduce_units_block,
|
||
estimate_tokens,
|
||
render_map_slice,
|
||
)
|
||
|
||
|
||
def _result(idx: int, detail: str, *, tldr: str = "요약", inc: list | None = None) -> dict:
|
||
return {
|
||
"index": idx,
|
||
"titles": [f"섹션{idx}"],
|
||
"tldr": tldr,
|
||
"detail": detail,
|
||
"inconsistencies": inc or [],
|
||
}
|
||
|
||
|
||
# ---------- render_map_slice ----------
|
||
|
||
def test_render_map_slice_label_and_body():
|
||
unit = SummarizeUnit(index=2, section_titles=["개요", None, "본론"], text="본문입니다")
|
||
out = render_map_slice(unit, total_units=5)
|
||
assert out.startswith("[유닛 3/5 — 섹션: 개요 · 본론]\n")
|
||
assert out.endswith("본문입니다")
|
||
|
||
|
||
def test_render_map_slice_untitled():
|
||
unit = SummarizeUnit(index=0, section_titles=[None], text="x")
|
||
assert "(무제 구간)" in render_map_slice(unit, total_units=1)
|
||
|
||
|
||
# ---------- build_reduce_units_block ----------
|
||
|
||
def test_reduce_block_within_budget_untouched():
|
||
results = [_result(i, "가" * 100) for i in range(3)]
|
||
block, truncated = build_reduce_units_block(results, budget_tokens=11_000)
|
||
assert not truncated
|
||
# 순서/라벨/TLDR 보존
|
||
assert block.index("[유닛 1/3") < block.index("[유닛 2/3") < block.index("[유닛 3/3")
|
||
assert "TLDR: 요약" in block
|
||
assert "가" * 100 in block
|
||
|
||
|
||
def test_reduce_block_truncates_to_budget():
|
||
# 유닛 8개 × 한글 detail 5,000자 ≈ 21K tok — budget 5,000 으로 절단 강제
|
||
results = [_result(i, "가" * 5_000) for i in range(8)]
|
||
block, truncated = build_reduce_units_block(results, budget_tokens=5_000)
|
||
assert truncated
|
||
assert estimate_tokens(block) <= 5_000
|
||
# 라벨(유닛 순서)은 절단 후에도 보존
|
||
assert "[유닛 1/8" in block
|
||
|
||
|
||
def test_reduce_block_hard_cut_floor():
|
||
# min_detail_chars floor 에 막혀 비례 절단으로 불충분한 극단 케이스 — 하드 컷 발동
|
||
results = [_result(i, "가" * 300) for i in range(50)]
|
||
block, truncated = build_reduce_units_block(results, budget_tokens=500)
|
||
assert truncated
|
||
assert estimate_tokens(block) <= 500
|
||
|
||
|
||
def test_reduce_block_preserves_inconsistencies():
|
||
results = [
|
||
_result(0, "가" * 50, inc=[{"kind": "version_drift", "desc": "개정판 차이"}]),
|
||
]
|
||
block, _ = build_reduce_units_block(results, budget_tokens=10_000)
|
||
assert "불일치(version_drift): 개정판 차이" in block
|