229 lines
7.0 KiB
Python
229 lines
7.0 KiB
Python
"""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"
|