"""R3: 섹터 캐시 건강도·자동 갱신 회귀 테스트 (네트워크는 예외 발생형 목).""" import asyncio import json import logging from datetime import datetime, timedelta, timezone import pytest from app.services import sector_mapper as sm @pytest.fixture(autouse=True) def _iso(monkeypatch, tmp_path): monkeypatch.setenv("AUTOSTOCK_SECTOR_CACHE_PATH", str(tmp_path / "sector_map.json")) monkeypatch.setattr(sm, "_cache_payload", {}) monkeypatch.setattr(sm, "_cache_sectors", {}) monkeypatch.setattr(sm, "_cache_industries", {}) monkeypatch.setattr(sm, "_cache_mtime", -1.0) def _boom(*a, **k): raise AssertionError("network blocked") monkeypatch.setattr(sm, "_fetch_json", _boom) def _write(tmp_path, days_old): ts = (datetime.now(timezone.utc) - timedelta(days=days_old)).isoformat() (tmp_path / "sector_map.json").write_text( json.dumps({"updated_at": ts, "sectors": {"000001": "finance"}}), encoding="utf-8") def test_missing_cache_is_unhealthy_and_logs_error(caplog): with caplog.at_level(logging.ERROR): h = sm.log_cache_health_v2() assert not h["ok"] and h["reason"] == "missing" assert any("섹터 캐시" in r.message for r in caplog.records) def test_fresh_cache_ok(tmp_path): _write(tmp_path, 1) h = sm.get_cache_health_v2() assert h["ok"] and not h["stale"] and h["stocks"] == 1 def test_stale_cache_flagged(tmp_path): _write(tmp_path, sm.SECTOR_CACHE_MAX_AGE_DAYS + 2) h = sm.get_cache_health_v2() assert not h["ok"] and h["reason"] == "stale" def test_next_delay_skips_weekend(): kst = timezone(timedelta(hours=9)) fri_after = datetime(2026, 10, 9, 9, 0, tzinfo=kst) # 금요일 09:00 d = sm._next_refresh_delay_v2(fri_after) target = fri_after + timedelta(seconds=d) assert target.weekday() == 0 and (target.hour, target.minute) == (8, 30) def test_scheduler_refreshes_immediately_when_missing(monkeypatch): calls = [] async def fake_refresh(**kw): calls.append(1) raise asyncio.CancelledError # 1회 호출 후 루프 종료 monkeypatch.setattr(sm, "refresh_sector_map", fake_refresh) with pytest.raises(asyncio.CancelledError): asyncio.run(sm.sector_map_refresh_scheduler()) assert calls == [1]