"""Consumer contract: nextcloud-mcp-server -> embedding-gateway batch OCR (Deck #332). When ``DOCUMENT_OCR_MODE=batch`` the ingest worker drives the gateway's async Batch OCR routes via :class:`GatewayBatchOcrClient` (``embedding/gateway_batch_client.py``): - ``POST /v1/ocr/batch`` — submit one document, returns a namespaced ``job_id``. - ``GET /v1/ocr/batch/{job_id}`` — poll; pending until terminal, then per-page markdown (succeeded) or an error (failed). This pact pins the request/response shapes the consumer depends on, for the ``astrolabe-cloud-gateway`` provider (whose verification job lives in astrolabe-cloud-website, services/embedding-gateway). Only the fields the client actually reads are asserted, so the contract stays robust to additive response changes (the gateway's ``OcrBatchJobOut`` carries more fields — total/completed/ counts — that the single-document client ignores). The gateway is unauthenticated today, so no bearer is sent (matching the M2M-optional ``GatewayBatchOcrClient``). See ADR-029 for the contract-testing architecture. """ import base64 import pytest from pact import match from nextcloud_mcp_server.embedding.gateway_batch_client import GatewayBatchOcrClient pytestmark = pytest.mark.contract _MODEL = "mistral/mistral-ocr-latest" # A small, valid base64 PDF payload — the gateway base64-decodes + size-checks # the document, so the replayed request must carry decodable bytes. _PDF_B64 = base64.b64encode(b"%PDF-1.4 contract test").decode("ascii") async def test_submit_returns_namespaced_job_id(gateway_consumer_pact): ( gateway_consumer_pact.upon_receiving("a batch OCR submission for one document") .given("the gateway accepts a batch OCR submission") .with_request("POST", "/v1/ocr/batch") .with_body( { "model": _MODEL, "documents": [ { "custom_id": "0", "mime_type": "application/pdf", "document_b64": _PDF_B64, } ], }, content_type="application/json", ) .will_respond_with(202) .with_body( { # Namespaced "/" — the only field submit() reads. "job_id": match.regex("mistral/job-abc", regex=r"[^/]+/.+"), "status": "pending", }, content_type="application/json", ) ) with gateway_consumer_pact.serve() as srv: client = GatewayBatchOcrClient(str(srv.url), _MODEL) job_id = await client.submit( b"%PDF-1.4 contract test", "application/pdf", custom_id="0" ) assert job_id == "mistral/job-abc" async def test_poll_pending(gateway_consumer_pact): ( gateway_consumer_pact.upon_receiving("a poll for a still-running batch OCR job") .given("a pending batch OCR job mistral/job-pending exists") .with_request("GET", "/v1/ocr/batch/mistral/job-pending") .will_respond_with(200) .with_body({"status": "pending"}, content_type="application/json") ) with gateway_consumer_pact.serve() as srv: result = await GatewayBatchOcrClient(str(srv.url), _MODEL).poll( "mistral/job-pending" ) assert result.is_pending async def test_poll_succeeded_returns_pages(gateway_consumer_pact): ( gateway_consumer_pact.upon_receiving("a poll for a succeeded batch OCR job") .given("a succeeded batch OCR job mistral/job-done exists") .with_request("GET", "/v1/ocr/batch/mistral/job-done") .will_respond_with(200) .with_body( { "status": "succeeded", "results": [ { "custom_id": "0", "pages": [ { "index": match.integer(0), "markdown": match.string("# Page one"), } ], } ], }, content_type="application/json", ) ) with gateway_consumer_pact.serve() as srv: result = await GatewayBatchOcrClient(str(srv.url), _MODEL).poll( "mistral/job-done" ) assert result.is_succeeded assert result.pages == [(0, "# Page one")] async def test_poll_failed_surfaces_error(gateway_consumer_pact): ( gateway_consumer_pact.upon_receiving("a poll for a failed batch OCR job") .given("a failed batch OCR job mistral/job-failed exists") .with_request("GET", "/v1/ocr/batch/mistral/job-failed") .will_respond_with(200) .with_body( {"status": "failed", "error": match.string("batch job failed")}, content_type="application/json", ) ) with gateway_consumer_pact.serve() as srv: result = await GatewayBatchOcrClient(str(srv.url), _MODEL).poll( "mistral/job-failed" ) assert result.is_failed assert result.error == "batch job failed"