unsloth/studio/backend/tests/test_chat_document_routes.py
danielhanchen 3851a543d8 Studio: contained chat document extraction with fast adaptive parsing
Adds document extraction to the chat composer with a small footprint that
reuses the existing RAG preview UI and is fast by default.

Backend
- Adaptive PDF extraction: born-digital pages produce layout-aware Markdown
  via pymupdf4llm and render no page images, so a text PDF issues no VLM
  calls. Only pages without a text layer are detected as scanned and
  rendered for OCR.
- Scanned pages are transcribed (not summarized) through the already loaded
  vision model over /v1/chat/completions. No dedicated OCR model is loaded
  and the chat model is never swapped out.
- Adaptive render DPI (120, env override) and bounded async caption
  concurrency (2 local, 3 gguf, env override).
- /chat/document-support and /chat/extract-document endpoints with NDJSON
  streaming progress, cancellation, multipart size guards, and token-budget
  truncation.

Frontend
- Reuses the existing RAG DocumentPreviewSheet and MarkdownPreview to render
  an extracted document inline (a new markdown preview target), instead of a
  separate preview panel.
- Extraction uses whatever model is loaded; there is no OCR model picker,
  cross-tab lock, or custom-code consent step.
- Compact document chips in the composer and transcript; image data is
  stripped from persisted attachments.
- Document settings expose a mode (fast text, auto, scanned), a caption
  toggle, a token budget, and concurrency. Unknown settings keys are ignored.

Adds backend tests for the adaptive path, the support probe, NDJSON
streaming, cancellation, error mapping, and the scanned-page dedup.
2026-06-25 12:28:33 +00:00

775 lines
26 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
from __future__ import annotations
import io
import sys
import threading
from types import ModuleType, SimpleNamespace
import pytest
pytest.importorskip("fastapi", reason = "route helper tests require FastAPI")
from fastapi import FastAPI, HTTPException # noqa: E402
from fastapi.testclient import TestClient # noqa: E402
from starlette.datastructures import Headers # noqa: E402
import core.chat.document_extractor as extractor # noqa: E402
from core.chat.vlm_capability import VlmCapability # noqa: E402
from routes import inference as route # noqa: E402
class _ChunkedUpload:
def __init__(self, chunks: list[bytes]) -> None:
self._chunks = list(chunks)
async def read(self, _size: int = -1) -> bytes:
if not self._chunks:
return b""
return self._chunks.pop(0)
class _FakeRequest:
def __init__(self, headers: dict[str, str]) -> None:
self.headers = headers
class _FakeStreamingRequest:
def __init__(
self,
chunks: list[bytes],
headers: Headers | None = None,
) -> None:
self._chunks = list(chunks)
self.headers = headers or Headers({})
async def stream(self):
for chunk in self._chunks:
yield chunk
def make_extract_result(**overrides):
"""Fake _extract_document result with sensible defaults; pass overrides per test."""
fields = {
"markdown": "# Doc\n",
"page_count": 1,
"tokens_est": 2,
"figures": [],
"describe_skipped_reason": None,
"vlm_source": "none",
"vlm_model": None,
"warnings": [],
}
fields.update(overrides)
return SimpleNamespace(**fields)
def _make_app(
monkeypatch,
fake_extract = None,
*,
detect_vlm = None,
llama_backend = None,
):
"""FastAPI test client with the document-extraction seams stubbed. `detect_vlm`
overrides the probe (default: no model); `llama_backend` stubs the GGUF backend."""
app = FastAPI()
app.dependency_overrides[route.get_current_subject] = lambda: "test-user"
app.include_router(route.studio_router, prefix = "/api/inference")
monkeypatch.setattr(route, "_DOCUMENT_EXTRACTION_AVAILABLE", True)
# CI may lack the optional pdf/docx parsers (501 before the behavioural
# checks run); report all formats available. Parser-missing tests patch back.
monkeypatch.setattr(
route,
"_document_parser_support",
lambda: {"pdf": True, "docx": True, "html": True, "text": True},
)
monkeypatch.setattr(route, "_document_parser_unavailable_reasons", lambda: {})
monkeypatch.setattr(
route,
"_extract_self_base_url",
lambda _request: "http://127.0.0.1:8000",
)
cap = detect_vlm if detect_vlm is not None else VlmCapability.none("no model loaded")
monkeypatch.setattr(route, "_detect_loaded_vlm", lambda *_args, **_kwargs: cap)
if fake_extract is not None:
monkeypatch.setattr(route, "_extract_document", fake_extract)
if llama_backend is not None:
monkeypatch.setattr(route, "get_llama_cpp_backend", lambda: llama_backend)
return TestClient(app)
def test_reject_oversized_content_length_allows_missing_header() -> None:
route._reject_oversized_content_length(_FakeRequest({}))
def test_reject_oversized_content_length_rejects_large_request() -> None:
max_request_bytes = route._EXTRACT_MAX_BYTES + route._EXTRACT_MULTIPART_OVERHEAD_BYTES + 1
with pytest.raises(HTTPException) as exc_info:
route._reject_oversized_content_length(
_FakeRequest({"content-length": str(max_request_bytes)})
)
assert exc_info.value.status_code == 413
@pytest.mark.asyncio
async def test_read_upload_limited_rejects_streaming_overflow() -> None:
upload = _ChunkedUpload([b"a" * 4, b"b" * 4, b"c"])
with pytest.raises(HTTPException) as exc_info:
await route._read_upload_limited(upload, max_bytes = 8)
assert exc_info.value.status_code == 413
@pytest.mark.asyncio
async def test_read_multipart_form_limited_rejects_streaming_overflow() -> None:
boundary = "studio-boundary"
body = (
(
f"--{boundary}\r\n"
'Content-Disposition: form-data; name="file"; filename="doc.md"\r\n'
"Content-Type: text/markdown\r\n"
"\r\n"
).encode()
+ b"a" * 32
+ f"\r\n--{boundary}--\r\n".encode()
)
request = _FakeStreamingRequest(
[body[:16], body[16:]],
Headers({"Content-Type": f"multipart/form-data; boundary={boundary}"}),
)
with pytest.raises(HTTPException) as exc_info:
await route._read_multipart_form_limited(request, max_bytes = 16)
assert exc_info.value.status_code == 413
def test_document_extraction_exports_are_available_to_routes() -> None:
assert route._DOCUMENT_EXTRACTION_AVAILABLE is True
assert route._extract_document is not None
assert route._DOCUMENT_EXTRACT_CONCURRENCY >= 1
assert route._DOC_SUFFIX_OK
assert ".pdf" in route._DOC_SUFFIX_OK
assert route._drain_doc_future_exception is extractor._drain_future_exception
def test_extract_process_zero_queue_wait_admits_available_slot(
monkeypatch: pytest.MonkeyPatch,
) -> None:
class FakeQueue:
def __init__(self, *, maxsize: int) -> None:
assert maxsize == 1
def get(self, *, timeout: float):
assert timeout > 0
return ("ok", ("plain text", [], 0, 0, 0))
def close(self) -> None:
pass
def join_thread(self) -> None:
pass
class FakeProcess:
exitcode = 0
def start(self) -> None:
pass
def is_alive(self) -> bool:
return False
def join(self, _timeout: float) -> None:
pass
def terminate(self) -> None:
raise AssertionError("process should not be terminated")
def kill(self) -> None:
raise AssertionError("process should not be killed")
class FakeContext:
def Queue(self, *, maxsize: int) -> FakeQueue: # noqa: N802 - mirrors mp API
return FakeQueue(maxsize = maxsize)
def Process(self, *, target, args, daemon: bool) -> FakeProcess: # noqa: N802
assert target is extractor._run_extract_worker
assert args[1] == b"plain text"
assert args[2] == "sample.txt"
assert daemon is True
return FakeProcess()
monkeypatch.setattr(extractor, "_EXTRACT_QUEUE_WAIT_SECONDS", 0.0)
monkeypatch.setattr(
extractor,
"_EXTRACT_SEMAPHORE",
threading.BoundedSemaphore(1),
)
monkeypatch.setattr(
extractor.multiprocessing,
"get_context",
lambda _method: FakeContext(),
)
assert extractor._run_extract_process_sync(
b"plain text",
"sample.txt",
{"extract_images": False},
"text/plain",
5,
) == ("plain text", [], 0, 0, 0)
@pytest.mark.parametrize(
("value", "expected"),
[
(None, True),
("", True),
("yes", True),
("OFF", False),
("0", False),
],
)
def test_parse_bool_form_accepts_known_tokens(value, expected) -> None:
assert route._parse_bool_form(value, default = True, field = "flag") is expected
def test_describe_images_form_field_missing_defaults_to_off() -> None:
"""When describe_images is absent/empty the server default must be False."""
assert route._parse_bool_form(None, default = False, field = "describe_images") is False
assert route._parse_bool_form("", default = False, field = "describe_images") is False
def test_parse_bool_form_rejects_unknown_token() -> None:
with pytest.raises(HTTPException) as exc_info:
route._parse_bool_form("bogus", default = True, field = "describe_images")
assert exc_info.value.status_code == 400
assert "describe_images" in exc_info.value.detail
def test_truncate_markdown_caps_returned_payload() -> None:
markdown = "word " * 2000
clipped, tokens_est, warning = route._truncate_markdown_to_token_budget(
markdown,
token_budget = 1000,
original_tokens_est = len(markdown) // 4,
)
assert len(clipped) < len(markdown)
assert tokens_est == len(clipped) // 4
assert warning and "truncated" in warning
def test_parse_int_form_defaults_invalid_and_clamps_bounds() -> None:
assert route._parse_int_form("bogus", default = 40, lo = 0, hi = 200) == 40
assert route._parse_int_form("-1", default = 40, lo = 0, hi = 200) == 0
assert route._parse_int_form("999", default = 40, lo = 0, hi = 200) == 200
assert route._parse_int_form("999999", default = 40, lo = 0) == 999999
def test_preflight_pdf_page_count_uses_pypdf(monkeypatch: pytest.MonkeyPatch) -> None:
class FakePdfReader:
def __init__(self, _stream, *, strict: bool) -> None:
assert strict is False
self.is_encrypted = False
self.pages = [object(), object(), object()]
fake_pypdf = ModuleType("pypdf")
fake_pypdf.PdfReader = FakePdfReader
monkeypatch.setitem(sys.modules, "pypdf", fake_pypdf)
assert route._preflight_pdf_page_count(b"%PDF", "paper.pdf", "application/pdf") == 3
def test_preflight_pdf_page_count_falls_back_to_pymupdf(monkeypatch: pytest.MonkeyPatch) -> None:
class BrokenPdfReader:
def __init__(self, _stream, *, strict: bool) -> None:
raise ValueError("xref is odd")
class FakeDocument:
is_encrypted = False
needs_pass = False
def __len__(self) -> int:
return 4
def close(self) -> None:
pass
fake_pypdf = ModuleType("pypdf")
fake_pypdf.PdfReader = BrokenPdfReader
monkeypatch.setitem(sys.modules, "pypdf", fake_pypdf)
fake_pymupdf = ModuleType("pymupdf")
fake_pymupdf.open = lambda *, stream, filetype: FakeDocument()
monkeypatch.setitem(sys.modules, "pymupdf", fake_pymupdf)
assert route._preflight_pdf_page_count(b"%PDF", "paper.pdf", "application/pdf") == 4
def test_preflight_pdf_page_count_skips_non_pdf() -> None:
assert route._preflight_pdf_page_count(b"text", "notes.md", "text/markdown") is None
def test_extract_document_endpoint_streams_ndjson_with_caption_progress(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""When the client sends `Accept: application/x-ndjson`, the
endpoint streams progress events plus a final `{stage:"result"}`."""
import json as _json
async def fake_extract_document(*_args, **kwargs):
# Parsing event, two captioning events, then a minimal result.
progress_cb = kwargs.get("progress_cb")
if progress_cb is not None:
await progress_cb({"stage": "parsing"})
await progress_cb(
{
"stage": "captioning",
"current": 1,
"total": 2,
"page": 1,
"total_pages": 3,
}
)
await progress_cb(
{
"stage": "captioning",
"current": 2,
"total": 2,
"page": 2,
"total_pages": 3,
}
)
return make_extract_result(markdown = "# Stream\n", page_count = 3, tokens_est = 5)
client = _make_app(monkeypatch, fake_extract = fake_extract_document)
response = client.post(
"/api/inference/chat/extract-document",
headers = {
"Authorization": "Bearer test-token",
"Accept": "application/x-ndjson",
},
data = {"describe_images": "false"},
files = {"file": ("sample.md", b"# Stream\n", "text/markdown")},
)
assert response.status_code == 200
assert response.headers["content-type"].startswith("application/x-ndjson")
events = [_json.loads(line) for line in response.text.splitlines() if line.strip()]
stages = [e.get("stage") for e in events]
assert "parsing" in stages
captioning_events = [e for e in events if e.get("stage") == "captioning"]
assert len(captioning_events) >= 2
assert captioning_events[0]["current"] == 1
assert captioning_events[0]["total"] == 2
assert captioning_events[0]["page"] == 1
assert captioning_events[0]["total_pages"] == 3
assert events[-1]["stage"] == "result"
assert events[-1]["data"]["markdown"] == "# Stream\n"
assert events[-1]["data"]["page_count"] == 3
def test_extract_document_endpoint_accepts_multipart_smoke(monkeypatch: pytest.MonkeyPatch) -> None:
captured: dict[str, object] = {}
async def fake_extract_document(*_args, **kwargs):
captured.update(kwargs)
return make_extract_result(markdown = "# Smoke\n")
client = _make_app(monkeypatch, fake_extract = fake_extract_document)
response = client.post(
"/api/inference/chat/extract-document",
headers = {"Authorization": "Bearer test-token"},
data = {
"describe_images": "false",
"max_figures": "12345",
"max_visual_payloads": "222",
},
files = {"file": ("sample.md", b"# Smoke\n", "text/markdown")},
)
assert response.status_code == 200
assert response.json()["markdown"] == "# Smoke\n"
assert response.json()["truncated"] is False
assert captured["authorization_header"] == "Bearer test-token"
assert captured["content_type"] == "text/markdown"
assert captured["max_figures"] == 12345
# The route clamps visual payloads to the advertised cap.
assert captured["max_visual_payloads"] == route._MAX_DOCUMENT_VISUAL_PAYLOADS == 10
def test_extract_document_endpoint_does_not_globally_gate_on_pdf_backend(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def fake_extract_document(*_args, **_kwargs):
return make_extract_result(markdown = "# Text\n")
client = _make_app(monkeypatch, fake_extract = fake_extract_document)
monkeypatch.setattr(route, "_DOCUMENT_EXTRACTION_AVAILABLE", False)
response = client.post(
"/api/inference/chat/extract-document",
files = {"file": ("sample.md", b"# Text\n", "text/markdown")},
)
assert response.status_code == 200
assert response.json()["markdown"] == "# Text\n"
def test_extract_document_endpoint_uses_llama_api_key_for_gguf_captions(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, object] = {}
async def fake_extract_document(*_args, **kwargs):
captured.update(kwargs)
return make_extract_result(
markdown = "# Smoke\n",
vlm_source = "gguf",
vlm_model = "vision.gguf",
)
client = _make_app(
monkeypatch,
fake_extract = fake_extract_document,
detect_vlm = VlmCapability(
is_vlm = True,
endpoint_url = "http://127.0.0.1:8080",
model_name = "vision.gguf",
source = "gguf",
),
llama_backend = SimpleNamespace(api_key = "llama-secret"),
)
response = client.post(
"/api/inference/chat/extract-document",
headers = {"Authorization": "Bearer studio-token"},
data = {"describe_images": "true"},
files = {"file": ("sample.md", b"# Smoke\n", "text/markdown")},
)
assert response.status_code == 200
assert captured["authorization_header"] == "Bearer llama-secret"
@pytest.mark.parametrize(
("make_exc", "alias_attr", "filename", "content", "mime", "data", "status", "detail"),
[
pytest.param(
lambda: route._DocumentExtractionBusy("document extraction is busy"),
None,
"sample.md",
b"# Smoke\n",
"text/markdown",
None,
503,
None,
id = "busy-503",
),
pytest.param(
lambda: ValueError("Unsupported file type: upload.bin"),
None,
"upload.bin",
b"hello",
"text/plain",
None,
415,
"Unsupported file type",
id = "value-error-415",
),
pytest.param(
lambda: ValueError("Could not parse document"),
None,
"upload.md",
b"# hello",
"text/markdown",
None,
400,
"Could not parse document",
id = "parse-value-error-400",
),
pytest.param(
lambda: extractor.DocumentExtractionTimeout("timed out"),
"_DocumentExtractionTimeout",
"doc.md",
b"# Doc\n",
"text/markdown",
{"describe_images": "false"},
504,
"120",
id = "timeout-504",
),
pytest.param(
lambda: route._DocumentExtractionEncrypted("Encrypted PDF"),
None,
"doc.md",
b"# Doc\n",
"text/markdown",
{"describe_images": "false"},
422,
"Encrypted PDF",
id = "encrypted-422",
),
pytest.param(
lambda: route._DocumentExtractionCancelled("cancelled"),
None,
"doc.md",
b"# Doc\n",
"text/markdown",
{"describe_images": "false"},
499,
"Client closed request",
id = "cancelled-499",
),
pytest.param(
lambda: extractor.DocumentExtractionUnavailable("document extraction is not installed"),
"_DocumentExtractionUnavailable",
"doc.md",
b"# Doc\n",
"text/markdown",
{"describe_images": "false"},
501,
None,
id = "unavailable-501",
),
],
)
def test_extract_document_endpoint_maps_extraction_errors(
monkeypatch, make_exc, alias_attr, filename, content, mime, data, status, detail
) -> None:
"""Each extractor failure maps to a stable HTTP status (and detail)."""
if alias_attr is not None:
# Re-bind the route's exception alias to the class the fake raises; no-op
# when the import succeeded, but pins the mapping to the real type.
monkeypatch.setattr(route, alias_attr, type(make_exc()))
async def fake_extract(*_args, **_kwargs):
raise make_exc()
client = _make_app(monkeypatch, fake_extract = fake_extract)
response = client.post(
"/api/inference/chat/extract-document",
data = data,
files = {"file": (filename, content, mime)},
)
assert response.status_code == status
if detail is not None:
actual = response.json()["detail"]
# The 499 detail is a fixed constant; pin it exactly.
assert actual == detail if status == 499 else detail in actual
def test_extract_document_endpoint_reports_truncated(monkeypatch: pytest.MonkeyPatch) -> None:
async def fake_extract_document(*_args, **_kwargs):
return make_extract_result(markdown = "word " * 2000, tokens_est = 2500)
client = _make_app(monkeypatch, fake_extract_document)
response = client.post(
"/api/inference/chat/extract-document",
data = {"token_budget": "1000"},
files = {"file": ("sample.md", b"# Smoke\n", "text/markdown")},
)
assert response.status_code == 200
assert response.json()["truncated"] is True
assert any("truncated" in w.lower() for w in response.json()["warnings"])
def test_extract_document_endpoint_sanitizes_extract_errors(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def fake_extract_document(*_args, **_kwargs):
raise RuntimeError("local path C:/secret/model/cache leaked")
client = _make_app(monkeypatch, fake_extract = fake_extract_document)
response = client.post(
"/api/inference/chat/extract-document",
files = {"file": ("sample.md", b"# Smoke\n", "text/markdown")},
)
assert response.status_code == 500
assert response.json()["detail"] == "Extraction failed"
def test_document_support_reports_format_parser_availability(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client = _make_app(monkeypatch)
monkeypatch.setattr(
route,
"_document_parser_support",
lambda: {"pdf": False, "docx": True, "text": True},
)
monkeypatch.setattr(
route,
"_document_parser_unavailable_reasons",
lambda: {"pdf": "PDF extraction requires pymupdf and pymupdf4llm."},
)
response = client.get("/api/inference/chat/document-support")
assert response.status_code == 200
body = response.json()
assert body["extraction_available"] is True
assert body["max_extract_concurrency"] == route._DOCUMENT_EXTRACT_CONCURRENCY
assert body["format_support"]["pdf"] is False
assert body["format_support"]["text"] is True
assert "pymupdf" in body["unavailable_formats"]["pdf"]
def test_document_support_maps_vlm_probe_bug_to_no_vlm(monkeypatch: pytest.MonkeyPatch) -> None:
client = _make_app(monkeypatch)
monkeypatch.setattr(
route,
"_detect_loaded_vlm",
lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("boom")),
)
response = client.get("/api/inference/chat/document-support")
assert response.status_code == 200
body = response.json()
assert body["extraction_available"] is True
assert body["vlm"]["is_vlm"] is False
assert "RuntimeError" in body["vlm"]["reason"]
def test_endpoint_rejects_unavailable_pdf_parser_before_extraction(
monkeypatch: pytest.MonkeyPatch,
) -> None:
async def fail_extract(*_args, **_kwargs):
raise AssertionError("unavailable parser should be rejected before extraction")
client = _make_app(monkeypatch, fake_extract = fail_extract)
monkeypatch.setattr(route, "_document_parser_support", lambda: {"pdf": False})
monkeypatch.setattr(
route,
"_document_parser_unavailable_reasons",
lambda: {"pdf": "PDF extraction requires pymupdf and pymupdf4llm."},
)
response = client.post(
"/api/inference/chat/extract-document",
files = {"file": ("paper.pdf", b"%PDF", "application/pdf")},
)
assert response.status_code == 501
assert "pymupdf" in response.json()["detail"]
def test_413_message_does_not_mention_roadmap(monkeypatch: pytest.MonkeyPatch) -> None:
"""The 413 detail must not promise background job support."""
monkeypatch.setattr(route, "_EXTRACT_MAX_PAGES_INLINE", 1)
class FakePdfReader:
def __init__(self, _stream, *, strict: bool) -> None:
self.is_encrypted = False
self.pages = [object(), object(), object()] # 3 pages > cap of 1
fake_pypdf = ModuleType("pypdf")
fake_pypdf.PdfReader = FakePdfReader
monkeypatch.setitem(sys.modules, "pypdf", fake_pypdf)
client = _make_app(monkeypatch)
response = client.post(
"/api/inference/chat/extract-document",
files = {"file": ("paper.pdf", b"%PDF", "application/pdf")},
)
assert response.status_code == 413
detail = response.json()["detail"]
assert "roadmap" not in detail.lower()
assert "split" in detail.lower() or "smaller" in detail.lower()
def test_figures_are_serialized_via_pydantic_model(monkeypatch: pytest.MonkeyPatch) -> None:
"""ExtractedFigureModel(**asdict(fig)) makes a dataclass field-name
mismatch a validation error, not a silently-wrong response."""
from core.chat.document_extractor import ExtractedFigure
async def fake_extract(*_args, **_kwargs):
return make_extract_result(
tokens_est = 3,
figures = [
ExtractedFigure(
id = "fig-0",
page = 1,
caption = "A chart",
error = None,
kind = "figure",
image_mime = None,
image_base64 = None,
image_width = None,
image_height = None,
)
],
)
client = _make_app(monkeypatch, fake_extract = fake_extract)
response = client.post(
"/api/inference/chat/extract-document",
data = {"describe_images": "false"},
files = {"file": ("doc.md", b"# Doc\n", "text/markdown")},
)
assert response.status_code == 200
figs = response.json()["figures"]
assert len(figs) == 1
assert figs[0]["id"] == "fig-0"
assert figs[0]["caption"] == "A chart"
def test_real_encrypted_pdf_preflight_returns_422(monkeypatch: pytest.MonkeyPatch) -> None:
pypdf = pytest.importorskip("pypdf")
writer = pypdf.PdfWriter()
writer.add_blank_page(width = 72, height = 72)
writer.encrypt("secret")
encrypted = io.BytesIO()
writer.write(encrypted)
async def fail_extract(*_args, **_kwargs):
raise AssertionError("encrypted PDFs should fail during preflight")
client = _make_app(monkeypatch, fake_extract = fail_extract)
response = client.post(
"/api/inference/chat/extract-document",
data = {"describe_images": "false"},
files = {
"file": ("locked.pdf", encrypted.getvalue(), "application/pdf"),
},
)
assert response.status_code == 422
assert "Encrypted PDF" in response.json()["detail"]
def test_endpoint_returns_415_for_unsupported_mime(monkeypatch: pytest.MonkeyPatch) -> None:
client = _make_app(monkeypatch)
response = client.post(
"/api/inference/chat/extract-document",
files = {"file": ("image.png", b"\x89PNG", "image/png")},
)
assert response.status_code == 415
def test_endpoint_returns_400_for_empty_file(monkeypatch: pytest.MonkeyPatch) -> None:
client = _make_app(monkeypatch)
response = client.post(
"/api/inference/chat/extract-document",
files = {"file": ("empty.md", b"", "text/markdown")},
)
assert response.status_code == 400
def test_endpoint_returns_415_for_pptx(monkeypatch: pytest.MonkeyPatch) -> None:
client = _make_app(monkeypatch)
response = client.post(
"/api/inference/chat/extract-document",
files = {
"file": (
"deck.pptx",
b"PK\x03\x04",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
)
},
)
assert response.status_code == 415