"""PR-Worker-Pool-Registry-1C compaction — deterministic top-N + aggregate. 3 항목 (사용자 결정 2026-05-19 spec): 1. 700+ memo → /jobs/recap 200 + payload_size < 1MB + payload_compacted=true + omitted > 0 2. 작은 fixture (10 memos) → compacted=false + omitted=0 3. 거대 title (1MB 초과 시뮬레이션) → 413 유지 monkeypatch 로 fetch_recap_context 또는 _memo_top_n 조정. """ from __future__ import annotations import json import os import sys import pytest import pytest_asyncio sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "app")) from httpx import ASGITransport, AsyncClient from _worker_pool_helpers import ( cleanup_worker_jobs, ensure_user, fetch_worker_job, mint_access_token, ) @pytest_asyncio.fixture async def env_setup(monkeypatch): monkeypatch.setenv("LAPTOP_WORKER_BOT_USERNAME", "laptop-worker-bot") def _make_fake_context(total_memos: int, top_n: int = 200) -> dict: """compaction 모사 — _compact_memo 형식 (5 필드) 의 row.""" from datetime import datetime, timezone from zoneinfo import ZoneInfo kst = ZoneInfo("Asia/Seoul") now = datetime.now(timezone.utc) kept_count = min(total_memos, top_n) omitted = max(0, total_memos - top_n) memos = [ { "id": i, "title": f"memo {i}", "ai_tldr": f"tldr {i}", "ai_event_kind": "note", "created_at": now.astimezone(kst).isoformat(), } for i in range(kept_count) ] return { "user_id": 1, "days": 7, "period_start": now.astimezone(kst).isoformat(), "period_end": now.astimezone(kst).isoformat(), "timezone": "Asia/Seoul", "memos": memos, "events": [], "memo_count": total_memos, "event_count": 0, "summary_stats": { "total_memos": total_memos, "memos_kept": kept_count, "omitted_memos": omitted, "top_n": top_n, "memos_by_day": {"2026-05-19": omitted} if omitted else {}, "memos_by_kind": {"note": omitted} if omitted else {}, }, "payload_compacted": omitted > 0, } @pytest.mark.asyncio async def test_compaction_kicks_in_at_top_n(env_setup, monkeypatch): """700 memo → 200 kept + 500 omitted + payload_compacted=true + payload < 1MB.""" from api import internal_worker as iw_mod from main import app fake = _make_fake_context(total_memos=700, top_n=200) async def fake_fetch(session, user_id, days=7): # user_id 채워 줌 fake["user_id"] = user_id return fake monkeypatch.setattr(iw_mod, "fetch_recap_context", fake_fetch) await ensure_user("test-recap-compact-1c") token = mint_access_token("test-recap-compact-1c") try: async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as c: r = await c.post( "/internal/worker/jobs/recap", json={"days": 7}, headers={"Authorization": f"Bearer {token}"}, ) assert r.status_code == 200, r.text js = r.json() assert js["memo_count"] == 700 assert js["payload_compacted"] is True assert js["omitted_memos"] == 500 assert js["payload_bytes"] < 1_000_000 # DB verify — payload 안에 compacted memos + summary_stats job = await fetch_worker_job(js["job_id"]) assert job is not None finally: await cleanup_worker_jobs("recap") @pytest.mark.asyncio async def test_no_compaction_when_under_top_n(env_setup, monkeypatch): """10 memo → kept 10 + omitted 0 + payload_compacted=false.""" from api import internal_worker as iw_mod from main import app fake = _make_fake_context(total_memos=10, top_n=200) async def fake_fetch(session, user_id, days=7): fake["user_id"] = user_id return fake monkeypatch.setattr(iw_mod, "fetch_recap_context", fake_fetch) await ensure_user("test-recap-uncompact-1c") token = mint_access_token("test-recap-uncompact-1c") try: async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as c: r = await c.post( "/internal/worker/jobs/recap", json={"days": 7}, headers={"Authorization": f"Bearer {token}"}, ) assert r.status_code == 200, r.text js = r.json() assert js["memo_count"] == 10 assert js["payload_compacted"] is False assert js["omitted_memos"] == 0 finally: await cleanup_worker_jobs("recap") @pytest.mark.asyncio async def test_413_when_compacted_still_over_cap(env_setup, monkeypatch): """비정상 큰 title (compaction 후에도 1MB 초과) → 413 유지.""" from api import internal_worker as iw_mod from main import app async def fake_fetch(session, user_id, days=7): huge_title = "x" * 6000 # per memo return { "user_id": user_id, "days": days, "period_start": "2026-05-12T00:00:00+09:00", "period_end": "2026-05-19T00:00:00+09:00", "timezone": "Asia/Seoul", "memos": [ { "id": i, "title": huge_title, "ai_tldr": None, "ai_event_kind": None, "created_at": "2026-05-19T00:00:00+09:00", } for i in range(200) ], # ~1.2MB "events": [], "memo_count": 200, "event_count": 0, "summary_stats": { "total_memos": 200, "memos_kept": 200, "omitted_memos": 0, "top_n": 200, "memos_by_day": {}, "memos_by_kind": {}, }, "payload_compacted": False, } monkeypatch.setattr(iw_mod, "fetch_recap_context", fake_fetch) await ensure_user("test-recap-413-large-1c") token = mint_access_token("test-recap-413-large-1c") async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as c: r = await c.post( "/internal/worker/jobs/recap", json={"days": 7}, headers={"Authorization": f"Bearer {token}"}, ) assert r.status_code == 413, r.text assert "after compaction" in r.json()["detail"]