"""일정 생성은 **언제** 걸리나. ★ 에디터 요청 안에서 생성하지 않는다. 두 기간 합쳐 50~100초다 — 지역 이야기가 잡으로 도는 이유와 같다(local_content_service._ensure_region_stories L444). 캔버스는 잡을 넣고, 이번 응답에는 안 실린다. **다음에 열 때** 보인다. ★ 가드가 "이야기가 다 찼나" 만 보면 안 된다. 이미 이야기가 찬 업장은 일정을 영영 못 받는다 — story_service 가 has_stories 하나로 판단하다 daily 를 못 받던 것과 같은 함정이다. """ import uuid from services import story_service from services.llm import perplexity from services.local_content_service import LocalContentService class _Place: """`_ensure_region_stories` 와 `run_local_sync` 가 getattr 로만 읽는 최소 객체.""" def __init__(self): self.place_id = uuid.uuid4() self.region_code = "52군산시" self.name = "스테이,머뭄" self.road_address = "전북특별자치도 군산시 절골길 18" self.address = self.road_address class _SyncResult: """`sync_place_by_id` 가 돌려주는 모양(ResSyncPlace)의 최소 대역.""" class result: success = True msg = "" festivals = attractions = restaurants = 0 async def test_local_sync_job_generates_itineraries(monkeypatch, db_engine): """검증: LOCAL_SYNC 잡이 일정 생성을 부른다. 기대결과: place_id 로 ensure_generated_by_id 를 부르고 결과를 잡 결과에 싣는다. ★ 이야기 블록은 키가 없으면(APP_ENV=test 는 .env 를 안 읽는다) 조기 반환한다 — 그래서 일정 블록이 **이야기보다 앞**에 있어야 이 테스트가 통과한다. 그게 의도다.""" called: list = [] async def fake_ensure(place_id): called.append(place_id) return {"place_id": str(place_id), "counts": {"1박 2일": 5, "2박 3일": 5}, "notes": []} async def fake_sync(self, place_id): return _SyncResult() monkeypatch.setattr("services.itinerary_llm_service.ensure_generated_by_id", fake_ensure) monkeypatch.setattr(LocalContentService, "sync_place_by_id", fake_sync) pid = uuid.uuid4() out = await story_service.run_local_sync({ "payload": {"place_id": str(pid), "region_code": "52군산시", "region_label": "전북특별자치도 군산시"}, }) assert called == [pid] assert out["itineraries"]["counts"] == {"1박 2일": 5, "2박 3일": 5} async def test_canvas_enqueues_job_when_only_itineraries_are_missing(monkeypatch, db_engine): """검증: 이야기는 다 찼지만 일정이 없는 업장이 캔버스를 연다. 기대결과: 잡을 넣는다 — 가드가 일정 누락도 보기 때문이다. ★ 이 테스트가 없으면 "이야기가 찬 업장은 일정을 영영 못 받는" 함정이 조용히 살아난다.""" enqueued: list = [] async def fake_enqueue(place): enqueued.append(place.place_id) return "job-1" async def no_missing_kinds(region_code): return [] async def two_missing_durations(place_id): return ["1박 2일", "2박 3일"] monkeypatch.setattr(perplexity, "is_configured", lambda: True) monkeypatch.setattr(story_service, "missing_kinds", no_missing_kinds) monkeypatch.setattr(story_service, "enqueue_region_job", fake_enqueue) monkeypatch.setattr("services.itinerary_llm_service.missing_durations", two_missing_durations) place = _Place() await LocalContentService()._ensure_region_stories(place) assert enqueued == [place.place_id] async def test_canvas_does_not_enqueue_when_everything_is_filled(monkeypatch, db_engine): """검증: 이야기도 일정도 다 있는 업장. 기대결과: 잡을 넣지 않는다 — 유료 호출을 다시 걸지 않는다(요금 가드).""" enqueued: list = [] async def fake_enqueue(place): enqueued.append(place.place_id) return "job-1" async def no_missing_kinds(region_code): return [] async def no_missing_durations(place_id): return [] monkeypatch.setattr(perplexity, "is_configured", lambda: True) monkeypatch.setattr(story_service, "missing_kinds", no_missing_kinds) monkeypatch.setattr(story_service, "enqueue_region_job", fake_enqueue) monkeypatch.setattr("services.itinerary_llm_service.missing_durations", no_missing_durations) await LocalContentService()._ensure_region_stories(_Place()) assert enqueued == []