"""Missão 8 — Bloco C: Testes direcionados para elevar cobertura de reference_registry e classification.""" import pytest from sqlalchemy import create_engine from sqlalchemy.orm import Session from central.app.models import Base, Dataset from central.app.reference_registry import ( register_draft, approve_draft, reject_draft, list_registry, diff_against_vigente, technical_checklist, RegistryError ) from central.app.classification import Classifier @pytest.fixture def memory_db(): engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(engine) with Session(engine) as session: yield session # ---------------- C.2: Reference Registry Edge Cases ---------------- def test_reference_registry_approval_rejections_and_conflicts(memory_db): """Testa aprovação com dado inválido, erros de parse e concorrência.""" # 1. Tentar aprovar dataset inexistente with pytest.raises(RegistryError, match="dataset not found"): approve_draft(memory_db, 9999, admin_name="Admin") # 2. Tentar rejeitar dataset inexistente with pytest.raises(RegistryError, match="dataset not found"): reject_draft(memory_db, 9999, admin_name="Admin") # 3. Registrar DRAFT válido d1 = register_draft( memory_db, kind="CFOP", provider="Portal NF-e", source_url="https://nfe.fazenda.gov.br", sha256="1" * 64, version_label="cfop-v1", record_count=619, ) # 4. Primeira aprovação tem sucesso app1 = approve_draft(memory_db, d1.id, admin_name="Admin 1") assert app1.status == "VIGENTE" # 5. Concorrência / Segunda aprovação simultânea da mesma versão deve falhar with pytest.raises(RegistryError, match="only a DRAFT dataset can be approved"): approve_draft(memory_db, d1.id, admin_name="Admin 2") # 6. Tentar rejeitar dataset que já é VIGENTE deve falhar with pytest.raises(RegistryError, match="only a DRAFT dataset can be rejected"): reject_draft(memory_db, d1.id, admin_name="Admin") # 7. Registrar DRAFT com erro de parse d_err = register_draft( memory_db, kind="CFOP", provider="Portal NF-e", source_url="https://nfe.fazenda.gov.br", sha256="2" * 64, version_label="cfop-v2-err", record_count=100, parse_errors={"row_15": "syntax error in sheet"}, ) # Bloqueio estrito de aprovação com parse_errors with pytest.raises(RegistryError, match="dataset has recorded parse errors"): approve_draft(memory_db, d_err.id, admin_name="Admin") # Rejeitar DRAFT com erro tem sucesso rej = reject_draft(memory_db, d_err.id, admin_name="Admin") assert rej.status == "REJECTED" # Rejeitar novamente falha with pytest.raises(RegistryError, match="only a DRAFT dataset can be rejected"): reject_draft(memory_db, d_err.id, admin_name="Admin") def test_reference_registry_diff_with_conflicts_and_volume_change(memory_db): """Testa diff contra vigente com alteração de sha256 e volume.""" # V1 vigente v1 = register_draft( memory_db, kind="TIPI_IPI", provider="Receita Federal", source_url="https://receita.gov.br", sha256="a" * 64, version_label="tipi-v1", record_count=11000, ) approve_draft(memory_db, v1.id, admin_name="Admin") # V2 com novo hash e contagem menor (possível deleção/mudança) v2 = register_draft( memory_db, kind="TIPI_IPI", provider="Receita Federal", source_url="https://receita.gov.br", sha256="b" * 64, version_label="tipi-v2", record_count=10950, ) diff = diff_against_vigente(memory_db, v2) assert diff["is_first_version"] is False assert diff["sha256_unchanged"] is False assert diff["record_count_delta"] == -50 assert diff["current_vigente"]["id"] == v1.id # ---------------- C.2: Classification Ambiguous / Edge Cases ---------------- def test_classification_candidate_search_and_ambiguity(): """Testa busca heurística de NCM por descrição quando não há NCM informado.""" class MockEntry: def __init__(self, code, desc): self.code = code self.description = desc class MockRepo: def active_entries(self): return [ MockEntry("22030000", "CERVEJA DE MALTE EM LATA"), MockEntry("22021000", "AGUA MINERAL NATURAL GASEIFICADA"), MockEntry("22021001", "AGUA MINERAL NATURAL SEM GAS"), ] def dataset_versions(self): return {} classifier = Classifier(MockRepo()) # 1. Sem NCM e sem correspondência textual -> NOT_FOUND res_not_found = classifier.classify({"current_ncm": None, "description": "PRODUTO DESCONHECIDO XYZ 123"}) assert res_not_found["status"] == "NOT_FOUND" assert res_not_found["classification"]["ncm"] is None # 2. Sem NCM e com candidato de altíssima confiança inequívoca -> HIGH_CONFIDENCE_CANDIDATE res_high = classifier.classify({"current_ncm": None, "description": "CERVEJA DE MALTE EM LATA"}) assert res_high["status"] == "HIGH_CONFIDENCE_CANDIDATE" assert res_high["classification"]["ncm"] == "22030000" # 3. Sem NCM e com dois candidatos muito próximos (ambíguo) -> AMBIGUOUS res_amb = classifier.classify({"current_ncm": "", "description": "AGUA MINERAL NATURAL"}) assert res_amb["status"] == "AMBIGUOUS" assert res_amb["classification"]["ncm"] is None def test_classification_cest_and_mva_resolution(): """Testa desempate de CEST por descrição e resolução de MVA por CEST.""" class MockCest: def __init__(self, code, desc): self.code = code self.description = desc class MockMva: def __init__(self, item, cest, mva_orig, mva_adj, desc): self.item = item self.cest = cest self.mva_original = mva_orig self.mva_adjusted = mva_adj self.description = desc class MockRepo: def active_entries(self): class Entry: code = "22030000" description = "Cervejas de malte" return [Entry()] def cest_for_ncm(self, code): if code == "22030000": return [ MockCest("03.021.01", "Cerveja em garrafa de vidro"), MockCest("03.021.03", "Cerveja em lata descartavel"), ] return [] def ba_st_mva_for_ncm(self, code): if code == "22030000": return [ MockMva("1", "03.021.01", 50.0, 70.0, "Cerveja garrafa"), MockMva("2", "03.021.03", 60.0, 80.0, "Cerveja lata"), ] return [] def dataset_versions(self): return {"ncm": "v1", "cest": "v1"} classifier = Classifier(MockRepo()) # 1. Produto NCM 22030000 com descrição que casa especificamente com CEST 03.021.03 (lata) res_lata = classifier.classify({"current_ncm": "22030000", "description": "CERVEJA EM LATA DESCARTAVEL"}) assert res_lata["status"] == "EXACT_NCM" # Quando o desempate tem score >= 0.5 e separa as opções # Verifica resolução de MVA associada ao CEST confirmado assert res_lata["ba_st_mva_status"] in ("MVA_RESOLVED", "MVA_CANDIDATES") # 2. Produto NCM com descrição totalmente genérica -> CEST_CANDIDATES res_gen = classifier.classify({"current_ncm": "22030000", "description": "BEBIDA"}) assert res_gen["cest_status"] == "CEST_DESCRIPTION_MISMATCH"