mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-21 11:11:26 -03:00
5adfa3be36
Standalone users previously had to hand-edit settings.json to configure primary folder_paths. Add a standalone-only Model Paths section to the settings modal: - Backend exposes standalone_mode, folder_paths (with template placeholder values filtered out) and a data-driven folder_path_schema derived from OTHER_MODEL_FOLDER_SUBTYPES via GET /api/lm/settings - The new section renders multi-path editors per model type from the schema, with inline enable_other_models / sub-type controls so other model types are configured without leaving the tab - Persistent restart-required cues after a save: nav dot, inline notice and a global banner (unique id per change so dismissals don't mute future reminders) - The missing-model-paths startup banner and the Other Models no-paths empty state now deep-link into the new section instead of pointing at settings.json
294 lines
8.7 KiB
Python
294 lines
8.7 KiB
Python
import json
|
|
from typing import Any, Optional
|
|
|
|
import pytest
|
|
|
|
from py.config import config
|
|
from py.routes.handlers.misc_handlers import SettingsHandler
|
|
|
|
|
|
class FakeRequest:
|
|
def __init__(self, *, json_data=None):
|
|
self._json_data = json_data or {}
|
|
|
|
async def json(self):
|
|
return self._json_data
|
|
|
|
|
|
class DummySettings:
|
|
def __init__(self):
|
|
self.activated = None
|
|
self.should_raise: Optional[Exception] = None
|
|
|
|
def activate_library(self, name):
|
|
if self.should_raise:
|
|
raise self.should_raise
|
|
self.activated = name
|
|
|
|
|
|
def json_payload(response) -> Any:
|
|
"""Decode the JSON body of a web.Response, asserting it is not null."""
|
|
text = response.text
|
|
assert text is not None
|
|
return json.loads(text)
|
|
|
|
|
|
class DummyDownloader:
|
|
async def refresh_session(self): # pragma: no cover - helper
|
|
return None
|
|
|
|
|
|
async def dummy_downloader_factory(): # pragma: no cover - helper
|
|
return DummyDownloader()
|
|
|
|
|
|
async def noop_async(*_args, **_kwargs): # pragma: no cover - helper
|
|
return None
|
|
|
|
|
|
@pytest.fixture
|
|
def handler():
|
|
return SettingsHandler(
|
|
settings_service=DummySettings(),
|
|
metadata_provider_updater=noop_async,
|
|
downloader_factory=dummy_downloader_factory,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_libraries_returns_registry(monkeypatch, handler):
|
|
registry = {"libraries": {"default": {"name": "Default"}}, "active_library": "default"}
|
|
monkeypatch.setattr(config, "get_library_registry_snapshot", lambda: registry)
|
|
|
|
response = await handler.get_libraries(FakeRequest())
|
|
payload = json_payload(response)
|
|
|
|
assert response.status == 200
|
|
assert payload == {
|
|
"success": True,
|
|
"libraries": registry["libraries"],
|
|
"active_library": "default",
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_libraries_handles_errors(monkeypatch, handler):
|
|
def boom():
|
|
raise RuntimeError("exploded")
|
|
|
|
monkeypatch.setattr(config, "get_library_registry_snapshot", boom)
|
|
|
|
response = await handler.get_libraries(FakeRequest())
|
|
payload = json_payload(response)
|
|
|
|
assert response.status == 500
|
|
assert payload["success"] is False
|
|
assert payload["error"] == "exploded"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_activate_library_success(monkeypatch):
|
|
dummy_settings = DummySettings()
|
|
handler = SettingsHandler(
|
|
settings_service=dummy_settings,
|
|
metadata_provider_updater=noop_async,
|
|
downloader_factory=dummy_downloader_factory,
|
|
)
|
|
|
|
registry = {"libraries": {"alpha": {"name": "Alpha"}}, "active_library": "alpha"}
|
|
monkeypatch.setattr(config, "get_library_registry_snapshot", lambda: registry)
|
|
|
|
response = await handler.activate_library(
|
|
FakeRequest(json_data={"library": "alpha"}) # pyright: ignore[reportArgumentType]
|
|
)
|
|
payload = json_payload(response)
|
|
|
|
assert response.status == 200
|
|
assert payload == {
|
|
"success": True,
|
|
"active_library": "alpha",
|
|
"libraries": registry["libraries"],
|
|
}
|
|
assert dummy_settings.activated == "alpha"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_activate_library_requires_name(handler):
|
|
response = await handler.activate_library(FakeRequest(json_data={}))
|
|
payload = json_payload(response)
|
|
|
|
assert response.status == 400
|
|
assert payload["success"] is False
|
|
assert payload["error"] == "Library name is required"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_activate_library_unknown_returns_404(monkeypatch):
|
|
dummy_settings = DummySettings()
|
|
dummy_settings.should_raise = KeyError("Unknown library")
|
|
handler = SettingsHandler(
|
|
settings_service=dummy_settings,
|
|
metadata_provider_updater=noop_async,
|
|
downloader_factory=dummy_downloader_factory,
|
|
)
|
|
|
|
response = await handler.activate_library(
|
|
FakeRequest(json_data={"library": "ghost"}) # pyright: ignore[reportArgumentType]
|
|
)
|
|
payload = json_payload(response)
|
|
|
|
assert response.status == 404
|
|
assert payload["success"] is False
|
|
assert payload["error"] == "'Unknown library'"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_activate_library_unexpected_error_returns_500(monkeypatch):
|
|
dummy_settings = DummySettings()
|
|
dummy_settings.should_raise = ValueError("bad things")
|
|
handler = SettingsHandler(
|
|
settings_service=dummy_settings,
|
|
metadata_provider_updater=noop_async,
|
|
downloader_factory=dummy_downloader_factory,
|
|
)
|
|
|
|
response = await handler.activate_library(
|
|
FakeRequest(json_data={"library": "broken"}) # pyright: ignore[reportArgumentType]
|
|
)
|
|
payload = json_payload(response)
|
|
|
|
assert response.status == 500
|
|
assert payload["success"] is False
|
|
assert payload["error"] == "bad things"
|
|
|
|
|
|
class DummySettingsForGet:
|
|
def __init__(self, values=None):
|
|
self._values = dict(values or {})
|
|
self.settings_file = "/tmp/settings.json"
|
|
self.set_calls = []
|
|
|
|
def keys(self):
|
|
return self._values.keys()
|
|
|
|
def get(self, key, default=None):
|
|
return self._values.get(key, default)
|
|
|
|
def set(self, key, value):
|
|
self.set_calls.append((key, value))
|
|
self._values[key] = value
|
|
|
|
def get_startup_messages(self):
|
|
return []
|
|
|
|
|
|
def make_get_handler(values=None) -> SettingsHandler:
|
|
return SettingsHandler(
|
|
settings_service=DummySettingsForGet(values),
|
|
metadata_provider_updater=noop_async,
|
|
downloader_factory=dummy_downloader_factory,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def patch_other_models_availability(monkeypatch):
|
|
monkeypatch.setattr(
|
|
config,
|
|
"get_other_models_availability",
|
|
lambda: {"available": False},
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_settings_plugin_mode_hides_folder_paths(
|
|
monkeypatch, patch_other_models_availability
|
|
):
|
|
monkeypatch.delenv("LORA_MANAGER_STANDALONE", raising=False)
|
|
handler = make_get_handler(
|
|
{
|
|
"language": "en",
|
|
"folder_paths": {"loras": ["/models/loras"]},
|
|
}
|
|
)
|
|
|
|
response = await handler.get_settings(FakeRequest())
|
|
payload = json_payload(response)
|
|
|
|
assert response.status == 200
|
|
settings = payload["settings"]
|
|
assert settings["standalone_mode"] is False
|
|
assert "folder_paths" not in settings
|
|
assert "folder_path_schema" not in settings
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_settings_standalone_exposes_folder_paths_and_schema(
|
|
monkeypatch, patch_other_models_availability
|
|
):
|
|
monkeypatch.setenv("LORA_MANAGER_STANDALONE", "1")
|
|
folder_paths = {"loras": ["/models/loras"], "vae": ["/models/vae"]}
|
|
handler = make_get_handler({"language": "en", "folder_paths": folder_paths})
|
|
|
|
response = await handler.get_settings(FakeRequest())
|
|
payload = json_payload(response)
|
|
|
|
assert response.status == 200
|
|
settings = payload["settings"]
|
|
assert settings["standalone_mode"] is True
|
|
assert settings["folder_paths"] == folder_paths
|
|
|
|
schema = settings["folder_path_schema"]
|
|
core_keys = [entry["key"] for entry in schema if entry["category"] == "core"]
|
|
assert core_keys == ["loras", "checkpoints", "unet", "embeddings"]
|
|
other_entries = {entry["key"]: entry for entry in schema if entry["category"] == "other"}
|
|
assert other_entries["vae"]["sub_type"] == "vae"
|
|
assert other_entries["text_encoders"]["sub_type"] == "text_encoder"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_settings_passes_folder_paths_through(
|
|
monkeypatch, patch_other_models_availability
|
|
):
|
|
monkeypatch.setenv("LORA_MANAGER_STANDALONE", "1")
|
|
handler = make_get_handler({"folder_paths": {}})
|
|
new_paths = {"loras": ["/models/loras"]}
|
|
|
|
response = await handler.update_settings(
|
|
FakeRequest(json_data={"folder_paths": new_paths})
|
|
)
|
|
payload = json_payload(response)
|
|
|
|
assert response.status == 200
|
|
assert payload["success"] is True
|
|
assert handler._settings.set_calls == [("folder_paths", new_paths)]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_settings_standalone_filters_template_placeholders(
|
|
monkeypatch, patch_other_models_availability
|
|
):
|
|
"""Fresh installs are seeded from settings.json.example; its placeholder
|
|
paths must not show up as real values in the Model Paths UI."""
|
|
monkeypatch.setenv("LORA_MANAGER_STANDALONE", "1")
|
|
handler = make_get_handler(
|
|
{
|
|
"folder_paths": {
|
|
"loras": ["C:/path/to/your/loras_folder", "/real/loras"],
|
|
"vae": ["C:/path/to/another/vae_folder"],
|
|
}
|
|
}
|
|
)
|
|
handler._settings.get_template_folder_path_placeholders = lambda: {
|
|
"C:/path/to/your/loras_folder",
|
|
"C:/path/to/another/vae_folder",
|
|
}
|
|
|
|
response = await handler.get_settings(FakeRequest())
|
|
payload = json_payload(response)
|
|
|
|
assert response.status == 200
|
|
assert payload["settings"]["folder_paths"] == {
|
|
"loras": ["/real/loras"],
|
|
"vae": [],
|
|
}
|