"""Regresiones de persistencia para ingestas desktop síncronas.""" from __future__ import annotations import asyncio from contextlib import contextmanager from types import SimpleNamespace import pytest from api import desktop_ingest from api import main as extraction_api class FakeSession: def __init__(self) -> None: self.added: list[object] = [] self.batch = SimpleNamespace(status="running", updated_at=None) def add(self, value: object) -> None: self.added.append(value) def flush(self) -> None: pass def get(self, _model: object, _identifier: str) -> object: return self.batch def refresh(self, _value: object) -> None: pass def test_multiple_document_types_complete_once_per_radicado(monkeypatch) -> None: monkeypatch.setattr( desktop_ingest, "flatten_extraction_result", lambda fused, _fields, _aliases: [fused], ) session = FakeSession() request = { "structure_mode": "radicados", "fields": ["factura/numero", "soporte/codigo"], "doc_types": [{"name": "factura"}, {"name": "soporte"}], "documents": [ {"radicado": "RAD-1", "filename": "factura.pdf", "doc_type": "factura"}, {"radicado": "RAD-1", "filename": "soporte.pdf", "doc_type": "soporte"}, ], } task = SimpleNamespace( result={"processed": 0, "total": 1, "row_count": 0, "partials": {}}, status="running", finished_at=None, updated_at=None, ) first = desktop_ingest.persist_desktop_ingest_result( session, task=task, batch_id="batch-1", radicado="RAD-1", filename="factura.pdf", doc_type="factura", request=request, normalized_obj={"numero": "F-1"}, ) desktop_ingest.mark_task_completed_if_done(session, task, "batch-1") assert first["finalized"] is False assert task.result["processed"] == 0 assert task.status == "running" assert task.result["partials"] == {"RAD-1": {"factura": {"numero": "F-1"}}} assert session.added == [] second = desktop_ingest.persist_desktop_ingest_result( session, task=task, batch_id="batch-1", radicado="RAD-1", filename="soporte.pdf", doc_type="soporte", request=request, normalized_obj={"codigo": "S-1"}, ) desktop_ingest.mark_task_completed_if_done(session, task, "batch-1") assert second["finalized"] is True assert task.result["processed"] == 1 assert task.result["partials"] == {} assert task.status == "completed" assert session.batch.status == "completed" assert len(session.added) == 1 row = session.added[0] assert row.radicado == "RAD-1" assert row.raw_json["factura"] == {"numero": "F-1"} assert row.raw_json["soporte"] == {"codigo": "S-1"} def test_document_failure_completes_radicado_and_discards_partials(monkeypatch) -> None: monkeypatch.setattr( desktop_ingest, "flatten_extraction_result", lambda fused, _fields, _aliases: [fused], ) session = FakeSession() request = { "structure_mode": "radicados", "fields": ["factura/numero", "soporte/codigo"], "doc_types": [{"name": "factura"}, {"name": "soporte"}], "documents": [ {"radicado": "RAD-1", "filename": "factura.pdf", "doc_type": "factura"}, {"radicado": "RAD-1", "filename": "soporte.pdf", "doc_type": "soporte"}, ], } task = SimpleNamespace( result={"processed": 0, "total": 1, "row_count": 0, "partials": {}}, status="running", finished_at=None, updated_at=None, ) desktop_ingest.persist_desktop_ingest_result( session, task=task, batch_id="batch-1", radicado="RAD-1", filename="factura.pdf", doc_type="factura", request=request, normalized_obj={"numero": "F-1"}, ) failure = desktop_ingest.record_desktop_ingest_error( session, task=task, radicado="RAD-1", filename="soporte.pdf", doc_type="soporte", error=RuntimeError("backend no disponible"), ) desktop_ingest.mark_task_completed_if_done(session, task, "batch-1") assert failure["first_failure"] is True assert task.result["processed"] == 1 assert task.result["partials"] == {} assert task.result["failed_radicados"] == ["RAD-1"] assert task.result["errors"] == [{ "radicado": "RAD-1", "filename": "soporte.pdf", "doc_type": "soporte", "error": "backend no disponible", }] assert task.status == "completed" assert session.added == [] late_success = desktop_ingest.persist_desktop_ingest_result( session, task=task, batch_id="batch-1", radicado="RAD-1", filename="soporte.pdf", doc_type="soporte", request=request, normalized_obj={"codigo": "S-1"}, ) duplicate_failure = desktop_ingest.record_desktop_ingest_error( session, task=task, radicado="RAD-1", filename="factura.pdf", doc_type="factura", error=RuntimeError("otro fallo tardío"), ) assert late_success["skipped"] is True assert duplicate_failure["first_failure"] is False assert task.result["processed"] == 1 assert task.result["partials"] == {} assert session.added == [] def test_process_sync_persists_failure_before_reraising(monkeypatch) -> None: task = SimpleNamespace( result={"processed": 0, "total": 1, "row_count": 0, "partials": {}}, status="running", finished_at=None, updated_at=None, ) session = FakeSession() def get(model: object, _identifier: str, **_kwargs: object) -> object: if model is extraction_api.Task: return task return session.batch session.get = get # type: ignore[method-assign] @contextmanager def fake_session_scope(): yield session class FailingService: extractor = SimpleNamespace(model="test-model") def extract_full_from_pdf(self, *_args: object, **_kwargs: object) -> object: raise RuntimeError("fallo simulado") monkeypatch.setattr(extraction_api, "session_scope", fake_session_scope) monkeypatch.setattr(extraction_api, "processing_service", FailingService()) with pytest.raises(RuntimeError, match="fallo simulado"): asyncio.run(extraction_api._process_sync( "task-1", "batch-1", b"pdf", "factura.pdf", "RAD-1", "factura", { "fields": ["factura/numero"], "doc_types": [{"name": "factura"}], "documents": [ {"radicado": "RAD-1", "filename": "factura.pdf", "doc_type": "factura"}, ], }, )) assert task.result["processed"] == 1 assert task.result["failed_radicados"] == ["RAD-1"] assert task.result["errors"][0]["error"] == "fallo simulado" assert task.status == "completed"