Some checks failed
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / tests (push) Successful in 1m39s
AI 技術雷達監控 / ai-technology-watch (push) Successful in 39s
CD Pipeline / build-and-deploy (push) Failing after 6m15s
CD Pipeline / post-deploy-checks (push) Has been skipped
110 lines
3.3 KiB
Python
110 lines
3.3 KiB
Python
from __future__ import annotations
|
|
|
|
from contextlib import asynccontextmanager
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
|
|
class _FakeMappings:
|
|
def __init__(self, rows: list[dict]):
|
|
self._rows = rows
|
|
|
|
def all(self) -> list[dict]:
|
|
return self._rows
|
|
|
|
|
|
class _FakeResult:
|
|
def __init__(self, rows: list[dict]):
|
|
self._rows = rows
|
|
|
|
def mappings(self) -> _FakeMappings:
|
|
return _FakeMappings(self._rows)
|
|
|
|
|
|
def _candidate_incident() -> dict:
|
|
return {
|
|
"incident_id": "INC-20260627-NODE110",
|
|
"project_id": "awoooi",
|
|
"status": "INVESTIGATING",
|
|
"severity": "warning",
|
|
"alertname": "NodeExporterDown",
|
|
"alert_category": "infrastructure",
|
|
"notification_type": "TYPE-3",
|
|
"signals": [
|
|
{
|
|
"alert_name": "NodeExporterDown",
|
|
"labels": {
|
|
"alertname": "NodeExporterDown",
|
|
"instance": "node-exporter-110",
|
|
},
|
|
"annotations": {},
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_backfill_enqueues_catalog_matched_incident(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
from src.jobs import awooop_ansible_candidate_backfill_job as job
|
|
|
|
fake_db = AsyncMock()
|
|
fake_db.execute = AsyncMock(return_value=_FakeResult([_candidate_incident()]))
|
|
|
|
@asynccontextmanager
|
|
async def fake_db_context(project_id: str = "awoooi"):
|
|
assert project_id == "awoooi"
|
|
yield fake_db
|
|
|
|
recorded: list[dict] = []
|
|
|
|
async def fake_recorder(**kwargs):
|
|
recorded.append(kwargs)
|
|
return True
|
|
|
|
async def fake_receipt_backfiller(**kwargs):
|
|
assert kwargs["project_id"] == "awoooi"
|
|
return {"written": 2, "error": None}
|
|
|
|
monkeypatch.setattr(job, "get_db_context", fake_db_context)
|
|
monkeypatch.setattr(job.settings, "ENABLE_AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_WORKER", True)
|
|
|
|
result = await job.enqueue_missing_ansible_candidates_once(
|
|
project_id="awoooi",
|
|
limit=5,
|
|
window_hours=24,
|
|
recorder=fake_recorder,
|
|
receipt_backfiller=fake_receipt_backfiller,
|
|
)
|
|
|
|
assert result["queued"] == 1
|
|
assert result["no_catalog_candidate"] == 0
|
|
assert result["repair_receipts_backfilled"] == 2
|
|
assert recorded[0]["decision_path"] == "repair_candidate_controlled_queue"
|
|
assert recorded[0]["incident"]["incident_id"] == "INC-20260627-NODE110"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_backfill_skips_when_worker_disabled(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
from src.jobs import awooop_ansible_candidate_backfill_job as job
|
|
|
|
monkeypatch.setattr(job.settings, "ENABLE_AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_WORKER", False)
|
|
|
|
result = await job.enqueue_missing_ansible_candidates_once()
|
|
|
|
assert result["skipped"] is True
|
|
assert result["queued"] == 0
|
|
|
|
|
|
def test_backfill_query_excludes_existing_ansible_candidate_rows() -> None:
|
|
from src.jobs import awooop_ansible_candidate_backfill_job as job
|
|
|
|
source = MagicMock(wraps=job._fetch_missing_candidate_incidents)
|
|
query_source = job._fetch_missing_candidate_incidents.__code__.co_consts
|
|
joined = "\n".join(str(item) for item in query_source)
|
|
|
|
assert source is not None
|
|
assert "NOT EXISTS" in joined
|
|
assert "ansible_candidate_matched" in joined
|
|
assert "resolved_at IS NULL" in joined
|