Files
roboco/tests/unit/services/test_telemetry_source.py
T

82 lines
2.9 KiB
Python
Raw Normal View History

"""Self-heal telemetry source — CI conclusion normalized to samples.
The GitHub-CI source watches ONLY RoboCo's own project
(``settings.self_heal_project_slug``): a failing run is a breaching sample, a
passing run a non-breaching one, and no target / no run yields nothing. It is
read-only and never raises.
"""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
import pytest
from roboco.config import settings as cfg
from roboco.services.git import GitService
from roboco.services.telemetry import GitHubCITelemetrySource
def _ci(conclusion: str) -> dict[str, str]:
return {
"conclusion": conclusion,
"head_sha": "abc123",
"run_url": "https://github.com/x/roboco/actions/runs/1",
"run_name": "CI",
"branch": "master",
"completed_at": "2026-06-17T00:00:00Z",
}
@pytest.mark.asyncio
async def test_no_target_yields_no_samples(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(cfg, "self_heal_project_slug", "")
assert await GitHubCITelemetrySource(MagicMock()).fetch() == []
@pytest.mark.asyncio
async def test_failing_ci_is_a_breach(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(cfg, "self_heal_project_slug", "roboco")
monkeypatch.setattr(
GitService, "get_latest_ci_conclusion", AsyncMock(return_value=_ci("failure"))
)
samples = await GitHubCITelemetrySource(MagicMock()).fetch()
assert len(samples) == 1
sample = samples[0]
assert sample.is_breach is True
assert sample.repo_hint == "roboco"
assert sample.signal_name == "ci_conclusion:roboco"
assert "failure" in sample.detail
assert sample.raw_ref.endswith("/runs/1")
@pytest.mark.asyncio
async def test_passing_ci_is_not_a_breach(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(cfg, "self_heal_project_slug", "roboco")
monkeypatch.setattr(
GitService, "get_latest_ci_conclusion", AsyncMock(return_value=_ci("success"))
)
samples = await GitHubCITelemetrySource(MagicMock()).fetch()
assert len(samples) == 1
assert samples[0].is_breach is False
@pytest.mark.asyncio
async def test_cancelled_run_is_not_a_breach(monkeypatch: pytest.MonkeyPatch) -> None:
# A cancelled run is not a failing build — it must not count as a regression.
monkeypatch.setattr(cfg, "self_heal_project_slug", "roboco")
monkeypatch.setattr(
GitService, "get_latest_ci_conclusion", AsyncMock(return_value=_ci("cancelled"))
)
samples = await GitHubCITelemetrySource(MagicMock()).fetch()
assert len(samples) == 1
assert samples[0].is_breach is False
@pytest.mark.asyncio
async def test_no_run_yields_no_samples(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(cfg, "self_heal_project_slug", "roboco")
monkeypatch.setattr(
GitService, "get_latest_ci_conclusion", AsyncMock(return_value=None)
)
assert await GitHubCITelemetrySource(MagicMock()).fetch() == []