From a020c52a30b39b1934af45d3f51b3332996153db Mon Sep 17 00:00:00 2001 From: gahusb Date: Sat, 11 Jul 2026 15:43:55 +0900 Subject: [PATCH] =?UTF-8?q?fix(agent-office):=20node=5Fmonitor=20=5Fbeat?= =?UTF-8?q?=5Fage=EA=B0=80=20epoch=20ts=20=ED=97=88=EC=9A=A9=20=E2=86=92?= =?UTF-8?q?=20naver-fetch=20=EC=98=81=EA=B5=AC=20down=20=EC=98=A4=ED=83=90?= =?UTF-8?q?=20=ED=95=B4=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 매물알림 스펙 §4.4는 fetcher heartbeat ts를 epoch 정수로 정의했는데 _beat_age가 ISO 문자열만 파싱해 int.replace AttributeError → age=None → naver-fetch가 heartbeat 정상 발신 중에도 /nodes에서 영구 alive=false. isinstance(int|float) 분기로 epoch·ISO 둘 다 지원 (신규 테스트 2건). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019jBG2fz48w1bMfaQo8YC6M --- agent-office/app/node_monitor.py | 8 ++++++-- agent-office/tests/test_node_monitor.py | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/agent-office/app/node_monitor.py b/agent-office/app/node_monitor.py index bc53673..8bbd9a6 100644 --- a/agent-office/app/node_monitor.py +++ b/agent-office/app/node_monitor.py @@ -28,9 +28,13 @@ def _get_redis(): return _redis -def _beat_age(ts_str, now): +def _beat_age(ts, now): + """ts는 ISO-8601 문자열 또는 epoch 숫자(매물알림 스펙 §4.4) 둘 다 허용.""" try: - beat = dt.datetime.fromisoformat(ts_str.replace("Z", "+00:00")) + if isinstance(ts, (int, float)): + beat = dt.datetime.fromtimestamp(ts, tz=dt.timezone.utc) + else: + beat = dt.datetime.fromisoformat(ts.replace("Z", "+00:00")) return max(0, int((now - beat).total_seconds())) except Exception: return None diff --git a/agent-office/tests/test_node_monitor.py b/agent-office/tests/test_node_monitor.py index cf7115c..509fea7 100644 --- a/agent-office/tests/test_node_monitor.py +++ b/agent-office/tests/test_node_monitor.py @@ -224,3 +224,25 @@ async def test_naver_fetch_http_pull_link(): st = await node_monitor.collect_status(redis=r) link = next(l for l in st["links"] if l["from"] == "naver-fetch") assert link["type"] == "http-pull" and link["to"] == "nas-realestate" and link["status"] == "healthy" + + +# ── ts epoch 포맷 지원 (매물알림 스펙 §4.4: naver-fetch는 ts를 epoch 정수로 발신) ── + +@pytest.mark.asyncio +async def test_epoch_ts_fresh_is_alive(): + """ts가 epoch 정수(스펙 §4.4)여도 신선하면 alive=True.""" + epoch_now = int(dt.datetime.now(dt.timezone.utc).timestamp()) + r = FakeRedis(kv={"worker:naver-fetch:heartbeat": _hb("naver-fetch", "fetcher", "idle", ts=epoch_now)}) + st = await node_monitor.collect_status(redis=r) + nf = next(w for w in st["workers"] if w["name"] == "naver-fetch") + assert nf["alive"] is True and nf["last_beat_age_s"] is not None + + +@pytest.mark.asyncio +async def test_epoch_ts_stale_is_dead(): + """epoch ts가 90s 초과 과거면 staleness 판정으로 alive=False.""" + epoch_old = int(dt.datetime.now(dt.timezone.utc).timestamp()) - 300 + r = FakeRedis(kv={"worker:naver-fetch:heartbeat": _hb("naver-fetch", "fetcher", "idle", ts=epoch_old)}) + st = await node_monitor.collect_status(redis=r) + nf = next(w for w in st["workers"] if w["name"] == "naver-fetch") + assert nf["alive"] is False and nf["last_beat_age_s"] >= 300