diff --git a/apps/api/src/api/v1/incidents.py b/apps/api/src/api/v1/incidents.py index 1a7f06800..2129edcb7 100644 --- a/apps/api/src/api/v1/incidents.py +++ b/apps/api/src/api/v1/incidents.py @@ -155,6 +155,12 @@ class IncidentTimelineResponse(BaseModel): """, ) async def list_incidents( + project_id: str = Query( + "awoooi", + min_length=1, + max_length=64, + description="租戶 ID;legacy incident surface 預設綁定 awoooi。", + ), generate_missing_decisions: bool = Query( False, description=( @@ -180,7 +186,9 @@ async def list_incidents( try: # 透過 Service 取得活躍事件 - incidents = await incident_service.get_active_incidents() + incidents = await incident_service.get_active_incidents( + project_id=project_id, + ) # 按時間排序 (最新優先) # 2026-03-26 修復: 處理 timezone-aware 與 naive datetime 混合問題 @@ -289,7 +297,15 @@ async def list_incidents( summary="取得單一事件", description="取得特定事件的詳細資訊。", ) -async def get_incident(incident_id: str) -> IncidentResponse: +async def get_incident( + incident_id: str, + project_id: str = Query( + "awoooi", + min_length=1, + max_length=64, + description="租戶 ID;legacy incident surface 預設綁定 awoooi。", + ), +) -> IncidentResponse: """ 取得單一事件 @@ -306,7 +322,10 @@ async def get_incident(incident_id: str) -> IncidentResponse: incident_service = get_incident_service() try: - incident = await incident_service.get_for_readback(incident_id) + incident = await incident_service.get_for_readback( + incident_id, + project_id=project_id, + ) if incident is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, diff --git a/apps/api/src/repositories/incident_repository.py b/apps/api/src/repositories/incident_repository.py index 66a5acebf..67cb5047d 100644 --- a/apps/api/src/repositories/incident_repository.py +++ b/apps/api/src/repositories/incident_repository.py @@ -168,13 +168,13 @@ class IncidentDBRepository(IIncidentRepository): record = result.scalar_one_or_none() return _record_to_incident(record) if record else None - async def get_active(self) -> list[Incident]: + async def get_active(self, *, project_id: str | None = None) -> list[Incident]: """取得所有活躍的 Incident""" active_statuses = [ IncidentStatus.INVESTIGATING, IncidentStatus.MITIGATING, ] - async with get_db_context() as db: + async with get_db_context(project_id) as db: result = await db.execute( select(IncidentRecord) .where(IncidentRecord.status.in_(active_statuses)) diff --git a/apps/api/src/repositories/interfaces.py b/apps/api/src/repositories/interfaces.py index 35362cfec..a647eb9d7 100644 --- a/apps/api/src/repositories/interfaces.py +++ b/apps/api/src/repositories/interfaces.py @@ -125,7 +125,11 @@ class IIncidentRepository(Protocol): """根據 ID 取得 Incident""" ... - async def get_active(self) -> list[Incident]: + async def get_active( + self, + *, + project_id: str | None = None, + ) -> list[Incident]: """取得所有活躍的 Incident""" ... diff --git a/apps/api/src/services/incident_service.py b/apps/api/src/services/incident_service.py index 45a5c54e1..fc9161294 100644 --- a/apps/api/src/services/incident_service.py +++ b/apps/api/src/services/incident_service.py @@ -730,9 +730,17 @@ class IncidentService: ) return "updated" - async def get_for_readback(self, incident_id: str) -> Incident | None: + async def get_for_readback( + self, + incident_id: str, + *, + project_id: str | None = None, + ) -> Incident | None: """Return durable incident truth, with Redis as an availability fallback.""" - incident = await self.get_from_episodic_memory(incident_id) + incident = await self.get_from_episodic_memory( + incident_id, + project_id=project_id, + ) if incident is not None: return incident @@ -742,7 +750,11 @@ class IncidentService: ) return await self.get_from_working_memory(incident_id) - async def get_active_incidents(self) -> list[Incident]: + async def get_active_incidents( + self, + *, + project_id: str | None = None, + ) -> list[Incident]: """ 列出所有活躍的 Incidents。 @@ -755,7 +767,9 @@ class IncidentService: try: from src.repositories.incident_repository import get_incident_repository - incidents = await get_incident_repository().get_active() + incidents = await get_incident_repository().get_active( + project_id=project_id, + ) logger.info( "get_active_incidents_from_episodic_memory", count=len(incidents), diff --git a/apps/api/tests/test_incident_readback_source_truth.py b/apps/api/tests/test_incident_readback_source_truth.py index b16ea0541..efd896b4f 100644 --- a/apps/api/tests/test_incident_readback_source_truth.py +++ b/apps/api/tests/test_incident_readback_source_truth.py @@ -37,11 +37,18 @@ async def test_readback_prefers_durable_resolved_incident_over_stale_cache(monke working_read = AsyncMock(return_value=cached) monkeypatch.setattr(service, "get_from_working_memory", working_read) - result = await service.get_for_readback(durable.incident_id) + result = await service.get_for_readback( + durable.incident_id, + project_id="awoooi", + ) assert result is durable assert result.status is IncidentStatus.RESOLVED working_read.assert_not_awaited() + service.get_from_episodic_memory.assert_awaited_once_with( + durable.incident_id, + project_id="awoooi", + ) @pytest.mark.asyncio @@ -61,7 +68,10 @@ async def test_readback_falls_back_to_working_memory_when_durable_record_unavail AsyncMock(return_value=cached), ) - result = await service.get_for_readback(cached.incident_id) + result = await service.get_for_readback( + cached.incident_id, + project_id="awoooi", + ) assert result is cached @@ -76,11 +86,17 @@ async def test_incident_detail_endpoint_uses_durable_readback(monkeypatch): service = _Service() monkeypatch.setattr(incidents_api, "get_incident_service", lambda: service) - result = await incidents_api.get_incident(durable.incident_id) + result = await incidents_api.get_incident( + durable.incident_id, + project_id="awoooi", + ) assert result.incident_id == durable.incident_id assert result.status == IncidentStatus.RESOLVED.value - service.get_for_readback.assert_awaited_once_with(durable.incident_id) + service.get_for_readback.assert_awaited_once_with( + durable.incident_id, + project_id="awoooi", + ) @pytest.mark.asyncio @@ -98,7 +114,7 @@ async def test_active_incidents_come_from_durable_repository(monkeypatch): lambda: repository, ) - incidents = await service.get_active_incidents() + incidents = await service.get_active_incidents(project_id="awoooi") assert incidents == [durable_active] - repository.get_active.assert_awaited_once_with() + repository.get_active.assert_awaited_once_with(project_id="awoooi") diff --git a/apps/api/tests/test_incidents_list_pure_read.py b/apps/api/tests/test_incidents_list_pure_read.py index ade418d26..b28fbed2c 100644 --- a/apps/api/tests/test_incidents_list_pure_read.py +++ b/apps/api/tests/test_incidents_list_pure_read.py @@ -15,7 +15,12 @@ from src.models.incident import Incident, Severity class _IncidentService: - async def get_active_incidents(self) -> list[Incident]: + async def get_active_incidents( + self, + *, + project_id: str | None = None, + ) -> list[Incident]: + assert project_id == "awoooi" return [ Incident( incident_id="INC-20260506-PURE01", @@ -52,7 +57,10 @@ async def test_list_incidents_does_not_trigger_ai_decision_by_default(monkeypatc monkeypatch.setattr(incidents_api, "get_incident_service", lambda: _IncidentService()) monkeypatch.setattr(incidents_api, "get_decision_manager", lambda: decision_manager) - result = await incidents_api.list_incidents(generate_missing_decisions=False) + result = await incidents_api.list_incidents( + project_id="awoooi", + generate_missing_decisions=False, + ) assert result.count == 1 assert result.incidents[0].incident_id == "INC-20260506-PURE01"