Files

200 lines
6.6 KiB
Python

import asyncio
import json
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from aiohttp import web
from core.contracts import ResolvedModel
from core.path_utils import is_path_within
from core.routes.context import RouteContext
from core.routes.directories import register_directory_routes
from core.routes.metadata import register_metadata_routes
from core.type_utils import to_bool, to_int
class _Routes:
def __init__(self):
self.handlers = {}
def get(self, path):
def decorator(handler):
self.handlers[("GET", path)] = handler
return handler
return decorator
def post(self, path):
def decorator(handler):
self.handlers[("POST", path)] = handler
return handler
return decorator
def _json_api_endpoint(_name, **_kwargs):
return lambda handler: handler
def _build_metadata_routes():
routes = _Routes()
values = {
"asyncio": asyncio,
"audit_metadata_sizes": MagicMock(return_value={}),
"build_missing_local_metadata": MagicMock(return_value={}),
"cancel_progress_response": MagicMock(),
"get_metadata_build_capabilities": MagicMock(return_value={}),
"get_model_files": MagicMock(
return_value=[{"filename": "model.safetensors"}]
),
"infer_download_path_templates": MagicMock(return_value=[]),
"get_progress_response": MagicMock(),
"invalidate_local_hash_match_cache": MagicMock(),
"json_api_endpoint": _json_api_endpoint,
"normalize_metadata_build_mode": MagicMock(return_value="fresh"),
"routes": routes,
"run_in_background_thread": MagicMock(),
"self": SimpleNamespace(
metadata_builder_progress=MagicMock(),
_update_metadata_build_progress=MagicMock(),
),
"to_bool": to_bool,
"to_int": to_int,
"web": web,
}
register_metadata_routes(RouteContext(values))
return routes.handlers, values
def _build_directory_routes():
routes = _Routes()
values = {
"TEMPLATE_KEY_ALIASES": {},
"asyncio": asyncio,
"dedupe_local_base_directories": MagicMock(
side_effect=lambda paths, **_kwargs: paths
),
"get_category_folder_keys": MagicMock(return_value=["checkpoints"]),
"get_comfy_root_path": MagicMock(return_value=r"C:\ComfyUI"),
"get_default_root_for_category": MagicMock(return_value=None),
"get_download_directory": MagicMock(return_value=r"C:\models"),
"get_enabled_download_categories": MagicMock(
side_effect=lambda categories: categories
),
"get_local_path_identity": MagicMock(),
"is_path_within": is_path_within,
"get_model_files": MagicMock(
return_value=[{"filename": "model.safetensors"}]
),
"infer_download_path_templates": MagicMock(
return_value=[{"category": "checkpoints"}]
),
"invalidate_local_hash_match_cache": MagicMock(),
"is_civarchive_available": MagicMock(return_value=False),
"is_lora_manager_archive_available": MagicMock(return_value=False),
"json_api_endpoint": _json_api_endpoint,
"load_resolver_settings": MagicMock(return_value={}),
"normalize_download_category": MagicMock(
side_effect=lambda category: category
),
"prefer_local_base_directory": MagicMock(),
"register_version_routes": MagicMock(),
"routes": routes,
"self": SimpleNamespace(logger=MagicMock()),
"split_path_segments": MagicMock(return_value=[]),
"to_bool": to_bool,
"web": web,
}
register_directory_routes(RouteContext(values))
return routes.handlers, values
def _request(query=None):
return SimpleNamespace(query=query or {}, match_info={})
def _body_request(payload):
return SimpleNamespace(
can_read_body=True,
json=AsyncMock(return_value=payload),
query={},
match_info={},
)
@pytest.mark.asyncio
async def test_models_route_forwards_force_rescan_and_invalidates_hash_cache():
handlers, values = _build_metadata_routes()
handler = handlers[("GET", "/model_resolver/models")]
response = await handler(_request({"force": "1"}))
assert json.loads(response.text) == [{"filename": "model.safetensors"}]
values["invalidate_local_hash_match_cache"].assert_called_once_with()
values["get_model_files"].assert_called_once_with(force_rescan=True)
@pytest.mark.asyncio
async def test_models_route_rejects_malformed_force_flag():
handlers, values = _build_metadata_routes()
handler = handlers[("GET", "/model_resolver/models")]
response = await handler(_request({"force": {"enabled": True}}))
assert response.status == 400
assert json.loads(response.text) == {
"error": "Models request force must be a boolean"
}
values["get_model_files"].assert_not_called()
@pytest.mark.asyncio
async def test_path_suggestions_route_scans_and_infers_in_background():
handlers, values = _build_directory_routes()
handler = handlers[("GET", "/model_resolver/path-template-suggestions")]
base_models = [{"name": "SDXL"}]
with patch(
"core.sources.popular.get_base_models_config",
return_value=base_models,
):
response = await handler(_request({"force": "1"}))
assert json.loads(response.text) == [{"category": "checkpoints"}]
values["get_model_files"].assert_called_once_with(True)
values["infer_download_path_templates"].assert_called_once_with(
[ResolvedModel(filename="model.safetensors")],
base_models,
)
@pytest.mark.asyncio
async def test_metadata_size_audit_rejects_malformed_numeric_options():
handlers, values = _build_metadata_routes()
handler = handlers[("POST", "/model_resolver/metadata-size-audit")]
response = await handler(_body_request({"worker_count": {"value": 4}}))
assert response.status == 400
assert json.loads(response.text) == {
"error": "Metadata size audit request worker_count must be an integer"
}
values["audit_metadata_sizes"].assert_not_called()
@pytest.mark.asyncio
async def test_metadata_build_start_rejects_malformed_options():
handlers, values = _build_metadata_routes()
handler = handlers[("POST", "/model_resolver/metadata-build/start")]
response = await handler(
_body_request({"force_rescan": [], "worker_count": 2})
)
assert response.status == 400
assert json.loads(response.text) == {
"error": "Metadata build request force_rescan must be a boolean"
}
values["run_in_background_thread"].assert_not_called()