Files
jundot--omlx/tests/test_markitdown_integration.py
wehub-resource-sync e9a2f726c9
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:29:51 +08:00

982 lines
30 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Tests for the MarkItDown integration."""
from __future__ import annotations
import asyncio
import base64
import logging
import sys
import types
import warnings
from dataclasses import dataclass
from unittest.mock import patch
import pytest
from fastapi.testclient import TestClient
from starlette.responses import StreamingResponse
import omlx.server as server_module
from omlx.api.markitdown import (
MARKITDOWN_EMPTY_PDF_MESSAGE,
MARKITDOWN_MODEL_ID,
MarkItDownFile,
MarkItDownRequestError,
convert_file_to_markdown,
parse_file_part,
preprocess_markitdown_file_parts,
preprocess_markitdown_file_parts_async,
quiet_pdf_parser_loggers,
)
from omlx.api.markitdown_pdf_fallback import (
convert_pdf_with_ocr_engine,
resolve_pdf_ocr_model,
stream_pdf_with_ocr_engine,
)
from omlx.api.openai_models import ChatCompletionRequest, Message
from omlx.engine_pool import EngineEntry, EnginePool
from omlx.server import ServerState, app
from omlx.settings import GlobalSettings
def _data_uri(payload: bytes = b"doc", mime_type: str = "application/pdf") -> str:
encoded = base64.b64encode(payload).decode("ascii")
return f"data:{mime_type};base64,{encoded}"
def _file_part(
filename: str = "sample.pdf",
data: str | None = None,
mime_type: str = "application/pdf",
) -> dict:
return {
"type": "file",
"file": {
"filename": filename,
"mime_type": mime_type,
"file_data": data or _data_uri(mime_type=mime_type),
},
}
class _EmptyPool:
def get_status(self) -> dict:
return {
"final_ceiling": 0,
"current_model_memory": 0,
"model_count": 0,
"loaded_count": 0,
"models": [],
}
def _settings_with_markitdown_model() -> GlobalSettings:
settings = GlobalSettings()
settings.integrations.markitdown_expose_model = True
return settings
def test_openai_models_hides_markitdown_by_default():
state = ServerState()
state.engine_pool = _EmptyPool()
state.global_settings = GlobalSettings()
with patch("omlx.server._server_state", state):
client = TestClient(app, raise_server_exceptions=False)
response = client.get("/v1/models")
assert response.status_code == 200
ids = [m["id"] for m in response.json()["data"]]
assert MARKITDOWN_MODEL_ID not in ids
def test_openai_models_includes_markitdown_when_exposed():
state = ServerState()
state.engine_pool = _EmptyPool()
state.global_settings = _settings_with_markitdown_model()
with patch("omlx.server._server_state", state):
client = TestClient(app, raise_server_exceptions=False)
response = client.get("/v1/models")
assert response.status_code == 200
ids = [m["id"] for m in response.json()["data"]]
assert "MarkItDown" in ids
def test_openai_models_hides_markitdown_when_disabled():
state = ServerState()
state.engine_pool = _EmptyPool()
state.global_settings = _settings_with_markitdown_model()
state.global_settings.integrations.markitdown_enabled = False
with patch("omlx.server._server_state", state):
client = TestClient(app, raise_server_exceptions=False)
response = client.get("/v1/models")
assert response.status_code == 200
ids = [m["id"] for m in response.json()["data"]]
assert MARKITDOWN_MODEL_ID not in ids
def test_openai_models_hides_markitdown_when_not_exposed():
state = ServerState()
state.engine_pool = _EmptyPool()
state.global_settings = GlobalSettings()
state.global_settings.integrations.markitdown_expose_model = False
with patch("omlx.server._server_state", state):
client = TestClient(app, raise_server_exceptions=False)
response = client.get("/v1/models")
assert response.status_code == 200
ids = [m["id"] for m in response.json()["data"]]
assert MARKITDOWN_MODEL_ID not in ids
def test_markitdown_chat_completion_converts_file(monkeypatch):
state = ServerState()
state.engine_pool = _EmptyPool()
state.global_settings = _settings_with_markitdown_model()
def fake_convert(file: MarkItDownFile, **kwargs) -> str:
assert file.filename == "sample.pdf"
return "# Converted"
monkeypatch.setattr("omlx.api.markitdown.convert_file_to_markdown", fake_convert)
with patch("omlx.server._server_state", state):
client = TestClient(app, raise_server_exceptions=False)
response = client.post(
"/v1/chat/completions",
json={
"model": MARKITDOWN_MODEL_ID,
"messages": [{"role": "user", "content": [_file_part()]}],
},
)
assert response.status_code == 200
content = response.json()["choices"][0]["message"]["content"]
assert "## Attached file: sample.pdf" in content
assert "# Converted" in content
def test_markitdown_chat_completion_uses_latest_user_turn(monkeypatch):
state = ServerState()
state.engine_pool = _EmptyPool()
state.global_settings = _settings_with_markitdown_model()
def fake_convert(file: MarkItDownFile, **kwargs) -> str:
return f"# Converted {file.filename}"
monkeypatch.setattr("omlx.api.markitdown.convert_file_to_markdown", fake_convert)
with patch("omlx.server._server_state", state):
client = TestClient(app, raise_server_exceptions=False)
response = client.post(
"/v1/chat/completions",
json={
"model": MARKITDOWN_MODEL_ID,
"messages": [
{"role": "user", "content": [_file_part("file1.pdf")]},
{
"role": "assistant",
"content": (
"## Attached file: file1.pdf\n\n# Converted file1.pdf"
),
},
{"role": "user", "content": [_file_part("file2.pdf")]},
],
},
)
assert response.status_code == 200
content = response.json()["choices"][0]["message"]["content"]
assert "file2.pdf" in content
assert "file1.pdf" not in content
def test_markitdown_chat_completion_disabled_returns_404():
state = ServerState()
state.engine_pool = _EmptyPool()
state.global_settings = GlobalSettings()
state.global_settings.integrations.markitdown_enabled = False
with patch("omlx.server._server_state", state):
client = TestClient(app, raise_server_exceptions=False)
response = client.post(
"/v1/chat/completions",
json={
"model": MARKITDOWN_MODEL_ID,
"messages": [{"role": "user", "content": "hello"}],
},
)
assert response.status_code == 404
assert "markitdown" in response.json()["error"]["message"].lower()
def test_markitdown_chat_completion_hidden_model_returns_404():
state = ServerState()
state.engine_pool = _EmptyPool()
state.global_settings = GlobalSettings()
state.global_settings.integrations.markitdown_expose_model = False
with patch("omlx.server._server_state", state):
client = TestClient(app, raise_server_exceptions=False)
response = client.post(
"/v1/chat/completions",
json={
"model": MARKITDOWN_MODEL_ID,
"messages": [{"role": "user", "content": "hello"}],
},
)
assert response.status_code == 404
assert "markitdown" in response.json()["error"]["message"].lower()
def test_markitdown_stream_response_starts_before_conversion(monkeypatch):
state = ServerState()
state.engine_pool = _EmptyPool()
state.global_settings = _settings_with_markitdown_model()
started = False
async def fake_stream_messages(*args, **kwargs):
nonlocal started
started = True
yield "Converted markdown"
monkeypatch.setattr(
server_module,
"stream_messages_to_markdown_async",
fake_stream_messages,
)
request = ChatCompletionRequest(
model=MARKITDOWN_MODEL_ID,
messages=[{"role": "user", "content": "hello"}],
stream=True,
)
async def exercise():
with patch("omlx.server._server_state", state):
response = await server_module._create_markitdown_chat_completion(
request,
None,
)
assert isinstance(response, StreamingResponse)
assert started is False
iterator = response.body_iterator.__aiter__()
first = await iterator.__anext__()
assert first.startswith("data: ")
assert started is False
role_chunk = await iterator.__anext__()
assert '"role":"assistant"' in role_chunk
assert started is False
content_chunk = await iterator.__anext__()
assert "Converted markdown" in content_chunk
assert started is True
await iterator.aclose()
asyncio.run(exercise())
def test_markitdown_non_stream_response_starts_before_conversion(monkeypatch):
state = ServerState()
state.engine_pool = _EmptyPool()
state.global_settings = _settings_with_markitdown_model()
started = False
async def fake_convert_messages(*args, **kwargs):
nonlocal started
started = True
return "Converted markdown"
monkeypatch.setattr(
server_module,
"convert_messages_to_markdown_async",
fake_convert_messages,
)
request = ChatCompletionRequest(
model=MARKITDOWN_MODEL_ID,
messages=[{"role": "user", "content": "hello"}],
)
async def exercise():
with patch("omlx.server._server_state", state):
response = await server_module._create_markitdown_chat_completion(
request,
None,
)
assert isinstance(response, StreamingResponse)
assert started is False
iterator = response.body_iterator.__aiter__()
first = await iterator.__anext__()
assert first == " "
chunk = await iterator.__anext__()
assert "Converted markdown" in chunk
assert started is True
await iterator.aclose()
asyncio.run(exercise())
def test_preprocess_file_parts_for_llm(monkeypatch):
def fake_convert(file: MarkItDownFile, **kwargs) -> str:
return "Converted text"
monkeypatch.setattr("omlx.api.markitdown.convert_file_to_markdown", fake_convert)
messages = [
Message(
role="user",
content=[
{"type": "text", "text": "Summarize this."},
_file_part("paper.pdf"),
],
)
]
processed = preprocess_markitdown_file_parts(
messages,
global_settings=GlobalSettings(),
)
parts = processed[0].content
assert isinstance(parts, list)
assert parts[0].type == "text"
assert parts[1].type == "text"
assert "## Attached file: paper.pdf" in (parts[1].text or "")
assert "Converted text" in (parts[1].text or "")
def test_preprocess_file_parts_works_when_model_not_exposed(monkeypatch):
def fake_convert(file: MarkItDownFile, **kwargs) -> str:
return "Converted text"
monkeypatch.setattr("omlx.api.markitdown.convert_file_to_markdown", fake_convert)
settings = GlobalSettings()
settings.integrations.markitdown_expose_model = False
processed = preprocess_markitdown_file_parts(
[Message(role="user", content=[_file_part("paper.pdf")])],
global_settings=settings,
)
parts = processed[0].content
assert isinstance(parts, list)
assert parts[0].type == "text"
assert "Converted text" in (parts[0].text or "")
def test_text_and_markdown_file_parts_are_inlined_without_converter(monkeypatch):
called = False
def fake_convert(file: MarkItDownFile, **kwargs) -> str:
nonlocal called
called = True
raise AssertionError("plain text attachments should not use MarkItDown")
monkeypatch.setattr("omlx.api.markitdown.convert_file_to_markdown", fake_convert)
processed = preprocess_markitdown_file_parts(
[
Message(
role="user",
content=[
_file_part(
"notes.txt",
data=_data_uri(b"Plain notes", mime_type="text/plain"),
mime_type="text/plain",
),
_file_part(
"guide.md",
data=_data_uri(b"# Guide", mime_type="text/markdown"),
mime_type="text/markdown",
),
],
)
],
global_settings=GlobalSettings(),
)
parts = processed[0].content
assert isinstance(parts, list)
rendered = "\n".join(part.text or "" for part in parts)
assert "## Attached file: notes.txt" in rendered
assert "Plain notes" in rendered
assert "## Attached file: guide.md" in rendered
assert "# Guide" in rendered
assert called is False
def test_preprocess_file_parts_does_not_create_mixed_content_warning(monkeypatch):
def fake_convert(file: MarkItDownFile, **kwargs) -> str:
return "Converted text"
monkeypatch.setattr("omlx.api.markitdown.convert_file_to_markdown", fake_convert)
messages = [
Message(
role="user",
content=[
{"type": "text", "text": "Summarize this."},
_file_part("paper.pdf"),
],
)
]
processed = preprocess_markitdown_file_parts(
messages,
global_settings=GlobalSettings(),
)
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
processed[0].model_dump()
assert not [
warning
for warning in caught
if "Pydantic serializer warnings" in str(warning.message)
]
def test_preprocess_allows_missing_historical_file_parts(monkeypatch):
def fake_convert(file: MarkItDownFile, **kwargs) -> str:
raise AssertionError("missing historical files should not be converted")
monkeypatch.setattr("omlx.api.markitdown.convert_file_to_markdown", fake_convert)
processed = preprocess_markitdown_file_parts(
[
Message(
role="user",
content=[
{"type": "text", "text": "Summarize this."},
{
"type": "file",
"file": {
"filename": "paper.pdf",
"mime_type": "application/pdf",
"data": "",
},
},
],
),
Message(role="assistant", content="Done."),
Message(role="user", content="Continue."),
],
global_settings=GlobalSettings(),
allow_missing_historical_files=True,
)
parts = processed[0].content
assert isinstance(parts, list)
assert parts[0].type == "text"
assert parts[1].type == "text"
assert "Attached file unavailable: paper.pdf" in (parts[1].text or "")
assert processed[2].content == "Continue."
def test_preprocess_still_rejects_missing_latest_file_part():
with pytest.raises(MarkItDownRequestError, match="file_data"):
preprocess_markitdown_file_parts(
[
Message(
role="user",
content=[
{
"type": "file",
"file": {
"filename": "paper.pdf",
"mime_type": "application/pdf",
"data": "",
},
}
],
)
],
global_settings=GlobalSettings(),
allow_missing_historical_files=True,
)
def test_async_preprocess_allows_missing_historical_file_parts(monkeypatch):
def fake_convert(file: MarkItDownFile, **kwargs) -> str:
raise AssertionError("missing historical files should not be converted")
monkeypatch.setattr("omlx.api.markitdown.convert_file_to_markdown", fake_convert)
async def exercise():
return await preprocess_markitdown_file_parts_async(
[
Message(
role="user",
content=[
{
"type": "file",
"file": {
"filename": "paper.pdf",
"mime_type": "application/pdf",
"data": "",
},
}
],
),
Message(role="assistant", content="Done."),
Message(role="user", content="Continue."),
],
global_settings=GlobalSettings(),
allow_missing_historical_files=True,
)
processed = asyncio.run(exercise())
parts = processed[0].content
assert isinstance(parts, list)
assert parts[0].type == "text"
assert "Attached file unavailable: paper.pdf" in (parts[0].text or "")
def test_server_llm_preprocess_allows_stored_document_placeholders(monkeypatch):
def fake_convert(file: MarkItDownFile, **kwargs) -> str:
raise AssertionError("stored document placeholders should not be converted")
monkeypatch.setattr("omlx.api.markitdown.convert_file_to_markdown", fake_convert)
state = ServerState()
state.engine_pool = _EmptyPool()
state.global_settings = GlobalSettings()
request = ChatCompletionRequest(
model="test-model",
messages=[
Message(
role="user",
content=[
{
"type": "file",
"file": {
"filename": "paper.pdf",
"mime_type": "application/pdf",
"data": "",
},
}
],
),
Message(role="assistant", content="Done."),
Message(role="user", content="Continue."),
],
)
async def exercise():
with patch("omlx.server._server_state", state):
return await server_module._preprocess_markitdown_files_for_llm(request)
processed = asyncio.run(exercise())
parts = processed.messages[0].content
assert isinstance(parts, list)
assert parts[0].type == "text"
assert "Attached file unavailable: paper.pdf" in (parts[0].text or "")
def test_preprocess_file_parts_rejects_when_disabled():
settings = GlobalSettings()
settings.integrations.markitdown_enabled = False
with pytest.raises(MarkItDownRequestError, match="disabled"):
preprocess_markitdown_file_parts(
[Message(role="user", content=[_file_part()])],
global_settings=settings,
)
def test_xlsx_is_rejected_without_pandas_dependency():
part = {
"type": "file",
"file": {
"filename": "sheet.xlsx",
"mime_type": (
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
),
"file_data": _data_uri(),
},
}
with pytest.raises(MarkItDownRequestError, match="Spreadsheet"):
parse_file_part(part, max_file_size_mb=25)
def test_openai_file_data_without_filename_infers_from_data_uri():
parsed = parse_file_part(
{
"type": "file",
"file": {
"file_data": _data_uri(
b"Plain notes",
mime_type="text/plain",
),
},
},
max_file_size_mb=25,
)
assert parsed.filename == "attachment.txt"
assert parsed.mime_type == "text/plain"
assert parsed.data == b"Plain notes"
def test_file_id_is_rejected():
with pytest.raises(MarkItDownRequestError, match="file_id"):
parse_file_part(
{"type": "file", "file": {"file_id": "file_123", "filename": "x.pdf"}},
max_file_size_mb=25,
)
def test_empty_pdf_conversion_logs_warning(monkeypatch, caplog):
@dataclass(frozen=True)
class FakeStreamInfo:
extension: str | None = None
mimetype: str | None = None
filename: str | None = None
class FakeResult:
markdown = ""
class FakeConverter:
def convert_stream(self, stream, stream_info=None):
return FakeResult()
fake_markitdown = types.ModuleType("markitdown")
fake_markitdown.StreamInfo = FakeStreamInfo
monkeypatch.setitem(sys.modules, "markitdown", fake_markitdown)
monkeypatch.setattr("omlx.api.markitdown._converter", FakeConverter())
caplog.set_level("WARNING")
with pytest.raises(MarkItDownRequestError) as exc_info:
convert_file_to_markdown(
MarkItDownFile(
filename="scan.pdf",
mime_type="application/pdf",
data=b"%PDF",
)
)
assert exc_info.value.detail == MARKITDOWN_EMPTY_PDF_MESSAGE
assert "no extractable text" in caplog.text.lower()
def test_pdf_parser_debug_loggers_are_quieted():
logger_names = ("pdfminer", "pdfminer.psparser", "pdfminer.pdfinterp")
original_levels = {name: logging.getLogger(name).level for name in logger_names}
try:
for name in logger_names:
logging.getLogger(name).setLevel(logging.DEBUG)
quiet_pdf_parser_loggers()
for name in logger_names:
assert logging.getLogger(name).level == logging.WARNING
finally:
for name, level in original_levels.items():
logging.getLogger(name).setLevel(level)
def test_ocr_pdf_engine_converts_pages_in_order_limits_concurrency_and_unloads(
monkeypatch,
):
class FakePool:
def __init__(self):
self.engine = FakeEngine()
self.unloaded = []
def resolve_model_id(self, model_id, settings_manager):
return model_id
def get_entry(self, model_id):
return types.SimpleNamespace(
config_model_type="dots_ocr",
engine_type="vlm",
)
def acquire(self, model_id):
assert model_id == "OCR-Model"
pool = self
class Lease:
async def __aenter__(self):
return pool.engine
async def __aexit__(self, exc_type, exc, tb):
return None
return Lease()
async def unload_if_idle_unpinned(self, model_id):
self.unloaded.append(model_id)
return True
active = 0
max_active = 0
class FakeEngine:
async def chat(self, messages, **kwargs):
nonlocal active, max_active
active += 1
max_active = max(max_active, active)
try:
await asyncio.sleep(0.01)
url = messages[0]["content"][0]["image_url"]["url"]
return types.SimpleNamespace(text=f"markdown for {url[-1]}")
finally:
active -= 1
settings = GlobalSettings()
settings.scheduler.max_concurrent_requests = 2
pool = FakePool()
monkeypatch.setattr(
"omlx.api.markitdown_pdf_fallback.render_pdf_pages_to_image_data_uris",
lambda file: ["page1", "page2", "page3"],
)
markdown = asyncio.run(
convert_pdf_with_ocr_engine(
MarkItDownFile(
filename="scan.pdf",
mime_type="application/pdf",
data=b"%PDF",
),
engine_model_id="OCR-Model",
engine_pool=pool,
settings_manager=None,
global_settings=settings,
get_sampling_params=lambda *_args: (
0.0,
1.0,
0,
1.0,
0.0,
0.0,
0.0,
4096,
0.0,
0.1,
),
)
)
assert "### Page 1\n\nmarkdown for 1" in markdown
assert "### Page 2\n\nmarkdown for 2" in markdown
assert "### Page 3\n\nmarkdown for 3" in markdown
assert max_active == 2
assert pool.unloaded == ["OCR-Model"]
def test_ocr_pdf_engine_streams_ready_prefix_in_page_order(monkeypatch):
class FakePool:
def __init__(self):
self.engine = FakeEngine()
def resolve_model_id(self, model_id, settings_manager):
return model_id
def get_entry(self, model_id):
return types.SimpleNamespace(
config_model_type="dots_ocr",
engine_type="vlm",
)
def acquire(self, model_id):
pool = self
class Lease:
async def __aenter__(self):
return pool.engine
async def __aexit__(self, exc_type, exc, tb):
return None
return Lease()
async def unload_if_idle_unpinned(self, model_id):
return True
class FakeEngine:
async def chat(self, messages, **kwargs):
url = messages[0]["content"][0]["image_url"]["url"]
delay_by_page = {"page1": 0.03, "page2": 0.0, "page3": 0.01}
await asyncio.sleep(delay_by_page[url])
return types.SimpleNamespace(text=f"markdown for {url[-1]}")
settings = GlobalSettings()
settings.scheduler.max_concurrent_requests = 3
monkeypatch.setattr(
"omlx.api.markitdown_pdf_fallback.render_pdf_pages_to_image_data_uris",
lambda file: ["page1", "page2", "page3"],
)
async def collect_chunks():
chunks = []
async for chunk in stream_pdf_with_ocr_engine(
MarkItDownFile(
filename="scan.pdf",
mime_type="application/pdf",
data=b"%PDF",
),
engine_model_id="OCR-Model",
engine_pool=FakePool(),
settings_manager=None,
global_settings=settings,
get_sampling_params=lambda *_args: (
0.0,
1.0,
0,
1.0,
0.0,
0.0,
0.0,
4096,
0.0,
0.1,
),
):
chunks.append(chunk)
return chunks
chunks = asyncio.run(collect_chunks())
assert [chunk.split("\n", 1)[0] for chunk in chunks] == [
"### Page 1",
"### Page 2",
"### Page 3",
]
def test_ocr_pdf_engine_rejects_non_ocr_config_model_type():
class FakePool:
def resolve_model_id(self, model_id, settings_manager):
return model_id
def get_entry(self, model_id):
return types.SimpleNamespace(
config_model_type="qwen3_vl",
engine_type="vlm",
)
with pytest.raises(MarkItDownRequestError, match="OCR"):
resolve_pdf_ocr_model(
"plain-model",
engine_pool=FakePool(),
settings_manager=None,
)
def test_ocr_pdf_engine_ignores_model_id_ocr_without_ocr_config_model_type():
class FakePool:
def resolve_model_id(self, model_id, settings_manager):
return model_id
def get_entry(self, model_id):
return types.SimpleNamespace(
model_id="Invoice-OCR",
source_repo_id=None,
config_model_type="qwen2_vl",
engine_type="vlm",
)
with pytest.raises(MarkItDownRequestError, match="config model_type"):
resolve_pdf_ocr_model(
"Invoice-OCR",
engine_pool=FakePool(),
settings_manager=None,
)
def test_async_preprocess_uses_ocr_pdf_processing_engine(monkeypatch):
class FakePool:
def resolve_model_id(self, model_id, settings_manager):
return model_id
def get_entry(self, model_id):
return types.SimpleNamespace(
config_model_type="dots_ocr",
engine_type="vlm",
)
def acquire(self, model_id):
class Lease:
async def __aenter__(self):
return types.SimpleNamespace(
chat=lambda **kwargs: None,
)
async def __aexit__(self, exc_type, exc, tb):
return None
return Lease()
async def fake_convert(file, **kwargs):
assert file.filename == "paper.pdf"
assert kwargs["engine_model_id"] == "OCR-Model"
return "OCR markdown"
settings = GlobalSettings()
settings.integrations.markitdown_pdf_processing_engine = "OCR-Model"
monkeypatch.setattr(
"omlx.api.markitdown_pdf_fallback.convert_pdf_with_ocr_engine",
fake_convert,
)
processed = asyncio.run(
preprocess_markitdown_file_parts_async(
[Message(role="user", content=[_file_part("paper.pdf")])],
global_settings=settings,
engine_pool=FakePool(),
settings_manager=None,
get_sampling_params=lambda *_args: None,
)
)
parts = processed[0].content
assert isinstance(parts, list)
assert "OCR markdown" in (parts[0].text or "")
def test_unload_if_idle_unpinned_skips_pinned_and_unloads_idle(monkeypatch):
class FakeEngine:
def has_active_requests(self):
return False
pool = EnginePool()
pool._entries["OCR-Model"] = EngineEntry(
model_id="OCR-Model",
model_path="/models/OCR-Model",
model_type="vlm",
engine_type="vlm",
estimated_size=1,
config_model_type="dots_ocr",
is_pinned=True,
engine=FakeEngine(),
)
unloaded = []
async def fake_unload(model_id):
unloaded.append(model_id)
monkeypatch.setattr(pool, "_unload_engine", fake_unload)
assert asyncio.run(pool.unload_if_idle_unpinned("OCR-Model")) is False
assert unloaded == []
pool._entries["OCR-Model"].is_pinned = False
assert asyncio.run(pool.unload_if_idle_unpinned("OCR-Model")) is True
assert unloaded == ["OCR-Model"]