Files
rar-autopass/tests/test_mapping.py
Claude Agent 14e1c463f0 feat(errors): erori pe 3 niveluri (problema+cauza+fix) pe API si UI (PRD 5.4)
Catalog central pur app/errors.py ca sursa unica cod->{problema,fix},
consumat de API+UI+worker. Aditiv (field/message pastrate la octet) +
rar_error stocat superset. Scope: fluxul de declarare; login/signup/CSRF
neatinse. labels.parse_erori degradeaza gratios; UI progresiv AA light+dark.
631 teste.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 10:28:09 +00:00

250 lines
9.5 KiB
Python

"""Teste mapare op ROAAUTO -> cod RAR: fuzzy, rezolvare pura, flux on-demand.
Contract hibrid (decis 2026-06-15): item de prestatie cu cod_prestatie (RAR direct)
SAU cod_op_service+denumire (mapat de gateway). Op nemapata -> needs_mapping, apare
in editor; la salvarea maparii submission-ul se re-rezolva automat.
"""
from __future__ import annotations
import os
import tempfile
import pytest
from fastapi.testclient import TestClient
from app.mapping import normalize_for_match, resolve_prestatii, suggest_codes
# --------------------------------------------------------------------------- #
# Pur #
# --------------------------------------------------------------------------- #
def test_normalize_scoate_diacritice_si_colapseaza():
assert normalize_for_match("Reparație motor") == "REPARATIE MOTOR"
assert normalize_for_match(" întreținere ") == "INTRETINERE"
assert normalize_for_match(None) == ""
_NOM = [
{"cod_prestatie": "OE-1", "nume_prestatie": "REPARATIE"},
{"cod_prestatie": "OE-2", "nume_prestatie": "INTRETINERE"},
{"cod_prestatie": "OE-3", "nume_prestatie": "REVIZIE PERIODICA"},
{"cod_prestatie": "R-ODO", "nume_prestatie": "REPARATIE ODOMETRU"},
]
def test_suggest_pune_potrivirea_evidenta_prima():
s = suggest_codes("Reparatie odometru electronic", _NOM, limit=4)
assert s[0]["cod_prestatie"] == "R-ODO"
assert s[0]["score"] >= 60
def test_suggest_denumire_goala_intoarce_nomenclator_scor_zero():
s = suggest_codes("", _NOM, limit=2)
assert len(s) == 2
assert all(x["score"] == 0 for x in s)
def test_resolve_cod_direct_trece_neatins():
resolved, unmapped = resolve_prestatii([{"cod_prestatie": "oe-1"}], {})
assert resolved[0]["cod_prestatie"] == "OE-1" # normalizat upper
assert unmapped == []
def test_resolve_op_mapata():
resolved, unmapped = resolve_prestatii(
[{"cod_op_service": "1234", "denumire": "Schimb ulei"}], {"1234": "OE-2"}
)
assert resolved[0]["cod_prestatie"] == "OE-2"
assert unmapped == []
def test_resolve_op_nemapata_iese_in_unmapped():
resolved, unmapped = resolve_prestatii(
[{"cod_op_service": "9999", "denumire": "Operatie noua"}], {}
)
assert resolved[0]["cod_prestatie"] is None
assert unmapped == [{"cod_op_service": "9999", "denumire": "Operatie noua"}]
# --------------------------------------------------------------------------- #
# Flux complet (API) #
# --------------------------------------------------------------------------- #
@pytest.fixture()
def client(monkeypatch):
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t.db"))
from app.config import get_settings
get_settings.cache_clear()
from app.main import app
with TestClient(app) as c:
yield c
get_settings.cache_clear()
def _body(prestatii, **over):
prez = {
"vin": "WVWZZZ1KZAW000123",
"nr_inmatriculare": "B999TST",
"data_prestatie": "2026-06-15",
"odometru_final": "123456",
"prestatii": prestatii,
}
prez.update(over)
return {"rar_credentials": {"email": "x@y.ro", "password": "s"}, "prezentari": [prez]}
def test_nomenclator_seed_la_boot(client):
r = client.get("/v1/nomenclator")
coduri = {n["cod_prestatie"] for n in r.json()["nomenclator"]}
assert {"OE-1", "R-ODO", "I-ODO"} <= coduri
def test_cod_op_nemapat_da_needs_mapping(client):
r = client.post("/v1/prezentari", json=_body([{"cod_op_service": "OP100", "denumire": "Reparatie generala"}]))
assert r.status_code == 200
assert r.json()["results"][0]["status"] == "needs_mapping"
def test_pending_arata_op_cu_sugestii(client):
client.post("/v1/prezentari", json=_body([{"cod_op_service": "OP100", "denumire": "Reparatie generala"}]))
pend = client.get("/v1/mapari/pending").json()["pending"]
assert len(pend) == 1
e = pend[0]
assert e["cod_op_service"] == "OP100"
assert e["blocked"] == 1
assert e["suggestions"] and e["suggestions"][0]["cod_prestatie"]
def test_salvare_mapare_deblocheaza_submission(client):
client.post("/v1/prezentari", json=_body([{"cod_op_service": "OP100", "denumire": "Reparatie"}]))
r = client.post("/v1/mapari", json={"cod_op_service": "OP100", "cod_prestatie": "OE-1", "auto_send": True})
assert r.status_code == 200
assert r.json()["reresolve"]["requeued"] == 1
# submission-ul e acum queued
subs = client.get("/v1/prezentari", params={"status": "queued"}).json()["submissions"]
assert len(subs) == 1
# nu mai e nimic in pending
assert client.get("/v1/mapari/pending").json()["pending"] == []
def test_mapare_cod_inexistent_respinsa(client):
client.post("/v1/prezentari", json=_body([{"cod_op_service": "OP100", "denumire": "x"}]))
r = client.post("/v1/mapari", json={"cod_op_service": "OP100", "cod_prestatie": "ZZZ", "auto_send": True})
assert r.status_code == 422
def test_mapare_apoi_re_ingestie_e_directa(client):
"""Dupa ce maparea exista, o noua comanda cu acelasi op intra direct queued."""
client.post("/v1/prezentari", json=_body([{"cod_op_service": "OP100", "denumire": "x"}]))
client.post("/v1/mapari", json={"cod_op_service": "OP100", "cod_prestatie": "OE-1", "auto_send": True})
r = client.post("/v1/prezentari", json=_body([{"cod_op_service": "OP100", "denumire": "x"}], vin="WVWZZZ1KZAW000999"))
assert r.json()["results"][0]["status"] == "queued"
def test_cod_prestatie_direct_inca_merge(client):
"""Back-compat: trimiterea codului RAR direct se comporta ca inainte (queued)."""
r = client.post("/v1/prezentari", json=_body([{"cod_prestatie": "OE-1"}]))
assert r.json()["results"][0]["status"] == "queued"
def test_op_mapat_declanseaza_regula_odometru(client):
"""Dupa mapare la R-ODO, validarea cere odometruInitial -> needs_data (nu queued)."""
client.post("/v1/prezentari", json=_body([{"cod_op_service": "OPODO", "denumire": "Reparatie odometru"}]))
r = client.post("/v1/mapari", json={"cod_op_service": "OPODO", "cod_prestatie": "R-ODO", "auto_send": True})
stats = r.json()["reresolve"]
assert stats["needs_data"] == 1 and stats["requeued"] == 0
def test_item_fara_cod_si_fara_op_e_422(client):
r = client.post("/v1/prezentari", json=_body([{"denumire": "doar text"}]))
assert r.status_code == 422
# --------------------------------------------------------------------------- #
# US-003: 3 niveluri in classify_prezentare (needs_mapping) #
# --------------------------------------------------------------------------- #
def test_unmapped_are_3niveluri(client):
"""cod_op_service necunoscut -> needs_mapping; rar_error are cheie 'unmapped'
PASTRATA + campurile COD_NEMAPAT (cod/problema/cauza/fix)."""
import json
from app.mapping import classify_prezentare
content = {
"vin": "WVWZZZ1KZAW000123",
"nr_inmatriculare": "B999TST",
"data_prestatie": "2026-06-15",
"odometru_final": "123456",
"prestatii": [{"cod_op_service": "OP_NECUNOSCUT", "denumire": "Reparatie necunoscuta"}],
}
mapping = {}
mapping_meta = {}
res = classify_prezentare(content, mapping, mapping_meta)
assert res["status"] == "needs_mapping"
err = json.loads(res["rar_error"])
# Cheia originala pastrata
assert "unmapped" in err
assert len(err["unmapped"]) == 1
assert err["unmapped"][0]["cod_op_service"] == "OP_NECUNOSCUT"
# 3 niveluri prezente
assert err["cod"] == "COD_NEMAPAT"
assert err["problema"]
assert err["cauza"]
assert err["fix"]
def test_auto_send_oprit_3niveluri(client):
"""Mapare cu auto_send=0 -> needs_mapping; rar_error are cheie 'auto_send'
PASTRATA + campurile AUTO_SEND_OPRIT (cod/problema/cauza/fix)."""
import json
from app.mapping import classify_prezentare
content = {
"vin": "WVWZZZ1KZAW000123",
"nr_inmatriculare": "B999TST",
"data_prestatie": "2026-06-15",
"odometru_final": "123456",
"prestatii": [{"cod_op_service": "OP_REVIEW", "denumire": "Operatie cu review"}],
}
mapping = {"OP_REVIEW": "OE-1"}
mapping_meta = {"OP_REVIEW": {"cod_prestatie": "OE-1", "auto_send": False}}
res = classify_prezentare(content, mapping, mapping_meta)
assert res["status"] == "needs_mapping"
err = json.loads(res["rar_error"])
# Cheia originala pastrata
assert "auto_send" in err
# 3 niveluri prezente
assert err["cod"] == "AUTO_SEND_OPRIT"
assert err["problema"]
assert err["cauza"]
assert err["fix"]
def test_needs_data_pass_through(client):
"""VIN invalid -> needs_data; rar_error = array cu erori care au cod/problema/fix (US-002)."""
import json
from app.mapping import classify_prezentare
content = {
"vin": "VIN_INVALID_XXXXXXXXX", # nu trece regex
"nr_inmatriculare": "B999TST",
"data_prestatie": "2026-06-15",
"odometru_final": "123456",
"prestatii": [{"cod_prestatie": "OE-1"}],
}
mapping = {}
mapping_meta = {}
res = classify_prezentare(content, mapping, mapping_meta)
assert res["status"] == "needs_data"
erori = json.loads(res["rar_error"])
assert isinstance(erori, list)
assert len(erori) >= 1
# Fiecare eroare are cele 3 niveluri (pass-through US-002)
for e in erori:
assert "cod" in e, f"lipseste 'cod' in {e}"
assert "problema" in e, f"lipseste 'problema' in {e}"
assert "fix" in e, f"lipseste 'fix' in {e}"