"""TDD L14-S6 — Integrare Layer 2/3 in editor (suggestion-only, DUPA 5.15). Scenarii acoperite: - F1-regression CRITIC: SILVER/shared GOLD NU auto-trimit (resolve_prestatii neschimbat) - pending_unmapped include sugestie GOLD partajat > SILVER > embeddings (precedenta Eng-F2) - record_human_validation apelat la confirmare umana (POST /mapari -> shared_mappings) - Degradare gratioasa cand embeddings indisponibil (mock is_available=False) - Separare structurala #13: resolve_prestatii/load_mapping NU citesc tabelele de sugestii """ from __future__ import annotations import os import tempfile import pytest # --------------------------------------------------------------------------- # # Fixtures # # --------------------------------------------------------------------------- # @pytest.fixture() def env(monkeypatch): """DB temporara cu schema initiata, auth dezactivata (mod dev).""" tmp = tempfile.mkdtemp() monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "l14_s6_test.db")) monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false") from app.config import get_settings get_settings.cache_clear() from app.db import init_db init_db() yield monkeypatch get_settings.cache_clear() @pytest.fixture() def conn(env): from app.db import get_connection c = get_connection() # Seed nomenclator (OE-1, OE-2, OE-3, OE-4 suficient pentru teste) c.executemany( "INSERT OR IGNORE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?, ?)", [ ("OE-1", "REPARATIE MOTOR"), ("OE-2", "INTRETINERE"), ("OE-3", "REVIZIE PERIODICA"), ("OE-4", "REGLARE"), ], ) c.commit() yield c c.close() @pytest.fixture() def client(env): from app.main import app from fastapi.testclient import TestClient with TestClient(app) as c: yield c # --------------------------------------------------------------------------- # # F1-regression CRITIC: SILVER/shared GOLD NU auto-trimit # # --------------------------------------------------------------------------- # def test_f1_silver_nu_auto_trimite(conn): """CRITICAL F1: un cod in SILVER (mapping_suggestions) NU produce auto-trimitere. resolve_prestatii cu mapping gol + SILVER existent -> operatie ramane nemapata. Submissionul ar ramane needs_mapping, NU queued. """ from app.shared_store import seed_suggestions from app.mapping import resolve_prestatii seed_suggestions(conn, [ {"denumire": "Revizie periodica", "cod_prestatie": "OE-3", "source": "llm", "confidence": 0.95}, ]) conn.commit() # resolve_prestatii cu mapping gol -> SILVER nu se vede resolved, unmapped = resolve_prestatii( [{"cod_op_service": "OP-REV", "denumire": "Revizie periodica"}], {}, # operations_mapping gol ) # Operatia ramane nemapata (SILVER nu e in resolve, #13) assert resolved[0]["cod_prestatie"] is None assert len(unmapped) == 1 def test_f1_shared_gold_nu_auto_trimite(conn): """CRITICAL F1: un cod in shared_mappings (GOLD partajat) NU produce auto-trimitere. resolve_prestatii cu mapping gol + shared GOLD existent -> operatie ramane nemapata. """ from app.shared_store import record_human_validation from app.mapping import resolve_prestatii record_human_validation(conn, "Schimb ulei motor", "OE-3") conn.commit() # resolve_prestatii cu mapping gol -> GOLD partajat nu se vede resolved, unmapped = resolve_prestatii( [{"cod_op_service": "OP-ULEI", "denumire": "Schimb ulei motor"}], {}, # operations_mapping gol ) # Operatia ramane nemapata (GOLD partajat nu e in resolve, #13) assert resolved[0]["cod_prestatie"] is None assert len(unmapped) == 1 def test_f1_load_mapping_nu_citeste_shared_gold(conn): """Separare #13: load_mapping NU returneaza coduri din shared_mappings.""" from app.shared_store import record_human_validation from app.mapping import load_mapping record_human_validation(conn, "Revizie anuala", "OE-3") conn.commit() mapping = load_mapping(conn, account_id=1) # GOLD partajat nu trebuie sa apara in load_mapping (citit de resolve_prestatii) assert "Revizie anuala" not in mapping # Maparea propriu-zisa (operations_mapping) ramane goala assert len(mapping) == 0 # --------------------------------------------------------------------------- # # enrich_suggestions: GOLD partajat > SILVER > embeddings # # --------------------------------------------------------------------------- # def test_enrich_fara_surse_returneaza_none(conn): """Fara GOLD/SILVER/embedding -> sugestie_principala = None.""" from app.mapping import enrich_suggestions result = enrich_suggestions(conn, "Operatie inexistenta") assert result["sugestie_principala"] is None assert result["surse"]["gold_partajat"] is None assert result["surse"]["silver"] is None assert result["surse"]["embedding"] is None def test_enrich_include_gold_partajat(conn): """enrich_suggestions returneaza sugestie GOLD partajat cand shared_mappings are match.""" from app.shared_store import record_human_validation from app.mapping import enrich_suggestions record_human_validation(conn, "Schimb ulei", "OE-3") conn.commit() result = enrich_suggestions(conn, "Schimb ulei") assert result["sugestie_principala"] is not None assert result["sugestie_principala"]["cod_prestatie"] == "OE-3" assert result["sugestie_principala"]["sursa"] == "gold_partajat" assert result["surse"]["gold_partajat"] == "OE-3" def test_enrich_include_silver(conn): """enrich_suggestions returneaza sugestie SILVER cand mapping_suggestions are match.""" from app.shared_store import seed_suggestions from app.mapping import enrich_suggestions seed_suggestions(conn, [ {"denumire": "Reparatie motor", "cod_prestatie": "OE-1", "source": "llm", "confidence": 0.9}, ]) conn.commit() result = enrich_suggestions(conn, "Reparatie motor") assert result["sugestie_principala"] is not None assert result["sugestie_principala"]["cod_prestatie"] == "OE-1" assert result["sugestie_principala"]["sursa"] == "silver" assert result["surse"]["silver"] == "OE-1" def test_enrich_precedenta_gold_peste_silver(conn): """Precedenta Eng-F2: GOLD partajat castiga fata de SILVER cand ambele exista.""" from app.shared_store import seed_suggestions, record_human_validation from app.mapping import enrich_suggestions # SILVER spune OE-1, GOLD spune OE-3 seed_suggestions(conn, [ {"denumire": "Verificare tehnica", "cod_prestatie": "OE-1", "source": "llm", "confidence": 0.8}, ]) record_human_validation(conn, "Verificare tehnica", "OE-3") conn.commit() result = enrich_suggestions(conn, "Verificare tehnica") assert result["sugestie_principala"] is not None assert result["sugestie_principala"]["cod_prestatie"] == "OE-3" assert result["sugestie_principala"]["sursa"] == "gold_partajat" # SILVER prezent dar nu castiga assert result["surse"]["silver"] == "OE-1" assert result["surse"]["gold_partajat"] == "OE-3" def test_enrich_degradare_embeddings_indisponibil(conn, monkeypatch): """Degradare gratioasa (#16b): cand embeddings nu e disponibil, nu eroare.""" import app.embeddings as emb_mod monkeypatch.setattr(emb_mod, "is_available", lambda: False) from app.mapping import enrich_suggestions # Fara surse -> sugestie_principala = None, fara exceptie result = enrich_suggestions(conn, "Operatie demo", include_embeddings=True) assert result["sugestie_principala"] is None assert result["surse"]["embedding"] is None def test_enrich_corpus_gol_nu_incarca_modelul(conn, monkeypatch): """Bug fix (code-review): enrich_suggestions NU lazy-load-eaza modelul de 220MB cand corpus-ul embeddings e gol. Implementarea veche apela `is_available()` neconditionat -> `_get_engine()` -> `_load_engine()` -> `FastEmbedBackend()` (incarcare sincrona 30-120s) chiar daca `index_corpus` nu a fost apelat niciodata in productie -> corpus gol -> `suggest_nearest` ar fi returnat [] oricum (zero beneficiu, cost mare). Fix: poarta `has_corpus()` (ieftina, nu construieste engine-ul cand `_engine is None`). """ import app.embeddings as emb_mod # Engine ne-initializat -> corpus gol prin definitie. monkeypatch.setattr(emb_mod, "_engine", None, raising=False) incarcari = {"n": 0} orig_load = emb_mod._load_engine def _spy_load(): incarcari["n"] += 1 return orig_load() monkeypatch.setattr(emb_mod, "_load_engine", _spy_load) from app.mapping import enrich_suggestions result = enrich_suggestions(conn, "Operatie oarecare", include_embeddings=True) assert result["surse"]["embedding"] is None assert incarcari["n"] == 0, ( "Modelul de embeddings NU trebuie incarcat cand corpus-ul e gol " f"(index_corpus nu e wired). _load_engine apelat de {incarcari['n']} ori." ) class _FakeEmbedBackend: """Backend embedding determinist (3 dimensiuni keyword) — fara model real 230MB.""" def embed(self, texts): out = [] for t in texts: tl = str(t).lower() out.append([ 1.0 if "ulei" in tl else 0.0, 1.0 if "motor" in tl else 0.0, 1.0 if "frana" in tl else 0.0, ]) return out def test_embeddings_functional_cand_flag_activ(conn, monkeypatch): """PRD #15: cu AUTOPASS_EMBEDDINGS_ENABLED=true, embeddings produce efectiv o sugestie. Wire-uieste ensure_embeddings_corpus (corpus din nomenclator) + enrich_suggestions. Backend injectat (determinist) -> nu incarca modelul real de 230MB. """ import app.embeddings as emb_mod from app.embeddings import EmbeddingEngine from app.config import get_settings # Activeaza flagul + injecteaza backend fals in singleton-ul global. monkeypatch.setenv("AUTOPASS_EMBEDDINGS_ENABLED", "true") get_settings.cache_clear() monkeypatch.setattr(emb_mod, "_engine", EmbeddingEngine(backend=_FakeEmbedBackend())) # Corpusul sursa = mapping_suggestions (SILVER) -- PRD 5.18 US-005. # (Inainte era nomenclator_rar; migrat la mapping_suggestions ca k-NN sa # opereze pe exemple reale etichetate, nu pe categorii generice RAR.) conn.execute( "INSERT OR REPLACE INTO mapping_suggestions " "(denumire_normalizata, cod_prestatie, is_nul, source, confidence) VALUES (?, ?, ?, ?, ?)", ("Schimb ulei", "UL-1", 0, "llm", 0.95), ) conn.execute( "INSERT OR REPLACE INTO mapping_suggestions " "(denumire_normalizata, cod_prestatie, is_nul, source, confidence) VALUES (?, ?, ?, ?, ?)", ("Placute frana", "FR-1", 0, "llm", 0.95), ) conn.commit() from app.mapping import ensure_embeddings_corpus, enrich_suggestions ensure_embeddings_corpus(conn) assert emb_mod.has_corpus(), "corpusul trebuie indexat cand flagul e activ" # "schimbat uleiul motor" -> vector [1,1,0] -> cel mai apropiat = UL-1 (Schimb ulei). result = enrich_suggestions(conn, "schimbat uleiul motor", include_embeddings=True) assert result["surse"]["embedding"] == "UL-1", ( f"embeddings trebuie sa sugereze UL-1, got {result['surse']}" ) get_settings.cache_clear() def test_embeddings_flag_off_ramane_noop(conn, monkeypatch): """Cu flagul off (default), ensure_embeddings_corpus e no-op total (nu indexeaza).""" import app.embeddings as emb_mod from app.embeddings import EmbeddingEngine from app.config import get_settings monkeypatch.setenv("AUTOPASS_EMBEDDINGS_ENABLED", "false") get_settings.cache_clear() # Engine cu backend disponibil, dar flagul off -> NU se indexeaza nimic. monkeypatch.setattr(emb_mod, "_engine", EmbeddingEngine(backend=_FakeEmbedBackend())) from app.mapping import ensure_embeddings_corpus ensure_embeddings_corpus(conn) assert not emb_mod.has_corpus(), "flag off -> corpusul NU trebuie indexat" get_settings.cache_clear() def test_enrich_silver_nul_ignorat(conn): """SILVER cu is_nul=1 (non-operatie) NU apare ca sugestie.""" from app.shared_store import seed_suggestions from app.mapping import enrich_suggestions seed_suggestions(conn, [ {"denumire": "ITP CT 12 ABC", "is_nul": True, "source": "llm", "confidence": 0.99}, ]) conn.commit() result = enrich_suggestions(conn, "ITP CT 12 ABC") assert result["sugestie_principala"] is None assert result["surse"]["silver"] is None # --------------------------------------------------------------------------- # # pending_unmapped: include sugestie_principala # # --------------------------------------------------------------------------- # def test_pending_unmapped_include_sugestie_principala(conn): """pending_unmapped returneaza entries cu sugestie_principala din GOLD/SILVER.""" from app.shared_store import record_human_validation from app.mapping import pending_unmapped import json record_human_validation(conn, "Schimb ulei motor", "OE-3") conn.commit() # Creeaza un submission needs_mapping cu "Schimb ulei motor" conn.execute( "INSERT INTO submissions (account_id, status, payload_json, idempotency_key) " "VALUES (1, 'needs_mapping', ?, 'key-test-001')", (json.dumps({ "vin": "WVWZZZ1KZAW001111", "prestatii": [{"cod_op_service": "OP-ULEI", "denumire": "Schimb ulei motor"}], }),), ) conn.commit() pending = pending_unmapped(conn, account_id=1) assert len(pending) == 1 entry = pending[0] # sugestie_principala adaugat de enrich_suggestions (L14-S6) assert "sugestie_principala" in entry sp = entry["sugestie_principala"] assert sp is not None assert sp["cod_prestatie"] == "OE-3" assert sp["sursa"] == "gold_partajat" def test_pending_unmapped_fara_surse_sugestie_principala_none(conn, monkeypatch): """pending_unmapped -> sugestie_principala = None cand nu exista nicio sursa. Dezactiveaza embeddings prin poarta reala `has_corpus`=False (gate-ul folosit de enrich_suggestions dupa wiring), independent de starea singleton-ului global lasata de alte teste (izolare de ordine). """ import app.embeddings as emb_mod monkeypatch.setattr(emb_mod, "has_corpus", lambda: False) monkeypatch.setattr(emb_mod, "is_available", lambda: False) from app.mapping import pending_unmapped import json conn.execute( "INSERT INTO submissions (account_id, status, payload_json, idempotency_key) " "VALUES (1, 'needs_mapping', ?, 'key-test-002')", (json.dumps({ "vin": "WVWZZZ1KZAW002222", "prestatii": [{"cod_op_service": "OP-FARA-SURSA", "denumire": "Operatie de nisa"}], }),), ) conn.commit() pending = pending_unmapped(conn, account_id=1) assert len(pending) == 1 entry = pending[0] assert "sugestie_principala" in entry assert entry["sugestie_principala"] is None # --------------------------------------------------------------------------- # # record_human_validation apelat la confirmare umana # # --------------------------------------------------------------------------- # def test_record_human_validation_la_post_mapari(env, client): """POST /mapari (tab Mapari) -> record_human_validation scrie in shared_mappings. Testul verifica ca GOLD partajat se populeaza automat la confirmarea umana din interfata de mapari. """ from app.db import get_connection import json # Creeaza un submission needs_mapping conn_setup = get_connection() try: conn_setup.executemany( "INSERT OR IGNORE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?, ?)", [("OE-3", "REVIZIE PERIODICA"), ("OE-1", "REPARATIE")], ) conn_setup.execute( "INSERT INTO submissions (account_id, status, payload_json, idempotency_key) " "VALUES (1, 'needs_mapping', ?, 'key-hv-001')", (json.dumps({ "vin": "WVWZZZ1KZAW003333", "prestatii": [{"cod_op_service": "OP-REV", "denumire": "Revizie anuala"}], }),), ) conn_setup.commit() finally: conn_setup.close() # POST /mapari cu denumire (L14-S6: form include denumire hidden) resp = client.post( "/mapari", data={ "cod_op_service": "OP-REV", "cod_prestatie": "OE-3", "denumire": "Revizie anuala", "csrf_token": "", }, ) assert resp.status_code == 200, resp.text # Verifica ca shared_mappings contine intrarea conn_check = get_connection() try: from app.shared_store import lookup_shared_gold row = lookup_shared_gold(conn_check, "Revizie anuala") assert row is not None, "record_human_validation nu a scris in shared_mappings" assert row["cod_prestatie"] == "OE-3" finally: conn_check.close() def test_record_human_validation_la_mapeaza_inline(env, client): """POST /trimitere/{id}/mapeaza -> record_human_validation scrie in shared_mappings. Testul verifica ca GOLD partajat se populeaza la maparea inline din panoul de detaliu. """ from app.db import get_connection import json # Setup submission needs_mapping conn_setup = get_connection() try: conn_setup.executemany( "INSERT OR IGNORE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?, ?)", [("OE-1", "REPARATIE"), ("OE-3", "REVIZIE")], ) conn_setup.execute( "INSERT INTO submissions (account_id, status, payload_json, idempotency_key) " "VALUES (1, 'needs_mapping', ?, 'key-inline-001')", (json.dumps({ "vin": "WVWZZZ1KZAW004444", "data_prestatie": "2026-06-15", "odometru_final": 100000, "prestatii": [{"cod_op_service": "OP-REP", "denumire": "Reparatie chiulasa"}], }),), ) conn_setup.commit() # Preia ID-ul submission-ului sid = conn_setup.execute("SELECT id FROM submissions WHERE idempotency_key='key-inline-001'").fetchone()["id"] finally: conn_setup.close() resp = client.post( f"/trimitere/{sid}/mapeaza", data={ "cod_op_service": "OP-REP", "cod_prestatie": "OE-1", "csrf_token": "", }, ) assert resp.status_code == 200, resp.text # Verifica shared_mappings conn_check = get_connection() try: from app.shared_store import lookup_shared_gold row = lookup_shared_gold(conn_check, "Reparatie chiulasa") assert row is not None, "record_human_validation nu a scris in shared_mappings pentru mapeaza inline" assert row["cod_prestatie"] == "OE-1" finally: conn_check.close() def test_mapare_salvata_fara_denumire_nu_polueaza_gold(env, client): """Bug fix (code-review 5.15): editarea unei mapari salvate FARA denumire NU scrie o intrare bogus in GOLD partajat (cheiata pe cod_op_service in loc de denumire umana). Formularul din _mapari.html nu trimite denumire; vechiul fallback `denumire or cod_op_service` scria shared_mappings cheiat pe cod_op_service -> lookup_shared_gold (pe denumirea umana) nu il potrivea niciodata -> poluare. Fix: _record_gold_validation sare scrierea cand denumire lipseste sau == cod_op_service. """ from app.db import get_connection conn_setup = get_connection() try: conn_setup.execute( "INSERT OR IGNORE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?, ?)", ("OE-1", "REPARATIE"), ) conn_setup.commit() finally: conn_setup.close() # Editare mapare salvata FARA denumire (ca formularul real din _mapari.html). resp = client.post( "/mapari/salvate", data={ "cod_op_service": "OP-SALV", "cod_prestatie": "OE-1", "csrf_token": "", }, ) assert resp.status_code == 200, resp.text conn_check = get_connection() try: from app.shared_store import lookup_shared_gold # NICIO intrare bogus cheiata pe cod_op_service. assert lookup_shared_gold(conn_check, "OP-SALV") is None, ( "GOLD partajat poluat cu cod_op_service ca si cheie (denumire lipsa)" ) finally: conn_check.close() # --------------------------------------------------------------------------- # # Separare structurala #13 (redundant cu test_shared_store dar explicit L14) # # --------------------------------------------------------------------------- # def test_separare_silver_din_resolve_prestatii(): """#13: resolve_prestatii nu citeste mapping_suggestions (SILVER).""" from app.mapping import resolve_prestatii # Apelam fara conn (pur) — SILVER nu e parametru si nu e accesat resolved, unmapped = resolve_prestatii( [{"cod_op_service": "OP-TEST", "denumire": "Test silver"}], {}, # mapping gol ) assert resolved[0]["cod_prestatie"] is None assert len(unmapped) == 1 def test_separare_shared_gold_din_resolve_prestatii(): """#13: resolve_prestatii nu citeste shared_mappings (GOLD partajat).""" from app.mapping import resolve_prestatii resolved, unmapped = resolve_prestatii( [{"cod_op_service": "OP-TEST2", "denumire": "Test gold partajat"}], {}, # mapping gol ) assert resolved[0]["cod_prestatie"] is None assert len(unmapped) == 1