Backend / Tests exigeant une base (push) Failing after 34s
Backend / Lint, typage et tests (push) Successful in 1m24s
Backend / Audit des dépendances (push) Successful in 57s
SonarQube / build-back (push) Successful in 1m5s
SonarQube / build-front (push) Successful in 9m39s
SonarQube / test-back (push) Failing after 51s
SonarQube / test-front (push) Failing after 5m6s
SonarQube / SonarQube (push) Skipped
`create_missing()` construisait un seul `INSERT ... VALUES` pour la totalite des propositions. Avec quatre colonnes par ligne et le plafond asyncpg de 32 767 parametres, la route echouait au-dela de 8 191 recommandations par appel, cas devenu realiste maintenant que la detection interne (#104) alimente `alert` en continu. L'insertion passe par des lots de `TAILLE_DE_LOT` lignes, sur le patron de `app/etl/historical_import.py`. L'ADR 0006, `20-backend.md` et la description de la PR annoncaient qu'aucune source n'alimentait `alert` et que #104 n'etait pas commencee. #104 est livree sur `dev` depuis la #113 : les phrases sont corrigees plutot que laissees a vieillir dans un ADR.
143 lines
4.4 KiB
Python
143 lines
4.4 KiB
Python
import uuid
|
|
from datetime import UTC, datetime
|
|
|
|
import pytest
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.energy import Alert, Recommendation, Site
|
|
from app.repositories import recommendation as module_recommendation
|
|
from app.repositories.recommendation import NouvelleRecommandation, RecommendationRepository
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
MOMENT = datetime(2024, 1, 1, tzinfo=UTC)
|
|
|
|
|
|
async def creer_site(session: AsyncSession) -> str:
|
|
site_id = f"TEST-{uuid.uuid4()}"
|
|
session.add(Site(site_id=site_id, site_name="Site de test", site_type="office"))
|
|
await session.flush()
|
|
return site_id
|
|
|
|
|
|
async def creer_alerte(session: AsyncSession) -> int:
|
|
site_id = await creer_site(session)
|
|
alerte = Alert(
|
|
source_alert_id=str(uuid.uuid4()),
|
|
site_id=site_id,
|
|
source="api_mock",
|
|
timestamp=MOMENT,
|
|
type="spike",
|
|
severity="high",
|
|
message="Test",
|
|
raw_data={},
|
|
)
|
|
session.add(alerte)
|
|
await session.flush()
|
|
return alerte.alert_id
|
|
|
|
|
|
async def creer(session: AsyncSession, **overrides: object) -> Recommendation:
|
|
recommendation = Recommendation(
|
|
alert_id=overrides.get("alert_id") or await creer_alerte(session),
|
|
action=overrides.get("action", "Vérifier la consommation"),
|
|
explanation=overrides.get("explanation", "Pic détecté"),
|
|
rule_reference=overrides.get("rule_reference", f"spike-{uuid.uuid4().hex[:8]}"),
|
|
)
|
|
session.add(recommendation)
|
|
await session.flush()
|
|
return recommendation
|
|
|
|
|
|
async def test_get_by_id_returns_the_matching_recommendation(session: AsyncSession) -> None:
|
|
depot = RecommendationRepository(session)
|
|
cree = await creer(session)
|
|
|
|
trouve = await depot.get_by_id(cree.recommendation_id)
|
|
action = trouve.action if trouve else None
|
|
await session.rollback()
|
|
|
|
assert action == "Vérifier la consommation"
|
|
|
|
|
|
async def test_get_by_id_returns_nothing_for_an_unknown_identifier(
|
|
session: AsyncSession,
|
|
) -> None:
|
|
trouve = await RecommendationRepository(session).get_by_id(0)
|
|
|
|
assert trouve is None
|
|
|
|
|
|
async def test_list_all_returns_the_recommendations_sorted_by_identifier(
|
|
session: AsyncSession,
|
|
) -> None:
|
|
depot = RecommendationRepository(session)
|
|
premiere = await creer(session)
|
|
seconde = await creer(session)
|
|
|
|
recommendations = await depot.list_all()
|
|
identifiants = [
|
|
r.recommendation_id
|
|
for r in recommendations
|
|
if r.recommendation_id in (premiere.recommendation_id, seconde.recommendation_id)
|
|
]
|
|
await session.rollback()
|
|
|
|
assert identifiants == sorted(identifiants)
|
|
|
|
|
|
def nouvelle(alert_id: int, reference: str = "spike-delestage-v1") -> NouvelleRecommandation:
|
|
return NouvelleRecommandation(
|
|
alert_id=alert_id,
|
|
action="Délester les équipements non prioritaires",
|
|
explanation="Pic de consommation signalé.",
|
|
rule_reference=reference,
|
|
)
|
|
|
|
|
|
async def test_create_missing_inserts_the_proposals(session: AsyncSession) -> None:
|
|
depot = RecommendationRepository(session)
|
|
alert_id = await creer_alerte(session)
|
|
|
|
creees = await depot.create_missing(
|
|
[nouvelle(alert_id), nouvelle(alert_id, "escalade-astreinte-v1")]
|
|
)
|
|
await session.rollback()
|
|
|
|
assert creees == 2
|
|
|
|
|
|
async def test_create_missing_ignores_a_rule_already_held_for_the_alert(
|
|
session: AsyncSession,
|
|
) -> None:
|
|
depot = RecommendationRepository(session)
|
|
alert_id = await creer_alerte(session)
|
|
await depot.create_missing([nouvelle(alert_id)])
|
|
|
|
creees = await depot.create_missing([nouvelle(alert_id)])
|
|
await session.rollback()
|
|
|
|
assert creees == 0
|
|
|
|
|
|
async def test_create_missing_returns_zero_without_any_proposal(session: AsyncSession) -> None:
|
|
creees = await RecommendationRepository(session).create_missing([])
|
|
|
|
assert creees == 0
|
|
|
|
|
|
async def test_create_missing_inserts_every_proposal_across_several_batches(
|
|
session: AsyncSession, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
monkeypatch.setattr(module_recommendation, "TAILLE_DE_LOT", 2)
|
|
depot = RecommendationRepository(session)
|
|
alert_id = await creer_alerte(session)
|
|
propositions = [nouvelle(alert_id, f"regle-{index}-v1") for index in range(5)]
|
|
|
|
creees = await depot.create_missing(propositions)
|
|
enregistrees = [r for r in await depot.list_all() if r.alert_id == alert_id]
|
|
await session.rollback()
|
|
|
|
assert creees == 5
|
|
assert len(enregistrees) == 5
|