mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-22 13:42:12 -03:00
- Add standalone mode detection via LORA_MANAGER_STANDALONE environment variable - Improve error handling for settings file loading with specific JSON decode errors - Add startup messages system to communicate configuration warnings and errors to users - Include settings file path and startup messages in settings API response - Automatically save settings when bootstrapping from defaults due to missing/invalid settings file - Add configuration warnings collection for environment variables and other settings issues The changes improve robustness of settings management and provide better user feedback when configuration issues occur.
232 lines
7.6 KiB
Python
232 lines
7.6 KiB
Python
"""Tests covering the standalone bootstrap flow."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from types import ModuleType, SimpleNamespace
|
|
from typing import List, Tuple
|
|
|
|
import pytest
|
|
from aiohttp import web
|
|
|
|
from py.utils.settings_paths import ensure_settings_file
|
|
|
|
|
|
ROUTE_CALLS_KEY: web.AppKey[List[Tuple[str, dict]]] = web.AppKey("route_calls")
|
|
|
|
|
|
@pytest.fixture
|
|
def standalone_module(monkeypatch) -> ModuleType:
|
|
"""Load the ``standalone`` module with a lightweight ``LoraManager`` stub."""
|
|
|
|
import importlib
|
|
import sys
|
|
from types import ModuleType
|
|
|
|
original_lora_manager = sys.modules.get("py.lora_manager")
|
|
|
|
stub_module = ModuleType("py.lora_manager")
|
|
|
|
class _StubLoraManager:
|
|
@classmethod
|
|
def add_routes(cls): # pragma: no cover - compatibility shim
|
|
return None
|
|
|
|
@classmethod
|
|
async def _initialize_services(cls): # pragma: no cover - compatibility shim
|
|
return None
|
|
|
|
@classmethod
|
|
async def _cleanup(cls, app): # pragma: no cover - compatibility shim
|
|
return None
|
|
|
|
stub_module.LoraManager = _StubLoraManager
|
|
sys.modules["py.lora_manager"] = stub_module
|
|
|
|
module = importlib.import_module("standalone")
|
|
yield module
|
|
|
|
sys.modules.pop("standalone", None)
|
|
if original_lora_manager is not None:
|
|
sys.modules["py.lora_manager"] = original_lora_manager
|
|
else:
|
|
sys.modules.pop("py.lora_manager", None)
|
|
|
|
|
|
def _write_settings(contents: dict) -> Path:
|
|
"""Persist *contents* into the isolated settings.json."""
|
|
|
|
settings_path = Path(ensure_settings_file())
|
|
settings_path.write_text(json.dumps(contents))
|
|
return settings_path
|
|
|
|
|
|
async def test_standalone_server_sets_up_routes(tmp_path, standalone_module):
|
|
"""``StandaloneServer.setup`` wires the HTTP routes and lifecycle hooks."""
|
|
|
|
example_images_dir = tmp_path / "example_images"
|
|
example_images_dir.mkdir()
|
|
(example_images_dir / "preview.png").write_text("placeholder")
|
|
|
|
_write_settings({"example_images_path": str(example_images_dir)})
|
|
|
|
server = standalone_module.StandaloneServer()
|
|
await server.setup()
|
|
|
|
canonical_routes = {resource.canonical for resource in server.app.router.resources()}
|
|
|
|
assert "/" in canonical_routes, "status endpoint should be registered"
|
|
assert (
|
|
"/example_images_static" in canonical_routes
|
|
), "static example image route should be exposed when the directory exists"
|
|
assert server.app.on_startup, "startup callbacks must be attached"
|
|
assert server.app.on_shutdown, "shutdown callbacks must be attached"
|
|
|
|
|
|
def test_standalone_server_raises_header_limits(standalone_module):
|
|
"""``StandaloneServer`` configures ``handler_args`` to tolerate large headers."""
|
|
|
|
server = standalone_module.StandaloneServer()
|
|
|
|
assert server.app._handler_args["max_field_size"] == standalone_module.HEADER_SIZE_LIMIT
|
|
assert server.app._handler_args["max_line_size"] == standalone_module.HEADER_SIZE_LIMIT
|
|
|
|
|
|
def test_validate_settings_warns_for_missing_model_paths(caplog, standalone_module):
|
|
"""Missing model folders trigger the configuration warning."""
|
|
|
|
caplog.set_level("WARNING")
|
|
|
|
_write_settings(
|
|
{
|
|
"folder_paths": {
|
|
"loras": ["/non/existent"],
|
|
"checkpoints": [],
|
|
"embeddings": [],
|
|
}
|
|
}
|
|
)
|
|
|
|
assert standalone_module.validate_settings() is True
|
|
warning_lines = [record.message for record in caplog.records if record.levelname == "WARNING"]
|
|
assert any("Standalone mode is using fallback" in line for line in warning_lines)
|
|
assert any("Model folders need setup" in line for line in warning_lines)
|
|
|
|
|
|
def test_standalone_lora_manager_registers_routes(monkeypatch, tmp_path, standalone_module):
|
|
"""``StandaloneLoraManager.add_routes`` registers static and websocket routes."""
|
|
|
|
app = web.Application()
|
|
route_calls: List[Tuple[str, dict]] = []
|
|
app[ROUTE_CALLS_KEY] = route_calls
|
|
|
|
locales_dir = tmp_path / "locales"
|
|
locales_dir.mkdir()
|
|
static_dir = tmp_path / "static"
|
|
static_dir.mkdir()
|
|
|
|
monkeypatch.setattr(
|
|
standalone_module,
|
|
"config",
|
|
SimpleNamespace(i18n_path=str(locales_dir), static_path=str(static_dir)),
|
|
)
|
|
|
|
register_calls: List[str] = []
|
|
|
|
import py.services.model_service_factory as factory_module
|
|
|
|
def fake_register_default_model_types() -> None:
|
|
register_calls.append("called")
|
|
|
|
monkeypatch.setattr(
|
|
factory_module,
|
|
"register_default_model_types",
|
|
fake_register_default_model_types,
|
|
)
|
|
|
|
def fake_setup_all_routes(cls, app_arg):
|
|
route_calls.append(("ModelServiceFactory.setup_all_routes", {"app": app_arg}))
|
|
|
|
monkeypatch.setattr(
|
|
factory_module.ModelServiceFactory,
|
|
"setup_all_routes",
|
|
classmethod(fake_setup_all_routes),
|
|
)
|
|
|
|
class DummyRecipeRoutes:
|
|
@staticmethod
|
|
def setup_routes(app_arg):
|
|
route_calls.append(("RecipeRoutes", {}))
|
|
|
|
class DummyUpdateRoutes:
|
|
@staticmethod
|
|
def setup_routes(app_arg):
|
|
route_calls.append(("UpdateRoutes", {}))
|
|
|
|
class DummyMiscRoutes:
|
|
@staticmethod
|
|
def setup_routes(app_arg):
|
|
route_calls.append(("MiscRoutes", {}))
|
|
|
|
class DummyExampleImagesRoutes:
|
|
@staticmethod
|
|
def setup_routes(app_arg, **kwargs):
|
|
route_calls.append(("ExampleImagesRoutes", kwargs))
|
|
|
|
class DummyPreviewRoutes:
|
|
@staticmethod
|
|
def setup_routes(app_arg):
|
|
route_calls.append(("PreviewRoutes", {}))
|
|
|
|
class DummyStatsRoutes:
|
|
def setup_routes(self, app_arg):
|
|
route_calls.append(("StatsRoutes", {}))
|
|
|
|
monkeypatch.setattr("py.routes.recipe_routes.RecipeRoutes", DummyRecipeRoutes)
|
|
monkeypatch.setattr("py.routes.update_routes.UpdateRoutes", DummyUpdateRoutes)
|
|
monkeypatch.setattr("py.routes.misc_routes.MiscRoutes", DummyMiscRoutes)
|
|
monkeypatch.setattr(
|
|
"py.routes.example_images_routes.ExampleImagesRoutes",
|
|
DummyExampleImagesRoutes,
|
|
)
|
|
monkeypatch.setattr("py.routes.preview_routes.PreviewRoutes", DummyPreviewRoutes)
|
|
monkeypatch.setattr("py.routes.stats_routes.StatsRoutes", DummyStatsRoutes)
|
|
|
|
async def _noop_ws_handler(request):
|
|
return web.Response(status=204)
|
|
|
|
ws_manager_stub = SimpleNamespace(
|
|
handle_connection=_noop_ws_handler,
|
|
handle_download_connection=_noop_ws_handler,
|
|
handle_init_connection=_noop_ws_handler,
|
|
)
|
|
monkeypatch.setattr("py.services.websocket_manager.ws_manager", ws_manager_stub)
|
|
|
|
server = SimpleNamespace(app=app)
|
|
|
|
standalone_module.StandaloneLoraManager.add_routes(server)
|
|
|
|
assert register_calls, "default model types should be registered"
|
|
|
|
canonical_routes = {resource.canonical for resource in app.router.resources()}
|
|
assert "/locales" in canonical_routes
|
|
assert "/loras_static" in canonical_routes
|
|
|
|
websocket_paths = {route.resource.canonical for route in app.router.routes() if "ws" in route.resource.canonical}
|
|
assert {
|
|
"/ws/fetch-progress",
|
|
"/ws/download-progress",
|
|
"/ws/init-progress",
|
|
} <= websocket_paths
|
|
|
|
assert any(call[0] == "ModelServiceFactory.setup_all_routes" for call in route_calls)
|
|
assert any(call[0] == "RecipeRoutes" for call in route_calls)
|
|
assert any(call[0] == "StatsRoutes" for call in route_calls)
|
|
|
|
prompt_server = pytest.importorskip("server").PromptServer
|
|
assert getattr(prompt_server, "instance", None) is server
|
|
|
|
assert app.on_startup, "service initialization hook should be scheduled"
|
|
assert app.on_shutdown, "cleanup hook should be scheduled"
|