diff --git a/apps/api/src/services/approval_db.py b/apps/api/src/services/approval_db.py index 42ea38166..b2e664bf4 100644 --- a/apps/api/src/services/approval_db.py +++ b/apps/api/src/services/approval_db.py @@ -1120,11 +1120,12 @@ class TimelineDBService: risk_level: str | None = None, approval_id: str | None = None, incident_id: str | None = None, + project_id: str | None = None, ) -> dict[str, Any]: """ 新增時間軸事件 """ - async with get_db_context() as db: + async with get_db_context(project_id) as db: event = TimelineEvent( event_type=event_type, status=status, diff --git a/apps/api/src/services/incident_memory.py b/apps/api/src/services/incident_memory.py index 750cd1018..01a226002 100644 --- a/apps/api/src/services/incident_memory.py +++ b/apps/api/src/services/incident_memory.py @@ -97,6 +97,8 @@ def _derive_incident_alert_metadata(incident: Incident) -> dict[str, Any]: async def _add_signal_timeline_event( incident: Incident, metadata: dict[str, Any], + *, + project_id: str, ) -> None: """Best-effort timeline seed for incidents created outside Alertmanager.""" alertname = metadata.get("alertname") @@ -115,6 +117,7 @@ async def _add_signal_timeline_event( actor_role="signal_worker", risk_level=getattr(incident.severity, "value", incident.severity), incident_id=incident.incident_id, + project_id=project_id, ) except Exception as exc: logger.warning( @@ -137,10 +140,13 @@ class IncidentDbAdapter: 注入到 lewooogo-brain 的 DualIncidentMemory """ + def __init__(self, project_id: str = "awoooi") -> None: + self._project_id = project_id + async def load(self, incident_id: str) -> Incident | None: """從 PostgreSQL 載入 Incident""" try: - async with get_db_context() as db: + async with get_db_context(self._project_id) as db: from sqlalchemy import select stmt = select(IncidentRecord).where( IncidentRecord.incident_id == incident_id @@ -161,7 +167,7 @@ class IncidentDbAdapter: metadata = _derive_incident_alert_metadata(incident) created = False try: - async with get_db_context() as db: + async with get_db_context(self._project_id) as db: from sqlalchemy import select stmt = select(IncidentRecord).where( @@ -191,6 +197,7 @@ class IncidentDbAdapter: else: record = IncidentRecord( incident_id=incident.incident_id, + project_id=self._project_id, status=incident.status.value, severity=incident.severity.value, signals=[ @@ -222,7 +229,11 @@ class IncidentDbAdapter: created = True if created: - await _add_signal_timeline_event(incident, metadata) + await _add_signal_timeline_event( + incident, + metadata, + project_id=self._project_id, + ) logger.debug("db_adapter_save_success", incident_id=incident.incident_id) return True diff --git a/apps/api/src/services/signal_observation_service.py b/apps/api/src/services/signal_observation_service.py index 98e3cd8a4..12359f1c3 100644 --- a/apps/api/src/services/signal_observation_service.py +++ b/apps/api/src/services/signal_observation_service.py @@ -436,6 +436,7 @@ async def _add_observation_timeline( actor_role="signal_worker", risk_level=_severity_value(getattr(incident, "severity", None)), incident_id=_incident_id(incident), + project_id=PROJECT_ID, ) except Exception as exc: logger.warning( @@ -477,7 +478,10 @@ async def _ensure_signal_observation_mcp_runtime() -> None: try: from src.plugins.mcp.providers import register_all_providers from src.plugins.mcp.registry import get_provider_registry - from src.services.mcp_tool_registry import get_mcp_tool_registry, init_mcp_tool_registry + from src.services.mcp_tool_registry import ( + get_mcp_tool_registry, + init_mcp_tool_registry, + ) provider_registry = get_provider_registry() if len(provider_registry) == 0: diff --git a/apps/api/tests/test_incident_memory_adapter.py b/apps/api/tests/test_incident_memory_adapter.py index 0ce7a60e8..5c21dd5d7 100644 --- a/apps/api/tests/test_incident_memory_adapter.py +++ b/apps/api/tests/test_incident_memory_adapter.py @@ -1,9 +1,16 @@ from __future__ import annotations +from contextlib import asynccontextmanager from types import SimpleNamespace +import pytest + from src.models.incident import Severity -from src.services.incident_memory import _derive_incident_alert_metadata +from src.services import incident_memory as incident_memory_module +from src.services.incident_memory import ( + IncidentDbAdapter, + _derive_incident_alert_metadata, +) def test_signal_worker_incident_metadata_uses_signal_alert_name() -> None: @@ -30,3 +37,31 @@ def test_signal_worker_incident_metadata_uses_signal_alert_name() -> None: assert metadata["description"] == "29 ERROR log entries in last 5min" assert metadata["actor"] == "journal" + +@pytest.mark.asyncio +async def test_incident_db_adapter_uses_explicit_project_context(monkeypatch) -> None: + project_ids: list[str | None] = [] + + class FakeResult: + def scalar_one_or_none(self): + return None + + class FakeDb: + async def execute(self, _statement): + return FakeResult() + + @asynccontextmanager + async def fake_get_db_context(project_id=None): + project_ids.append(project_id) + yield FakeDb() + + monkeypatch.setattr( + incident_memory_module, + "get_db_context", + fake_get_db_context, + ) + + result = await IncidentDbAdapter(project_id="awoooi").load("INC-NOT-FOUND") + + assert result is None + assert project_ids == ["awoooi"] diff --git a/apps/api/tests/test_signal_observation_service.py b/apps/api/tests/test_signal_observation_service.py index 9a34281d7..8f3401745 100644 --- a/apps/api/tests/test_signal_observation_service.py +++ b/apps/api/tests/test_signal_observation_service.py @@ -2,7 +2,10 @@ from __future__ import annotations from types import SimpleNamespace +import pytest + from src.models.incident import Severity +from src.services import signal_observation_service as signal_observation_module from src.services.signal_observation_service import ( build_signal_worker_provider_event_id, enrich_incident_for_signal_observation, @@ -72,3 +75,26 @@ def test_signal_worker_event_content_is_operator_readable() -> None: assert "Incident: INC-20260518-TEST01" in content assert "Alert: HostErrorLogFlood" in content assert "Telegram: not sent" in content + + +@pytest.mark.asyncio +async def test_signal_observation_timeline_uses_explicit_project_context(monkeypatch) -> None: + calls: list[dict] = [] + + class FakeTimelineService: + async def add_event(self, **kwargs): + calls.append(kwargs) + + monkeypatch.setattr( + "src.services.approval_db.get_timeline_service", + lambda: FakeTimelineService(), + ) + + await signal_observation_module._add_observation_timeline( + incident=_host_incident(), + signal_data={"alert_name": "HostErrorLogFlood"}, + labels={"target": "ollama"}, + raw_evidence_created=True, + ) + + assert calls[0]["project_id"] == "awoooi"