mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
82 lines
2.9 KiB
Python
82 lines
2.9 KiB
Python
"""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() == []
|