""" test_incident_service_resolve_idempotency ========================================== 驗證 `IncidentService.resolve_incident` 對已經 RESOLVED 的 incident 必須 idempotent: - 直接 return existing incident - 不呼叫 save_to_working_memory(避免重複 Redis write) - 不呼叫 incident_repository.update_status(避免重複 DB write) - 不觸發 postmortem / KB extract / KM convert / disposition 副作用 對應 critic 必修 #2 — 沒這個單測,未來有人挪 guard 位置會悄悄破功, 重新放大「resolve_incident 重複觸發 postmortem 洗版」的舊風險。 """ from types import SimpleNamespace from unittest.mock import AsyncMock import pytest from src.models.incident import IncidentStatus from src.services.incident_service import IncidentService @pytest.mark.asyncio async def test_resolve_incident_skips_when_already_resolved(monkeypatch): """RESOLVED 的 incident 重複 resolve 應 idempotent。""" fake_incident = SimpleNamespace( incident_id="INC-IDEMPO-001", status=IncidentStatus.RESOLVED, ) svc = IncidentService() # Mock 入口讀取 → 回 RESOLVED incident monkeypatch.setattr( svc, "get_from_working_memory", AsyncMock(return_value=fake_incident) ) # Mock 後續所有副作用 → 用 AsyncMock 監看是否被呼叫 save_mock = AsyncMock(return_value=True) monkeypatch.setattr(svc, "save_to_working_memory", save_mock) result = await svc.resolve_incident("INC-IDEMPO-001") # 應 return existing incident assert result is fake_incident # 副作用一律不能觸發(guard 必須早於 line 1117 的 status mutation) save_mock.assert_not_called() @pytest.mark.asyncio async def test_resolve_incident_returns_none_when_not_found(monkeypatch): """incident 不存在時 return None。確保 guard 不影響 not-found 路徑。""" svc = IncidentService() monkeypatch.setattr( svc, "get_from_working_memory", AsyncMock(return_value=None) ) save_mock = AsyncMock(return_value=True) monkeypatch.setattr(svc, "save_to_working_memory", save_mock) result = await svc.resolve_incident("INC-NOT-EXIST") assert result is None save_mock.assert_not_called()