"""PR-Worker-Pool-Registry-1C — /internal/worker/jobs/recap + recap context. 3 항목 (단독 실행 기준 PASS; 같은 파일 sequential 한계 = test fixture isolation follow-up): 1. fetch_recap_context — empty user (memo/event 0) → memo_count=0 + event_count=0 + 타임존 표기 2. /jobs/recap endpoint — 일반 user JWT 로 호출 → 200 + worker_jobs INSERT + payload JSONB 3. payload size guard — fetch_recap_context monkeypatch 로 거대 context 반환 → 413 """ 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 sqlalchemy import text from sqlalchemy.ext.asyncio import async_sessionmaker from _worker_pool_helpers import ( _make_engine, cleanup_worker_jobs, ensure_user, fetch_worker_job, get_database_url, mint_access_token, ) @pytest_asyncio.fixture async def env_setup(monkeypatch): monkeypatch.setenv("LAPTOP_WORKER_BOT_USERNAME", "laptop-worker-bot") @pytest.mark.asyncio async def test_fetch_recap_context_shape(env_setup): """fetch_recap_context — 시그니처 + 빈 user 시 0/0 + KST 타임존 표기.""" from services.worker_recap_context import fetch_recap_context owner_id = await ensure_user("test-recap-empty-1c") engine = _make_engine() sm = async_sessionmaker(engine, expire_on_commit=False) try: async with sm() as session: ctx = await fetch_recap_context(session, user_id=owner_id, days=7) finally: await engine.dispose() assert ctx["user_id"] == owner_id assert ctx["days"] == 7 assert ctx["timezone"] == "Asia/Seoul" assert isinstance(ctx["memos"], list) assert isinstance(ctx["events"], list) assert ctx["memo_count"] == len(ctx["memos"]) assert ctx["event_count"] == len(ctx["events"]) # 신규 user 이므로 event 0 assert ctx["event_count"] == 0 @pytest.mark.asyncio async def test_recap_endpoint_creates_worker_job(env_setup): """/jobs/recap 호출 → 200 + worker_jobs INSERT.""" from main import app owner_id = await ensure_user("test-recap-endpoint-1c") token = mint_access_token("test-recap-endpoint-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 "job_id" in js assert js["memo_count"] >= 0 assert js["event_count"] >= 0 assert js["payload_bytes"] > 0 assert "payload_compacted" in js assert "omitted_memos" in js # DB verify job = await fetch_worker_job(js["job_id"]) assert job is not None # payload 는 JSONB dict 로 저장됨 finally: await cleanup_worker_jobs("recap") @pytest.mark.asyncio async def test_recap_payload_413_when_oversize(env_setup, monkeypatch): """payload 1MB 초과 시 413 (사용자 결정 2026-05-19 cap 1MB).""" from api import internal_worker as iw_mod from main import app owner_id = await ensure_user("test-recap-413-1c") token = mint_access_token("test-recap-413-1c") async def fake_fetch(session, user_id, days=7): 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", # ~1.2MB raw payload (compaction 후에도 cap 초과 가정) "memos": [{"id": i, "title": "x" * 6000} for i in range(200)], "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) 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 "bytes" in r.json()["detail"] assert "after compaction" in r.json()["detail"]