Files
odoo_ocr/tests/test_cli.py
T
fegger da37e91c86 Add CLI orchestration with confidence-gated XML output
- Refactors process_single into inner pipeline with guaranteed client close.

- Runs classifier -> OCR branch -> extraction -> review -> XML/sidecar.

- Uses review-corrected invoice when available.

- Confidence >= 0.75 produces XML; < 0.90 adds a human-review comment.

- Adds end-to-end CLI tests covering confidence bands, corrections, and batch mode.
2026-08-21 16:44:35 +02:00

134 lines
3.9 KiB
Python

"""End-to-end CLI pipeline tests using the fake VLM client."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
import pytest
from lxml import etree
from PIL import Image
from odoo_ocr import cli
from odoo_ocr.config import Settings
from tests.conftest import FakeVLMClient
EXTRACTION: dict[str, Any] = {
"vendor_name": "Acme",
"vendor_address": None,
"vendor_vat": None,
"invoice_number": "INV-9",
"invoice_date": "2024-01-01",
"due_date": None,
"currency": "EUR",
"payment_terms": None,
"line_items": [
{
"description": "Widget",
"quantity": 1,
"unit_price": 100.0,
"total_price": 100.0,
"tax_rate": 0,
}
],
"subtotal": 100.0,
"tax_total": 0.0,
"total": 100.0,
"iban": None,
"raw_ocr_text": "",
}
def _responses(confidence: float) -> dict[str, Any]:
return {
"document classifier": {
"category": "scanned_print",
"confidence": 0.9,
"reasoning": "printed invoice",
},
"OCR engine": {"text": "Invoice INV-9 from Acme, total 100.00 EUR"},
"invoice data extraction": EXTRACTION,
"invoice review assistant": {
"valid": True,
"confidence": confidence,
"issues": [],
"corrected_invoice": None,
},
}
def _write_invoice(tmp_path: Path) -> Path:
src = tmp_path / "invoice.jpg"
Image.new("RGB", (100, 100), "white").save(src)
return src
def _install_fake(monkeypatch: pytest.MonkeyPatch, responses: dict[str, Any]) -> FakeVLMClient:
fake = FakeVLMClient(responses=responses)
monkeypatch.setattr(cli, "OllamaClient", lambda _settings: fake)
return fake
@pytest.mark.asyncio
@pytest.mark.parametrize(
("confidence", "expect_xml", "expect_review_comment"),
[(0.95, True, False), (0.80, True, True), (0.70, False, False)],
)
async def test_confidence_bands(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
confidence: float,
expect_xml: bool,
expect_review_comment: bool,
) -> None:
src = _write_invoice(tmp_path)
out_dir = tmp_path / "out"
_install_fake(monkeypatch, _responses(confidence))
sidecar = await cli.process_single(src, out_dir, Settings())
assert sidecar["review"]["confidence"] == confidence
assert (out_dir / "invoice_sidecar.json").exists()
data = json.loads((out_dir / "invoice_sidecar.json").read_text(encoding="utf-8"))
assert data["extracted_invoice"]["vendor_name"] == "Acme"
xml_file = out_dir / "invoice.xml"
assert xml_file.exists() is expect_xml
if expect_xml:
xml = xml_file.read_text(encoding="utf-8")
assert ("HUMAN REVIEW ADVISED" in xml) is expect_review_comment
root = etree.fromstring(xml.encode("utf-8"))
models = {record.get("model") for record in root.iter("record")}
assert {"res.partner", "account.move", "account.move.line", "account.tax"} <= models
@pytest.mark.asyncio
async def test_uses_corrected_invoice_from_review(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
src = _write_invoice(tmp_path)
responses = _responses(0.95)
responses["invoice review assistant"]["corrected_invoice"] = {
**EXTRACTION,
"invoice_number": "INV-999",
}
_install_fake(monkeypatch, responses)
await cli.process_single(src, tmp_path / "out", Settings())
xml = (tmp_path / "out" / "invoice.xml").read_text(encoding="utf-8")
assert "<field name=\"ref\">INV-999</field>" in xml
@pytest.mark.asyncio
async def test_process_batch(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
_install_fake(monkeypatch, _responses(0.95))
(tmp_path / "a.jpg").touch()
Image.new("RGB", (10, 10), "white").save(tmp_path / "b.jpg")
results = await cli.process_batch(tmp_path, tmp_path / "out", Settings())
assert len(results) == 1
assert (tmp_path / "out" / "b.xml").exists()