fix(config): keep unavailable model roots in the saved library paths

Starting ComfyUI with an external drive switched off erased that drive's path
from `settings.json`. `Config._dedupe_existing_paths()` drops paths that do not
exist at the moment the root list is built, and `save_folder_paths_to_settings()`
(plugin mode, called from `Config.__init__`) persisted `list(self.loras_roots)`
through `upsert_library(folder_paths=...)`, which REPLACES the library's paths.
One boot with the drive off was therefore enough to lose the configuration, not
just to hide the root — the same applied to checkpoints/unet/embeddings and the
other-model keys.

* `Config` now records the *configured* (existence-unfiltered) primary paths per
  model type while building the live lists, and the startup sync persists those.
  A path the host (plugin mode) or the library (standalone mode) no longer
  configures is still dropped, so removing a path keeps working.
* In standalone mode the host mock (`MockFolderPaths.get_folder_paths`) already
  filters non-existent paths out of settings.json, so the active library snapshot
  is the only record of what the user configured; `_remember_library_configured_paths()`
  reads it there and leaves plugin mode to ComfyUI's own unfiltered list.
* `_resolve_valid_default_root()` receives the configured paths in
  `allowed_paths`, so a `default_*_root` on a switched-off drive is no longer
  "repaired" onto another root.
* `_append_new_paths()` is the shared append-only union (order preserved, case
  insensitive) used by the sync.

Verified: a new config test builds the plugin-mode sync with one switched-off
drive, one readable root and one path the host dropped — the switched-off path
and the default root that points at it survive, the dropped path does not, and
the live `loras_roots` still holds only the readable root. tests/config 64 passed.
This commit is contained in:
Will Miao
2026-10-07 16:35:29 +08:00
parent 0941f99226
commit 08a8e54d87
2 changed files with 244 additions and 7 deletions
+63
View File
@@ -905,3 +905,66 @@ def test_save_paths_removes_stale_empty_default_when_comfyui_exists(
assert name == "comfyui"
assert payload["activate"] is True
assert fake_settings.active_library == "comfyui"
def test_save_paths_keeps_configured_root_of_a_switched_off_drive(
monkeypatch: pytest.MonkeyPatch, tmp_path
):
"""A root whose directory is missing right now must stay in the library.
`upsert_library(folder_paths=...)` replaces the stored paths, so persisting
the existence-filtered live list would erase a drive that happened to be
switched off when ComfyUI started. A path the host no longer configures must
still be dropped.
"""
folder_paths = _setup_config_environment(monkeypatch, tmp_path)
readable = folder_paths["loras"][0]
switched_off = str(tmp_path / "drive-Z" / "loras")
removed_by_user = str(tmp_path / "old-location" / "loras")
# The host still configures the switched-off drive; it no longer configures
# the old location (that one only survives in the stored library paths).
folder_paths["loras"] = [switched_off, readable]
class FakeSettingsService:
active_library = "comfyui"
name: str = ""
payload: Dict[str, Any] = {}
def get_libraries(self):
return {
"comfyui": {
"folder_paths": {
"loras": [removed_by_user, readable],
"checkpoints": [],
"unet": [],
"embeddings": [],
},
"default_lora_root": switched_off,
}
}
def rename_library(self, *_):
raise AssertionError("rename_library should not be invoked")
def get_active_library_name(self):
return self.active_library
def upsert_library(self, name: str, **payload):
self.name = name
self.payload = payload
fake_settings = FakeSettingsService()
monkeypatch.setattr(settings_manager_module, "settings", fake_settings)
config_instance = config_module.Config()
persisted = fake_settings.payload["folder_paths"]["loras"]
# The switched-off drive keeps its configured path...
assert switched_off.replace("\\", "/") in persisted
assert readable.replace("\\", "/") in persisted
# ...the path the host really dropped is gone...
assert removed_by_user.replace("\\", "/") not in persisted
# ...and the live list still only holds what is readable right now.
assert config_instance.loras_roots == [readable.replace("\\", "/")]
# The user's default root survives because it is merely unavailable.
assert fake_settings.payload["default_lora_root"] == switched_off.replace("\\", "/")