Files
ComfyUI-Lora-Manager/tests/services/test_settings_manager.py
T
Will Miao 74736f7560 fix(organize): exclude Civitai meta tags from folder names (#1119)
Follow-up to the keyword-dump guard. The reported model's tag list is
["lora, character, ... face", "base model"], so skipping the dump left the
"base model" label to be picked as the folder name. That label describes
Civitai's listing rather than the model's content, which makes it as
meaningless as a folder as the blob was.

Add CIVITAI_META_TAGS and is_civitai_meta_tag(), and skip those labels in
the automatic fallback. An explicit priority entry still matches them, so a
user who does want a "base model" folder can configure one.

The reported model now resolves to "Krea 2/no tags" instead of
"Krea 2/base model".

Also correct a comment that listed ".civitai.info" among the files sitting
next to a model. LoRA Manager only reads that sidecar -- other tools write
it -- and writes ".metadata.json" itself.
2026-09-23 13:33:59 +08:00

1501 lines
52 KiB
Python

import asyncio
import copy
import json
import os
from concurrent.futures import Future
import pytest
from py.services import service_registry
from py.services import settings_manager as settings_manager_module
from py.services.settings_manager import SettingsManager
from py.utils import settings_paths
@pytest.mark.no_settings_dir_isolation
def test_portable_settings_use_project_root(tmp_path, monkeypatch):
from importlib import reload
settings_paths_module = reload(settings_paths)
monkeypatch.setattr(
settings_paths_module, "get_project_root", lambda: str(tmp_path)
)
monkeypatch.setattr(
settings_paths_module,
"user_config_dir",
lambda *_args, **_kwargs: str(tmp_path / "user_config"),
)
portable_settings = {"use_portable_settings": True}
(tmp_path / "settings.json").write_text(
json.dumps(portable_settings), encoding="utf-8"
)
config_dir = settings_paths_module.get_settings_dir(create=True)
assert config_dir == str(tmp_path)
from py.services import persistent_model_cache as persistent_model_cache_module
cache_module = reload(persistent_model_cache_module)
monkeypatch.setattr(cache_module.PersistentModelCache, "_instances", {})
monkeypatch.delenv("LORA_MANAGER_CACHE_DB", raising=False)
cache = cache_module.PersistentModelCache(library_name="portable_lib")
expected_cache_path = tmp_path / "cache" / "model" / "portable_lib.sqlite"
assert cache.get_database_path() == str(expected_cache_path)
assert expected_cache_path.parent.is_dir()
@pytest.fixture
def manager(tmp_path, monkeypatch):
monkeypatch.setattr(SettingsManager, "_save_settings", lambda self: None)
fake_settings_path = tmp_path / "settings.json"
monkeypatch.setattr(
"py.services.settings_manager.ensure_settings_file",
lambda logger=None: str(fake_settings_path),
)
mgr = SettingsManager()
mgr.settings_file = str(fake_settings_path)
return mgr
def test_initial_save_persists_minimal_template(tmp_path, monkeypatch):
settings_path = tmp_path / "settings.json"
monkeypatch.setattr(
"py.services.settings_manager.ensure_settings_file",
lambda logger=None: str(settings_path),
)
template = {
"_note": "template note",
"language": "fr",
"folder_paths": {"loras": ["/loras"]},
}
def fake_template_loader(self):
self._seed_template = copy.deepcopy(template)
return copy.deepcopy(template)
monkeypatch.setattr(
SettingsManager, "_load_settings_template", fake_template_loader
)
manager = SettingsManager()
persisted = json.loads(settings_path.read_text(encoding="utf-8"))
assert persisted["_note"] == "template note"
assert "libraries" not in persisted
assert persisted["folder_paths"]["loras"] == ["/loras"]
assert manager.get_libraries()["default"]["folder_paths"]["loras"] == ["/loras"]
def test_existing_folder_paths_seed_default_library(tmp_path, monkeypatch):
monkeypatch.setenv("LORA_MANAGER_STANDALONE", "1")
lora_dir = tmp_path / "loras"
checkpoint_dir = tmp_path / "checkpoints"
unet_dir = tmp_path / "unet"
diffusion_dir = tmp_path / "diffusion_models"
embedding_dir = tmp_path / "embeddings"
for directory in (lora_dir, checkpoint_dir, unet_dir, diffusion_dir, embedding_dir):
directory.mkdir()
initial = {
"folder_paths": {
"loras": [str(lora_dir)],
"checkpoints": [str(checkpoint_dir)],
"unet": [str(diffusion_dir), str(unet_dir)],
"embeddings": [str(embedding_dir)],
}
}
manager = _create_manager_with_settings(tmp_path, monkeypatch, initial)
stored_paths = manager.get("folder_paths")
assert stored_paths["loras"] == [str(lora_dir)]
assert stored_paths["checkpoints"] == [str(checkpoint_dir)]
assert stored_paths["unet"] == [str(diffusion_dir), str(unet_dir)]
assert stored_paths["embeddings"] == [str(embedding_dir)]
libraries = manager.get_libraries()
assert "default" in libraries
assert libraries["default"]["folder_paths"]["loras"] == [str(lora_dir)]
assert libraries["default"]["folder_paths"]["checkpoints"] == [str(checkpoint_dir)]
assert libraries["default"]["folder_paths"]["unet"] == [
str(diffusion_dir),
str(unet_dir),
]
assert libraries["default"]["folder_paths"]["embeddings"] == [str(embedding_dir)]
assert manager.get_startup_messages() == []
def test_environment_variable_overrides_settings(tmp_path, monkeypatch):
monkeypatch.setattr(SettingsManager, "_save_settings", lambda self: None)
monkeypatch.setenv("CIVITAI_API_KEY", "secret")
fake_settings_path = tmp_path / "settings.json"
monkeypatch.setattr(
"py.services.settings_manager.ensure_settings_file",
lambda logger=None: str(fake_settings_path),
)
mgr = SettingsManager()
mgr.settings_file = str(fake_settings_path)
assert mgr.get("civitai_api_key") == "secret"
def test_default_download_backend_is_python(manager):
assert manager.get("download_backend") == "python"
assert manager.get("aria2c_path") == ""
def _create_manager_with_settings(
tmp_path, monkeypatch, initial_settings, *, save_spy=None
):
"""Helper to instantiate SettingsManager with predefined settings."""
fake_settings_path = tmp_path / "settings.json"
monkeypatch.setattr(
"py.services.settings_manager.ensure_settings_file",
lambda logger=None: str(fake_settings_path),
)
if save_spy is None:
monkeypatch.setattr(SettingsManager, "_save_settings", lambda self: None)
else:
monkeypatch.setattr(SettingsManager, "_save_settings", save_spy)
monkeypatch.setattr(
SettingsManager,
"_load_settings",
lambda self: copy.deepcopy(initial_settings),
)
mgr = SettingsManager()
mgr.settings_file = str(fake_settings_path)
return mgr
def _setup_storage_paths(tmp_path, monkeypatch):
project_root = tmp_path / "repo"
project_root.mkdir(parents=True, exist_ok=True)
user_dir = tmp_path / "user_config"
user_dir.mkdir(parents=True, exist_ok=True)
user_settings_path = user_dir / "settings.json"
monkeypatch.setattr(
"py.services.settings_manager.ensure_settings_file",
lambda logger=None: str(user_settings_path),
)
monkeypatch.setattr(
settings_manager_module,
"user_config_dir",
lambda *args, **kwargs: str(user_dir),
)
monkeypatch.setattr(settings_paths, "get_project_root", lambda: str(project_root))
return project_root, user_dir, user_settings_path
def _populate_settings_dir(root_dir):
"""Create test data for all managed subdirectories under a settings directory."""
(root_dir / "cache" / "symlink").mkdir(parents=True, exist_ok=True)
(root_dir / "cache" / "symlink" / "symlink_map.json").write_text(
'{"migrated": true}', encoding="utf-8"
)
(root_dir / "backups").mkdir(parents=True, exist_ok=True)
(root_dir / "backups" / "backup_test.zip").write_text(
"backup", encoding="utf-8"
)
(root_dir / "logs").mkdir(parents=True, exist_ok=True)
(root_dir / "logs" / "session.log").write_text("log", encoding="utf-8")
(root_dir / "stats").mkdir(parents=True, exist_ok=True)
(root_dir / "stats" / "stats.json").write_text(
'{"stats": true}', encoding="utf-8"
)
(root_dir / "wildcards").mkdir(parents=True, exist_ok=True)
(root_dir / "wildcards" / "test.txt").write_text("wildcard", encoding="utf-8")
def test_switch_to_portable_mode_copies_subdirectories(tmp_path, monkeypatch):
project_root, user_dir, user_settings = _setup_storage_paths(tmp_path, monkeypatch)
_populate_settings_dir(user_dir)
manager = SettingsManager()
manager.set("use_portable_settings", True)
assert manager.settings_file == str(project_root / "settings.json")
# Managed subdirectories should all be migrated
assert (
project_root / "cache" / "symlink" / "symlink_map.json"
).read_text(encoding="utf-8") == '{"migrated": true}'
assert (
project_root / "backups" / "backup_test.zip"
).read_text(encoding="utf-8") == "backup"
assert (project_root / "logs" / "session.log").read_text(
encoding="utf-8"
) == "log"
assert (project_root / "stats" / "stats.json").read_text(
encoding="utf-8"
) == '{"stats": true}'
assert (project_root / "wildcards" / "test.txt").read_text(
encoding="utf-8"
) == "wildcard"
assert user_settings.exists()
def test_switching_back_to_user_config_moves_subdirectories(tmp_path, monkeypatch):
project_root, user_dir, user_settings = _setup_storage_paths(tmp_path, monkeypatch)
_populate_settings_dir(user_dir)
manager = SettingsManager()
manager.set("use_portable_settings", True)
# Populate project-root managed subdirectories
(project_root / "cache" / "model").mkdir(parents=True, exist_ok=True)
(project_root / "cache" / "model" / "default.sqlite").write_text(
"project_db", encoding="utf-8"
)
(project_root / "backups" / "project_backup.zip").write_text(
"project_backup", encoding="utf-8"
)
(project_root / "logs" / "project.log").write_text(
"project_log", encoding="utf-8"
)
(project_root / "stats" / "project_stats.json").write_text(
'{"project": true}', encoding="utf-8"
)
(project_root / "wildcards" / "project.txt").write_text(
"project_wildcard", encoding="utf-8"
)
manager.set("use_portable_settings", False)
assert manager.settings_file == str(user_settings)
assert (user_dir / "cache" / "model" / "default.sqlite").read_text(
encoding="utf-8"
) == "project_db"
assert (user_dir / "backups" / "project_backup.zip").read_text(
encoding="utf-8"
) == "project_backup"
assert (user_dir / "logs" / "project.log").read_text(
encoding="utf-8"
) == "project_log"
assert (user_dir / "stats" / "project_stats.json").read_text(
encoding="utf-8"
) == '{"project": true}'
assert (user_dir / "wildcards" / "project.txt").read_text(
encoding="utf-8"
) == "project_wildcard"
def test_portable_switch_ignored_when_settings_dir_pinned(tmp_path, monkeypatch):
"""An explicit settings dir (--settings-path) must never trigger the
portable-mode directory migration between project root and user config."""
project_root, user_dir, user_settings = _setup_storage_paths(tmp_path, monkeypatch)
_populate_settings_dir(user_dir)
custom_dir = tmp_path / "custom_settings"
custom_dir.mkdir()
monkeypatch.setattr(
"py.services.settings_manager.ensure_settings_file",
lambda logger=None: str(custom_dir / "settings.json"),
)
settings_paths.set_settings_dir_override(str(custom_dir))
try:
manager = SettingsManager()
manager.settings_file = str(custom_dir / "settings.json")
manager.set("use_portable_settings", True)
# Settings file stays pinned; no directories are migrated anywhere and
# the settings file is not mirrored to the user config dir.
assert manager.settings_file == str(custom_dir / "settings.json")
assert not (project_root / "cache").exists()
assert not (project_root / "backups").exists()
assert not (project_root / "settings.json").exists()
assert not user_settings.exists()
finally:
settings_paths.set_settings_dir_override(None)
def test_download_path_template_parses_json_string(manager):
templates = {"lora": "{author}", "checkpoint": "{author}", "embedding": "{author}"}
manager.settings["download_path_templates"] = json.dumps(templates)
template = manager.get_download_path_template("lora")
assert template == "{author}"
assert isinstance(manager.settings["download_path_templates"], dict)
def test_download_path_template_invalid_json(manager):
manager.settings["download_path_templates"] = "not json"
template = manager.get_download_path_template("checkpoint")
assert template == "{base_model}/{first_tag}"
assert (
manager.settings["download_path_templates"]["lora"]
== "{base_model}/{first_tag}"
)
def test_download_path_template_defaults_flat_for_other(manager):
"""'other' has no settings-UI row and no priority_tags entry, so an
unconfigured template must resolve to a flat layout rather than the
{base_model}/{first_tag} fallback (which scatters files into arbitrary
CivitAI-tag folders)."""
manager.settings["download_path_templates"] = {}
assert manager.get_download_path_template("other") == ""
# An explicit user configuration still wins.
manager.settings["download_path_templates"] = {"other": "{base_model}"}
assert manager.get_download_path_template("other") == "{base_model}"
def test_download_path_template_unknown_type_is_flat(manager):
manager.settings["download_path_templates"] = {}
assert manager.get_download_path_template("not-a-model-type") == ""
# Real CivitAI data for the model reported in issue #1119: the uploader dumped
# a whole keyword list into a single tag.
KEYWORD_DUMP_TAG = (
"lora, character, rosie, irish, redhead, auburn, freckles, green eyes, "
"curly hair, woman, female, photorealistic, realistic, krea2, dark beast, "
"kreativity, nsfw, nude, portrait, face"
)
def test_resolve_priority_tag_prefers_configured_priority(manager):
# Priority order from CIVITAI_MODEL_TAGS: "character" precedes "anime".
assert manager.resolve_priority_tag_for_model(["anime", "character"], "lora") == (
"character"
)
def test_resolve_priority_tag_falls_back_to_first_usable_tag(manager):
assert (
manager.resolve_priority_tag_for_model(["portrait", "anime-ish"], "lora")
== "portrait"
)
def test_resolve_priority_tag_skips_keyword_dump_tag(manager):
"""A keyword-dump tag must not be used as a folder name (#1119)."""
assert manager.resolve_priority_tag_for_model([KEYWORD_DUMP_TAG], "lora") == ""
def test_resolve_priority_tag_skips_keyword_dump_and_uses_next_tag(manager):
assert (
manager.resolve_priority_tag_for_model([KEYWORD_DUMP_TAG, "portrait"], "lora")
== "portrait"
)
def test_resolve_priority_tag_skips_unusable_tags(manager):
overlong_tag = "x" * 51
assert manager.resolve_priority_tag_for_model([overlong_tag], "lora") == ""
assert manager.resolve_priority_tag_for_model([overlong_tag, " "], "lora") == ""
# Non-string entries never win the fallback.
assert manager.resolve_priority_tag_for_model([None, 42], "lora") == ""
# A tag at the length budget is still accepted and stripped.
assert manager.resolve_priority_tag_for_model(["x" * 50], "lora") == "x" * 50
assert manager.resolve_priority_tag_for_model([" portrait "], "lora") == "portrait"
def test_resolve_priority_tag_skips_civitai_meta_tags(manager):
"""Civitai's structural labels are not content, so they cannot be folders."""
assert manager.resolve_priority_tag_for_model(["base model"], "lora") == ""
assert (
manager.resolve_priority_tag_for_model(["Base Model"], "lora") == ""
), "the meta tag check must be case-insensitive"
assert manager.resolve_priority_tag_for_model(["base model", " "], "lora") == ""
# A real tag after the label is still used.
assert (
manager.resolve_priority_tag_for_model(["base model", "portrait"], "lora")
== "portrait"
)
def test_resolve_priority_tag_meta_tag_can_be_opted_into(manager):
"""An explicit priority entry still wins over the meta tag exclusion."""
manager.settings["priority_tags"] = {"lora": "base model"}
assert (
manager.resolve_priority_tag_for_model(["base model", "portrait"], "lora")
== "base model"
)
def test_resolve_priority_tag_real_1119_tag_list(manager):
"""End to end for the reported model: both of its tags are unusable."""
assert (
manager.resolve_priority_tag_for_model(
[KEYWORD_DUMP_TAG, "base model"], "lora"
)
== ""
)
def test_auto_set_default_roots(manager):
# Clear any previously auto-set values to test fresh behavior
manager.settings["default_lora_root"] = ""
manager.settings["default_checkpoint_root"] = ""
manager.settings["default_embedding_root"] = ""
manager.settings["default_unet_root"] = ""
manager.settings["folder_paths"] = {
"loras": ["/loras"],
"checkpoints": ["/checkpoints"],
"embeddings": ["/embeddings"],
}
manager._auto_set_default_roots()
assert manager.get("default_lora_root") == "/loras"
assert manager.get("default_checkpoint_root") == "/checkpoints"
assert manager.get("default_embedding_root") == "/embeddings"
def test_auto_set_default_roots_repairs_stale_values(manager):
manager.settings["default_lora_root"] = "/stale-lora"
manager.settings["default_checkpoint_root"] = "/stale-checkpoint"
manager.settings["default_embedding_root"] = "/stale-embedding"
manager.settings["default_unet_root"] = "/stale-unet"
manager.settings["folder_paths"] = {
"loras": ["/loras"],
"checkpoints": ["/checkpoints"],
"unet": ["/unet"],
"embeddings": ["/embeddings"],
}
manager._auto_set_default_roots()
assert manager.get("default_lora_root") == "/loras"
assert manager.get("default_checkpoint_root") == "/checkpoints"
assert manager.get("default_unet_root") == "/unet"
assert manager.get("default_embedding_root") == "/embeddings"
def test_auto_set_default_roots_keeps_valid_values(manager):
manager.settings["default_lora_root"] = "/loras"
manager.settings["default_checkpoint_root"] = "/checkpoints"
manager.settings["default_embedding_root"] = "/embeddings"
manager.settings["default_unet_root"] = "/unet"
manager.settings["folder_paths"] = {
"loras": ["/loras", "/other-loras"],
"checkpoints": ["/checkpoints"],
"unet": ["/unet", "/other-unet"],
"embeddings": ["/embeddings"],
}
manager._auto_set_default_roots()
assert manager.get("default_lora_root") == "/loras"
assert manager.get("default_checkpoint_root") == "/checkpoints"
assert manager.get("default_unet_root") == "/unet"
assert manager.get("default_embedding_root") == "/embeddings"
def test_auto_set_default_roots_keeps_valid_extra_values(manager):
manager.settings["default_lora_root"] = "/extra-loras"
manager.settings["default_checkpoint_root"] = "/extra-checkpoints"
manager.settings["default_embedding_root"] = "/extra-embeddings"
manager.settings["default_unet_root"] = "/extra-unet"
manager.settings["folder_paths"] = {
"loras": ["/loras"],
"checkpoints": ["/checkpoints"],
"unet": ["/unet"],
"embeddings": ["/embeddings"],
}
manager.settings["extra_folder_paths"] = {
"loras": ["/extra-loras"],
"checkpoints": ["/extra-checkpoints"],
"unet": ["/extra-unet"],
"embeddings": ["/extra-embeddings"],
}
manager._auto_set_default_roots()
assert manager.get("default_lora_root") == "/extra-loras"
assert manager.get("default_checkpoint_root") == "/extra-checkpoints"
assert manager.get("default_unet_root") == "/extra-unet"
assert manager.get("default_embedding_root") == "/extra-embeddings"
def test_auto_set_default_roots_keeps_valid_extra_values_with_windows_slash_mismatch(manager):
manager.settings["default_lora_root"] = "U:/Lora7/Loras"
manager.settings["default_checkpoint_root"] = "U:/Lora7/Models"
manager.settings["folder_paths"] = {
"loras": ["R:/ComfyUI/models/loras"],
"checkpoints": ["R:/ComfyUI/models/checkpoints"],
}
manager.settings["extra_folder_paths"] = {
"loras": ["U:\\Lora7\\Loras"],
"checkpoints": ["U:\\Lora7\\Models"],
}
manager._auto_set_default_roots()
assert manager.get("default_lora_root") == "U:/Lora7/Loras"
assert manager.get("default_checkpoint_root") == "U:/Lora7/Models"
def test_auto_set_default_roots_falls_back_to_extra_when_primary_missing(manager):
manager.settings["default_lora_root"] = ""
manager.settings["folder_paths"] = {"loras": []}
manager.settings["extra_folder_paths"] = {"loras": ["/extra-loras"]}
manager._auto_set_default_roots()
assert manager.get("default_lora_root") == "/extra-loras"
def test_delete_setting(manager):
manager.set("example", 1)
manager.delete("example")
assert manager.get("example") is None
def test_missing_mature_blur_level_defaults_to_r(tmp_path, monkeypatch):
manager = _create_manager_with_settings(
tmp_path,
monkeypatch,
{
"blur_mature_content": True,
"folder_paths": {},
},
)
assert manager.get("mature_blur_level") == "R"
def test_invalid_mature_blur_level_is_normalized_to_r(tmp_path, monkeypatch):
manager = _create_manager_with_settings(
tmp_path,
monkeypatch,
{
"mature_blur_level": "unsafe",
"folder_paths": {},
},
)
assert manager.get("mature_blur_level") == "R"
def test_model_name_display_setting_notifies_scanners(tmp_path, monkeypatch):
initial = {
"libraries": {
"default": {
"folder_paths": {},
"default_lora_root": "",
"default_checkpoint_root": "",
"default_embedding_root": "",
}
},
"active_library": "default",
"model_name_display": "model_name",
}
manager = _create_manager_with_settings(tmp_path, monkeypatch, initial)
loop = asyncio.new_event_loop()
setattr(loop, "_thread_id", 1)
class DummyScanner:
def __init__(self):
self.calls = []
self.loop = loop
async def on_model_name_display_changed(self, mode: str) -> None:
self.calls.append(mode)
dummy_scanner = DummyScanner()
dispatched_loops = []
futures = []
def tracking_run_coroutine_threadsafe(coro, target_loop):
dispatched_loops.append(target_loop)
future = Future()
futures.append(future)
try:
result = asyncio.run(coro)
except Exception as exc:
future.set_exception(exc)
else:
future.set_result(result)
return future
def fake_get_service_sync(cls, name):
return dummy_scanner if name == "lora_scanner" else None
monkeypatch.setattr(
service_registry.ServiceRegistry,
"get_service_sync",
classmethod(fake_get_service_sync),
)
monkeypatch.setattr(
asyncio, "run_coroutine_threadsafe", tracking_run_coroutine_threadsafe
)
try:
manager.set("model_name_display", "file_name")
for future in futures:
future.result(timeout=1)
assert dummy_scanner.calls == ["file_name"]
assert dispatched_loops == [dummy_scanner.loop]
finally:
setattr(loop, "_thread_id", None)
loop.close()
def test_migrates_legacy_settings_file(tmp_path, monkeypatch):
legacy_root = tmp_path / "legacy"
legacy_root.mkdir()
legacy_file = legacy_root / "settings.json"
legacy_file.write_text('{"value": 1}', encoding="utf-8")
target_dir = tmp_path / "config"
monkeypatch.setattr(settings_paths, "get_project_root", lambda: str(legacy_root))
monkeypatch.setattr(
settings_paths, "user_config_dir", lambda *_, **__: str(target_dir)
)
migrated_path = settings_paths.ensure_settings_file()
assert migrated_path == str(target_dir / "settings.json")
assert (target_dir / "settings.json").exists()
assert not legacy_file.exists()
def test_uses_portable_settings_file_when_enabled(tmp_path, monkeypatch):
repo_root = tmp_path / "repo"
repo_root.mkdir()
repo_settings = repo_root / "settings.json"
repo_settings.write_text(
json.dumps({"use_portable_settings": True, "value": 1}),
encoding="utf-8",
)
user_dir = tmp_path / "user"
monkeypatch.setattr(settings_paths, "get_project_root", lambda: str(repo_root))
monkeypatch.setattr(
settings_paths, "user_config_dir", lambda *_, **__: str(user_dir)
)
resolved = settings_paths.ensure_settings_file()
assert resolved == str(repo_settings)
assert repo_settings.exists()
assert not user_dir.exists()
def test_migrate_creates_default_library(manager):
libraries = manager.get_libraries()
assert "default" in libraries
assert manager.get_active_library_name() == "default"
assert libraries["default"].get("folder_paths", {}) == manager.settings.get(
"folder_paths", {}
)
def test_migrate_sanitizes_legacy_libraries(tmp_path, monkeypatch):
initial = {
"libraries": {"legacy": "not-a-dict"},
"active_library": "legacy",
"folder_paths": {"loras": ["/old"]},
}
manager = _create_manager_with_settings(tmp_path, monkeypatch, initial)
libraries = manager.get_libraries()
assert set(libraries.keys()) == {"legacy"}
payload = libraries["legacy"]
assert payload["folder_paths"] == {}
assert payload["default_lora_root"] == ""
assert payload["default_checkpoint_root"] == ""
assert payload["default_embedding_root"] == ""
assert payload["recipes_path"] == ""
assert manager.get_active_library_name() == "legacy"
def test_active_library_syncs_top_level_settings(tmp_path, monkeypatch):
initial = {
"libraries": {
"default": {
"folder_paths": {"loras": ["/loras"]},
"default_lora_root": "/loras",
"default_checkpoint_root": "/ckpt",
"default_embedding_root": "/embed",
"recipes_path": "/loras/recipes",
},
"studio": {
"folder_paths": {"loras": ["/studio"]},
"default_lora_root": "/studio",
"default_checkpoint_root": "/studio_ckpt",
"default_embedding_root": "/studio_embed",
"recipes_path": "/studio/custom-recipes",
},
},
"active_library": "studio",
# Drifted top-level values that should be corrected during init
"folder_paths": {"loras": ["/loras"]},
"default_lora_root": "/loras",
"default_checkpoint_root": "/ckpt",
"default_embedding_root": "/embed",
"recipes_path": "/loras/recipes",
}
manager = _create_manager_with_settings(tmp_path, monkeypatch, initial)
assert manager.get_active_library_name() == "studio"
assert manager.get("folder_paths")["loras"] == ["/studio"]
assert manager.get("default_lora_root") == "/studio"
assert manager.get("default_checkpoint_root") == "/studio_ckpt"
assert manager.get("default_embedding_root") == "/studio_embed"
assert manager.get("recipes_path") == "/studio/custom-recipes"
# Drift the top-level values again and ensure activate_library repairs them
manager.settings["folder_paths"] = {"loras": ["/loras"]}
manager.settings["default_lora_root"] = "/loras"
manager.settings["recipes_path"] = "/loras/recipes"
manager.activate_library("studio")
assert manager.get("folder_paths")["loras"] == ["/studio"]
assert manager.get("default_lora_root") == "/studio"
assert manager.get("recipes_path") == "/studio/custom-recipes"
def test_refresh_environment_variables_updates_stored_value(tmp_path, monkeypatch):
calls = []
def save_spy(self):
calls.append(self.settings.get("civitai_api_key"))
initial = {
"civitai_api_key": "stale",
"libraries": {
"default": {
"folder_paths": {},
"default_lora_root": "",
"default_checkpoint_root": "",
"default_embedding_root": "",
"recipes_path": "",
}
},
"active_library": "default",
}
monkeypatch.setenv("CIVITAI_API_KEY", "from-init")
manager = _create_manager_with_settings(
tmp_path, monkeypatch, initial, save_spy=save_spy
)
assert calls[-1] == "from-init"
monkeypatch.setenv("CIVITAI_API_KEY", "refreshed")
manager.refresh_environment_variables()
assert calls[-1] == "refreshed"
def test_upsert_library_creates_entry_and_activates(manager, tmp_path):
lora_dir = tmp_path / "loras"
lora_dir.mkdir()
manager.upsert_library(
"studio",
folder_paths={"loras": [str(lora_dir)]},
activate=True,
)
assert manager.get_active_library_name() == "studio"
libraries = manager.get_libraries()
stored_paths = libraries["studio"]["folder_paths"]["loras"]
normalized_stored_paths = [p.replace(os.sep, "/") for p in stored_paths]
assert str(lora_dir).replace(os.sep, "/") in normalized_stored_paths
def test_set_recipes_path_updates_active_library_entry(manager, tmp_path):
recipes_dir = tmp_path / "custom" / "recipes"
manager.set("recipes_path", str(recipes_dir))
assert manager.get("recipes_path") == str(recipes_dir.resolve())
assert (
manager.get_libraries()["default"]["recipes_path"]
== str(recipes_dir.resolve())
)
def test_set_recipes_path_migrates_existing_recipe_files(manager, tmp_path):
lora_root = tmp_path / "loras"
old_recipes_dir = lora_root / "recipes" / "nested"
old_recipes_dir.mkdir(parents=True)
manager.set("folder_paths", {"loras": [str(lora_root)]})
recipe_id = "recipe-1"
old_image_path = old_recipes_dir / f"{recipe_id}.webp"
old_json_path = old_recipes_dir / f"{recipe_id}.recipe.json"
old_image_path.write_bytes(b"image-bytes")
old_json_path.write_text(
json.dumps(
{
"id": recipe_id,
"file_path": str(old_image_path),
"title": "Recipe 1",
}
),
encoding="utf-8",
)
new_recipes_dir = tmp_path / "custom_recipes"
manager.set("recipes_path", str(new_recipes_dir))
migrated_image_path = new_recipes_dir / "nested" / f"{recipe_id}.webp"
migrated_json_path = new_recipes_dir / "nested" / f"{recipe_id}.recipe.json"
assert manager.get("recipes_path") == str(new_recipes_dir.resolve())
assert migrated_image_path.read_bytes() == b"image-bytes"
migrated_payload = json.loads(migrated_json_path.read_text(encoding="utf-8"))
assert migrated_payload["file_path"] == str(migrated_image_path)
assert not old_image_path.exists()
assert not old_json_path.exists()
def test_clearing_recipes_path_migrates_files_to_default_location(manager, tmp_path):
lora_root = tmp_path / "loras"
custom_recipes_dir = tmp_path / "custom_recipes"
old_recipes_dir = custom_recipes_dir / "nested"
old_recipes_dir.mkdir(parents=True)
manager.set("folder_paths", {"loras": [str(lora_root)]})
manager.settings["recipes_path"] = str(custom_recipes_dir)
recipe_id = "recipe-2"
old_image_path = old_recipes_dir / f"{recipe_id}.webp"
old_json_path = old_recipes_dir / f"{recipe_id}.recipe.json"
old_image_path.write_bytes(b"image-bytes")
old_json_path.write_text(
json.dumps(
{
"id": recipe_id,
"file_path": str(old_image_path),
"title": "Recipe 2",
}
),
encoding="utf-8",
)
manager.set("recipes_path", "")
fallback_recipes_dir = lora_root / "recipes"
migrated_image_path = fallback_recipes_dir / "nested" / f"{recipe_id}.webp"
migrated_json_path = fallback_recipes_dir / "nested" / f"{recipe_id}.recipe.json"
assert manager.get("recipes_path") == ""
assert migrated_image_path.read_bytes() == b"image-bytes"
migrated_payload = json.loads(migrated_json_path.read_text(encoding="utf-8"))
assert migrated_payload["file_path"] == str(migrated_image_path)
assert not old_image_path.exists()
assert not old_json_path.exists()
def test_moving_recipes_path_back_to_parent_directory_is_allowed(manager, tmp_path):
lora_root = tmp_path / "loras"
manager.set("folder_paths", {"loras": [str(lora_root)]})
source_recipes_dir = lora_root / "recipes" / "custom"
source_recipes_dir.mkdir(parents=True)
recipe_id = "recipe-parent"
old_image_path = source_recipes_dir / f"{recipe_id}.webp"
old_json_path = source_recipes_dir / f"{recipe_id}.recipe.json"
old_image_path.write_bytes(b"parent-bytes")
old_json_path.write_text(
json.dumps(
{
"id": recipe_id,
"file_path": str(old_image_path),
"title": "Recipe Parent",
}
),
encoding="utf-8",
)
manager.settings["recipes_path"] = str(source_recipes_dir)
manager.set("recipes_path", str(lora_root / "recipes"))
migrated_image_path = lora_root / "recipes" / f"{recipe_id}.webp"
migrated_json_path = lora_root / "recipes" / f"{recipe_id}.recipe.json"
assert manager.get("recipes_path") == str((lora_root / "recipes").resolve())
assert migrated_image_path.read_bytes() == b"parent-bytes"
migrated_payload = json.loads(migrated_json_path.read_text(encoding="utf-8"))
assert migrated_payload["file_path"] == str(migrated_image_path)
assert not old_image_path.exists()
assert not old_json_path.exists()
def test_set_recipes_path_rewrites_symlinked_recipe_metadata(manager, tmp_path):
real_recipes_dir = tmp_path / "real_recipes"
real_recipes_dir.mkdir()
symlink_recipes_dir = tmp_path / "linked_recipes"
symlink_recipes_dir.symlink_to(real_recipes_dir, target_is_directory=True)
manager.settings["recipes_path"] = str(symlink_recipes_dir)
manager.set("folder_paths", {"loras": [str(tmp_path / "loras")]})
recipe_id = "recipe-symlink"
old_image_path = real_recipes_dir / f"{recipe_id}.webp"
old_json_path = real_recipes_dir / f"{recipe_id}.recipe.json"
old_image_path.write_bytes(b"symlink-bytes")
old_json_path.write_text(
json.dumps(
{
"id": recipe_id,
"file_path": str(old_image_path),
"title": "Recipe Symlink",
}
),
encoding="utf-8",
)
new_recipes_dir = tmp_path / "migrated_recipes"
manager.set("recipes_path", str(new_recipes_dir))
migrated_image_path = new_recipes_dir / f"{recipe_id}.webp"
migrated_json_path = new_recipes_dir / f"{recipe_id}.recipe.json"
assert migrated_image_path.read_bytes() == b"symlink-bytes"
migrated_payload = json.loads(migrated_json_path.read_text(encoding="utf-8"))
assert migrated_payload["file_path"] == str(migrated_image_path)
assert not old_image_path.exists()
assert not old_json_path.exists()
def test_set_recipes_path_allows_cross_drive_migration(manager, tmp_path, monkeypatch):
# Windows regression: os.path.commonpath raises ValueError for paths on
# different drives (ntpath semantics). Cross-drive moves must succeed.
lora_root = tmp_path / "loras"
old_recipes_dir = lora_root / "recipes" / "nested"
old_recipes_dir.mkdir(parents=True)
manager.set("folder_paths", {"loras": [str(lora_root)]})
recipe_id = "recipe-cross-drive"
old_image_path = old_recipes_dir / f"{recipe_id}.webp"
old_json_path = old_recipes_dir / f"{recipe_id}.recipe.json"
old_image_path.write_bytes(b"image-bytes")
old_json_path.write_text(
json.dumps(
{
"id": recipe_id,
"file_path": str(old_image_path),
"title": "Recipe Cross Drive",
}
),
encoding="utf-8",
)
new_recipes_dir = tmp_path / "N_drive" / "AI" / "Library" / "Recipes"
# The effective current recipes dir (source of the migration) is
# lora_root/recipes — the nested subdirectory holds the recipe files.
source = str(lora_root / "recipes")
target = str(new_recipes_dir)
real_commonpath = os.path.commonpath
def fake_commonpath(paths):
# Simulate ntpath on Windows: a source/target pair on different
# drives shares no common root and raises ValueError.
if {source, target} <= set(paths):
raise ValueError("Paths don't have the same drive")
return real_commonpath(paths)
monkeypatch.setattr(os.path, "commonpath", fake_commonpath)
manager.set("recipes_path", str(new_recipes_dir))
migrated_image_path = new_recipes_dir / "nested" / f"{recipe_id}.webp"
migrated_json_path = new_recipes_dir / "nested" / f"{recipe_id}.recipe.json"
assert manager.get("recipes_path") == str(new_recipes_dir.resolve())
assert migrated_image_path.read_bytes() == b"image-bytes"
migrated_payload = json.loads(migrated_json_path.read_text(encoding="utf-8"))
assert migrated_payload["file_path"] == str(migrated_image_path)
assert not old_image_path.exists()
assert not old_json_path.exists()
def test_set_recipes_path_rejects_file_target(manager, tmp_path):
lora_root = tmp_path / "loras"
lora_root.mkdir()
manager.set("folder_paths", {"loras": [str(lora_root)]})
target_file = tmp_path / "not_a_directory"
target_file.write_text("blocked", encoding="utf-8")
with pytest.raises(ValueError, match="directory"):
manager.set("recipes_path", str(target_file))
assert manager.get("recipes_path") == ""
def test_extra_folder_paths_stored_separately(manager, tmp_path):
lora_dir = tmp_path / "loras"
extra_dir = tmp_path / "extra_loras"
lora_dir.mkdir()
extra_dir.mkdir()
manager.upsert_library(
"test_library",
folder_paths={"loras": [str(lora_dir)]},
extra_folder_paths={"loras": [str(extra_dir)]},
activate=True,
)
libraries = manager.get_libraries()
lib = libraries["test_library"]
# Verify folder_paths contains main path
assert str(lora_dir) in lib["folder_paths"]["loras"]
# Verify extra_folder_paths contains extra path
assert str(extra_dir) in lib["extra_folder_paths"]["loras"]
# Verify they are separate
assert str(extra_dir) not in lib["folder_paths"]["loras"]
def test_get_extra_folder_paths(manager, tmp_path):
extra_dir = tmp_path / "extra_loras"
extra_dir.mkdir()
manager.update_extra_folder_paths({"loras": [str(extra_dir)]})
extra_paths = manager.get_extra_folder_paths()
assert str(extra_dir) in extra_paths.get("loras", [])
def test_library_switch_preserves_extra_paths(manager, tmp_path):
"""Test that switching libraries preserves each library's extra paths."""
lora_dir1 = tmp_path / "lib1_loras"
extra_dir1 = tmp_path / "lib1_extra"
lora_dir2 = tmp_path / "lib2_loras"
extra_dir2 = tmp_path / "lib2_extra"
for directory in (lora_dir1, extra_dir1, lora_dir2, extra_dir2):
directory.mkdir()
manager.create_library(
"library1",
folder_paths={"loras": [str(lora_dir1)]},
extra_folder_paths={"loras": [str(extra_dir1)]},
activate=True,
)
manager.create_library(
"library2",
folder_paths={"loras": [str(lora_dir2)]},
extra_folder_paths={"loras": [str(extra_dir2)]},
)
assert manager.get_active_library_name() == "library1"
lib1 = manager.get_active_library()
assert str(lora_dir1) in lib1["folder_paths"]["loras"]
assert str(extra_dir1) in lib1["extra_folder_paths"]["loras"]
manager.activate_library("library2")
assert manager.get_active_library_name() == "library2"
lib2 = manager.get_active_library()
assert str(lora_dir2) in lib2["folder_paths"]["loras"]
assert str(extra_dir2) in lib2["extra_folder_paths"]["loras"]
def test_extra_paths_validation_no_overlap_with_other_libraries(manager, tmp_path):
"""Test that extra paths cannot overlap with other libraries' paths."""
lora_dir1 = tmp_path / "lib1_loras"
lora_dir1.mkdir()
manager.create_library(
"library1",
folder_paths={"loras": [str(lora_dir1)]},
activate=True,
)
extra_dir = tmp_path / "extra_loras"
extra_dir.mkdir()
manager.create_library(
"library2",
folder_paths={"loras": [str(extra_dir)]},
activate=True,
)
with pytest.raises(ValueError, match="already assigned to library"):
manager.update_extra_folder_paths({"loras": [str(lora_dir1)]})
def test_extra_paths_validation_no_overlap_with_active_primary_lora_root(
manager, tmp_path
):
"""Test that extra LoRA paths cannot overlap the active library primary LoRA roots."""
real_lora_dir = tmp_path / "loras_real"
real_lora_dir.mkdir()
lora_link = tmp_path / "loras_link"
lora_link.symlink_to(real_lora_dir, target_is_directory=True)
manager.create_library(
"library1",
folder_paths={"loras": [str(lora_link)]},
activate=True,
)
with pytest.raises(
ValueError, match="overlap with the active library's primary LoRA roots"
):
manager.update_extra_folder_paths({"loras": [str(real_lora_dir)]})
def test_extra_paths_validation_no_overlap_with_active_primary_lora_root_case_insensitive(
manager, monkeypatch, tmp_path
):
"""Overlap validation should treat differently-cased Windows-like paths as the same path."""
real_lora_dir = tmp_path / "loras_real"
real_lora_dir.mkdir()
lora_link = tmp_path / "loras_link"
lora_link.symlink_to(real_lora_dir, target_is_directory=True)
manager.create_library(
"library1",
folder_paths={"loras": [str(lora_link)]},
activate=True,
)
original_exists = settings_manager_module.os.path.exists
original_realpath = settings_manager_module.os.path.realpath
original_normcase = settings_manager_module.os.path.normcase
def fake_exists(path):
if isinstance(path, str) and path.lower() in {
str(lora_link).lower(),
str(real_lora_dir).lower(),
}:
return True
return original_exists(path)
def fake_realpath(path):
if isinstance(path, str) and path.lower() == str(lora_link).lower():
return str(real_lora_dir)
return original_realpath(path)
monkeypatch.setattr(settings_manager_module.os.path, "exists", fake_exists)
monkeypatch.setattr(settings_manager_module.os.path, "realpath", fake_realpath)
monkeypatch.setattr(
settings_manager_module.os.path,
"normcase",
lambda value: original_normcase(value).lower(),
)
with pytest.raises(
ValueError, match="overlap with the active library's primary LoRA roots"
):
manager.update_extra_folder_paths({"loras": [str(real_lora_dir).upper()]})
def test_extra_paths_validation_allows_missing_non_overlapping_lora_root(
manager, tmp_path
):
"""Missing non-overlapping extra LoRA paths should not be rejected."""
lora_dir = tmp_path / "loras"
lora_dir.mkdir()
missing_extra = tmp_path / "missing_loras"
manager.create_library(
"library1",
folder_paths={"loras": [str(lora_dir)]},
activate=True,
)
manager.update_extra_folder_paths({"loras": [str(missing_extra)]})
extra_paths = manager.get_extra_folder_paths()
assert extra_paths["loras"] == [str(missing_extra)]
def test_extra_paths_validation_rejects_primary_root_first_level_symlink_target(
manager, tmp_path
):
"""Extra LoRA paths should be rejected when already reachable via a first-level symlink under the primary root."""
lora_dir = tmp_path / "loras"
lora_dir.mkdir()
external_dir = tmp_path / "external_loras"
external_dir.mkdir()
link_dir = lora_dir / "link"
link_dir.symlink_to(external_dir, target_is_directory=True)
manager.create_library(
"library1",
folder_paths={"loras": [str(lora_dir)]},
activate=True,
)
with pytest.raises(
ValueError, match="overlap with the active library's primary LoRA roots"
):
manager.update_extra_folder_paths({"loras": [str(external_dir)]})
def test_delete_library_switches_active(manager, tmp_path):
other_dir = tmp_path / "other"
other_dir.mkdir()
manager.create_library(
"other",
folder_paths={"loras": [str(other_dir)]},
activate=True,
)
assert manager.get_active_library_name() == "other"
manager.delete_library("other")
assert manager.get_active_library_name() == "default"
def test_download_skip_base_models_are_normalized(manager):
manager.settings["download_skip_base_models"] = [
"SDXL 1.0",
"Invalid",
"SDXL 1.0",
"Pony",
"Other",
]
result = manager.get_download_skip_base_models()
assert result == ["SDXL 1.0", "Pony"]
assert manager.settings["download_skip_base_models"] == ["SDXL 1.0", "Pony"]
def test_setting_download_skip_base_models_normalizes_string_input(manager):
manager.set("download_skip_base_models", "SDXL 1.0, Pony; Invalid\nSDXL 1.0")
assert manager.get("download_skip_base_models") == ["SDXL 1.0", "Pony"]
def test_skip_previously_downloaded_model_versions_defaults_false(manager):
assert manager.get_skip_previously_downloaded_model_versions() is False
def test_skip_previously_downloaded_model_versions_coerces_string_input(manager):
manager.settings["skip_previously_downloaded_model_versions"] = "true"
assert manager.get_skip_previously_downloaded_model_versions() is True
assert manager.settings["skip_previously_downloaded_model_versions"] is True
def test_default_other_roots_stay_empty_without_other_folders(manager):
assert manager._get_default_settings()["default_other_roots"] == {}
manager.settings["default_other_roots"] = {}
manager.settings["folder_paths"] = {}
manager.settings["extra_folder_paths"] = {}
manager._auto_set_default_roots()
assert manager.get("default_other_roots") == {}
def test_other_models_disabled_by_default(manager):
assert manager.is_other_models_enabled() is False
assert manager.get_enabled_other_sub_types() == []
assert manager.is_other_sub_type_enabled("vae") is False
def test_default_enabled_other_sub_types_are_the_dependency_trio(manager):
"""clip_vision and controlnet are workflow-driven, so both stay opt-in."""
manager.settings["enable_other_models"] = True
assert manager.get_enabled_other_sub_types() == [
"vae",
"upscaler",
"text_encoder",
]
assert manager.is_other_sub_type_enabled("clip_vision") is False
assert manager.is_other_sub_type_enabled("controlnet") is False
def test_enabled_other_sub_types_falls_back_to_defaults(manager):
manager.settings["enable_other_models"] = True
manager.settings["enabled_other_sub_types"] = None
assert manager.get_enabled_other_sub_types() == [
"vae",
"upscaler",
"text_encoder",
]
def test_auto_set_default_other_roots_skipped_when_feature_off(manager):
manager.settings["enable_other_models"] = False
manager.settings["default_other_roots"] = {}
manager.settings["folder_paths"] = {"vae": ["/vae"]}
manager._auto_set_default_roots()
assert manager.get("default_other_roots") == {}
def test_set_enabled_other_sub_types_normalizes(manager):
manager.settings["enable_other_models"] = True
manager.set("enabled_other_sub_types", ["controlnet", "vae", "nope", "vae", 42])
assert manager.get("enabled_other_sub_types") == ["vae", "controlnet"]
assert manager.is_other_sub_type_enabled("vae") is True
assert manager.is_other_sub_type_enabled("upscaler") is False
def test_auto_set_default_other_roots(manager):
manager.settings["enable_other_models"] = True
manager.settings["default_other_roots"] = {}
# clip_vision is opt-in, so enable it explicitly for this iteration test.
manager.settings["enabled_other_sub_types"] = ["vae", "upscaler", "clip_vision"]
manager.settings["folder_paths"] = {
"vae": ["/vae"],
"upscale_models": ["/upscalers"],
"clip_vision": ["/clip_vision"],
}
manager._auto_set_default_roots()
roots = manager.get("default_other_roots")
assert roots["vae"] == "/vae"
assert roots["upscaler"] == "/upscalers"
assert roots["clip_vision"] == "/clip_vision"
# text_encoder has no configured folders -> no entry
assert "text_encoder" not in roots
assert "controlnet" not in roots
def test_auto_set_default_other_roots_text_encoder_dual_key_union(manager):
"""text_encoder candidates merge text_encoders and the legacy clip key."""
manager.settings["enable_other_models"] = True
manager.settings["default_other_roots"] = {}
manager.settings["folder_paths"] = {
"clip": ["/legacy-clip"],
"text_encoders": ["/text-encoders"],
}
manager._auto_set_default_roots()
roots = manager.get("default_other_roots")
assert roots["text_encoder"] in {"/legacy-clip", "/text-encoders"}
# A value pointing at either key's root is considered valid
manager.settings["default_other_roots"] = {"text_encoder": "/legacy-clip"}
manager._auto_set_default_roots()
assert manager.get("default_other_roots")["text_encoder"] == "/legacy-clip"
def test_auto_set_default_other_roots_repairs_stale(manager):
manager.settings["enable_other_models"] = True
manager.settings["default_other_roots"] = {"vae": "/stale-vae"}
manager.settings["folder_paths"] = {"vae": ["/vae"]}
manager._auto_set_default_roots()
assert manager.get("default_other_roots")["vae"] == "/vae"
def test_auto_set_default_other_roots_uses_extra_folder_paths(manager):
manager.settings["enable_other_models"] = True
manager.settings["default_other_roots"] = {}
manager.settings["folder_paths"] = {"vae": []}
manager.settings["extra_folder_paths"] = {"vae": ["/extra-vae"]}
manager._auto_set_default_roots()
assert manager.get("default_other_roots")["vae"] == "/extra-vae"
def test_set_default_other_roots_syncs_active_library(manager):
manager.set("default_other_roots", {"vae": "/vae"})
libraries = manager.get_libraries()
active = manager.get_active_library_name()
assert libraries[active]["default_other_roots"] == {"vae": "/vae"}
assert manager.get("default_other_roots") == {"vae": "/vae"}
def test_set_default_other_roots_rejects_illegal_sub_type(manager):
with pytest.raises(ValueError, match="Unknown other-model sub-type"):
manager.set("default_other_roots", {"vae": "/vae", "lora": "/loras"})
def test_set_default_other_roots_normalizes_values(manager):
manager.set("default_other_roots", {"vae": " /vae ", "upscaler": ""})
assert manager.get("default_other_roots") == {"vae": "/vae"}
def test_upsert_library_passthrough_default_other_roots(manager, tmp_path):
manager.upsert_library(
"studio",
folder_paths={"loras": ["/studio/loras"], "vae": ["/studio/vae"]},
default_other_roots={"vae": "/studio/vae"},
activate=True,
)
libraries = manager.get_libraries()
assert libraries["studio"]["default_other_roots"] == {"vae": "/studio/vae"}
assert manager.get("default_other_roots") == {"vae": "/studio/vae"}
# Omitting the argument preserves the stored value
manager.upsert_library("studio", folder_paths={"loras": ["/studio/loras"]})
libraries = manager.get_libraries()
assert libraries["studio"]["default_other_roots"] == {"vae": "/studio/vae"}
def test_library_switch_restores_default_other_roots(manager):
manager.set("default_other_roots", {"vae": "/default-vae"})
manager.create_library(
"studio",
folder_paths={"loras": ["/studio/loras"]},
default_other_roots={"vae": "/studio-vae"},
)
manager.activate_library("studio")
assert manager.get("default_other_roots") == {"vae": "/studio-vae"}
manager.activate_library("default")
assert manager.get("default_other_roots") == {"vae": "/default-vae"}
def test_migrate_sanitizes_legacy_libraries_includes_other_roots(tmp_path, monkeypatch):
initial = {
"libraries": {"legacy": "not-a-dict"},
"active_library": "legacy",
"folder_paths": {"loras": ["/old"]},
}
manager = _create_manager_with_settings(tmp_path, monkeypatch, initial)
payload = manager.get_libraries()["legacy"]
assert payload["default_other_roots"] == {}