mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-20 18:51:26 -03:00
feat(other-models): announce the feature only when folders are available
Other Models management is opt-in and its folders come from folder_paths.get_folder_paths(). In plugin mode ComfyUI registers vae, upscale_models, text_encoders, clip_vision and controlnet out of the box, so enabling the feature works immediately. Standalone only knows the keys present in settings.json.folder_paths, and that file is edited by hand - there is no UI for those keys - so a standalone user who followed the announcement banner reached "Enable Other Models" and then an empty page. Gate the announcement on the capability instead of on how the process was started: - Config.get_other_models_availability() probes every canonical other key (legacy clip collapses into text_encoders where the host exposes map_legacy) and reports which sub_types resolve to a folder that exists on disk. It deliberately ignores enable_other_models: the question is "could this work here at all?". An empty folder counts, because CivitAI downloads can target it. - /api/lm/settings exposes it as the derived, non-persisted other_models_paths_available flag; a probe failure yields null and the banner fails open. - BannerService only registers the announcement when the flag is not false. `=== false` (not falsy) keeps a cached/older payload working, and nothing is written to dismissed_banners, so the banner can return once folders exist. - The Other page grows an "enabled but nothing to scan" empty state driven by config.other_roots, showing the settings.json snippet for standalone and a pointer to ComfyUI model paths otherwise, plus an Open Settings action. It also covers the corner where only a non-default sub_type has a folder. Translate the six other.noPaths.* keys into all nine locales and record the new "folder key" / "on disk" terminology in the i18n guidelines. Backend tests and pytest tests/i18n could not run in this environment (no pytest/platformdirs); the probe was exercised against a stubbed folder_paths. Frontend: 120 files / 1101 JS tests passed.
This commit is contained in:
@@ -414,3 +414,74 @@ class TestOtherRootsWiring:
|
||||
config._rebuild_preview_roots()
|
||||
|
||||
assert config.is_preview_path_allowed(str(vae_dir / "model.preview.png"))
|
||||
|
||||
|
||||
class TestOtherModelsAvailability:
|
||||
"""Config.get_other_models_availability ignores the opt-in toggle.
|
||||
|
||||
It answers "could Other Models work here at all?", which the settings
|
||||
payload and the announcement banner use to avoid promising a page that
|
||||
cannot list anything.
|
||||
"""
|
||||
|
||||
def _stub_folder_paths(self, monkeypatch, mapping):
|
||||
def get_folder_paths(key):
|
||||
value = mapping.get(key, [])
|
||||
return [value] if isinstance(value, str) else list(value)
|
||||
|
||||
monkeypatch.setattr(
|
||||
config_module.folder_paths, "get_folder_paths", get_folder_paths
|
||||
)
|
||||
# No host alias rewriting: every key stays independently queryable,
|
||||
# which is what the standalone mock does.
|
||||
monkeypatch.delattr(config_module.folder_paths, "map_legacy", raising=False)
|
||||
|
||||
def test_reports_available_when_a_folder_exists(self, monkeypatch, tmp_path):
|
||||
vae_dir = tmp_path / "vae"
|
||||
vae_dir.mkdir()
|
||||
self._stub_folder_paths(monkeypatch, {"vae": str(vae_dir)})
|
||||
|
||||
# The feature stays off on purpose: availability must not depend on it.
|
||||
get_settings_manager().set("enable_other_models", False)
|
||||
|
||||
availability = _make_config().get_other_models_availability()
|
||||
|
||||
assert availability["available"] is True
|
||||
assert availability["sub_types"] == {"vae": [_normalize(str(vae_dir))]}
|
||||
|
||||
def test_counts_an_empty_but_existing_folder(self, monkeypatch, tmp_path):
|
||||
vae_dir = tmp_path / "vae"
|
||||
vae_dir.mkdir()
|
||||
self._stub_folder_paths(monkeypatch, {"vae": str(vae_dir)})
|
||||
|
||||
availability = _make_config().get_other_models_availability()
|
||||
|
||||
assert availability["available"] is True
|
||||
|
||||
def test_ignores_missing_folders(self, monkeypatch, tmp_path):
|
||||
self._stub_folder_paths(
|
||||
monkeypatch, {"vae": str(tmp_path / "does-not-exist")}
|
||||
)
|
||||
|
||||
availability = _make_config().get_other_models_availability()
|
||||
|
||||
assert availability == {"available": False, "sub_types": {}}
|
||||
|
||||
def test_reports_unavailable_without_any_configuration(self, monkeypatch):
|
||||
self._stub_folder_paths(monkeypatch, {})
|
||||
|
||||
availability = _make_config().get_other_models_availability()
|
||||
|
||||
assert availability == {"available": False, "sub_types": {}}
|
||||
|
||||
def test_merges_legacy_clip_key_into_text_encoder(self, monkeypatch, tmp_path):
|
||||
clip_dir = tmp_path / "clip"
|
||||
clip_dir.mkdir()
|
||||
self._stub_folder_paths(monkeypatch, {"clip": [str(clip_dir)]})
|
||||
|
||||
availability = _make_config().get_other_models_availability()
|
||||
|
||||
assert availability["available"] is True
|
||||
assert availability["sub_types"] == {
|
||||
"text_encoder": [_normalize(str(clip_dir))]
|
||||
}
|
||||
|
||||
@@ -214,6 +214,7 @@ describe('BannerService', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
state.global.settings.enable_other_models = false;
|
||||
state.global.settings.other_models_paths_available = true;
|
||||
});
|
||||
|
||||
it('announces the feature while it is switched off', () => {
|
||||
@@ -225,6 +226,25 @@ describe('BannerService', () => {
|
||||
.toContain('Other Models Management is available');
|
||||
});
|
||||
|
||||
it('stays silent when the host exposes no other-model folders', () => {
|
||||
// Standalone installs without the folder_paths keys in
|
||||
// settings.json would land on an empty page, so do not announce.
|
||||
state.global.settings.other_models_paths_available = false;
|
||||
|
||||
prepareBanner();
|
||||
|
||||
expect(bannerElement()).toBeNull();
|
||||
expect(bannerService.banners.has(OTHER_MODELS_BANNER_ID)).toBe(false);
|
||||
});
|
||||
|
||||
it('still announces when availability is unknown (older payload)', () => {
|
||||
delete state.global.settings.other_models_paths_available;
|
||||
|
||||
prepareBanner();
|
||||
|
||||
expect(bannerElement()).not.toBeNull();
|
||||
});
|
||||
|
||||
it('stays silent once the feature is enabled', () => {
|
||||
state.global.settings.enable_other_models = true;
|
||||
|
||||
|
||||
@@ -22,7 +22,10 @@ describe('Other Models disabled page', () => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
initializeAppMock.mockResolvedValue(undefined);
|
||||
document.body.innerHTML = '<button id="enableOtherModelsBtn"></button>';
|
||||
document.body.innerHTML = [
|
||||
'<button id="enableOtherModelsBtn"></button>',
|
||||
'<button id="openOtherModelsSettingsBtn"></button>',
|
||||
].join('');
|
||||
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { ...originalLocation, reload: vi.fn() },
|
||||
@@ -43,12 +46,24 @@ describe('Other Models disabled page', () => {
|
||||
writable: true,
|
||||
});
|
||||
delete global.fetch;
|
||||
delete window.modalManager;
|
||||
});
|
||||
|
||||
it('boots the shared app core so the header stays usable', () => {
|
||||
expect(initializeAppMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('opens the Library settings from the no-folders state', () => {
|
||||
const showModal = vi.fn();
|
||||
window.modalManager = { showModal };
|
||||
|
||||
document.getElementById('openOtherModelsSettingsBtn').dispatchEvent(
|
||||
new MouseEvent('click', { bubbles: true }),
|
||||
);
|
||||
|
||||
expect(showModal).toHaveBeenCalledWith('settingsModal');
|
||||
});
|
||||
|
||||
it('enables Other Models through the settings API and reloads', async () => {
|
||||
global.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
'civitai_api_key_set': True,
|
||||
'language': 'en',
|
||||
'llm_api_key_set': False,
|
||||
'other_models_paths_available': False,
|
||||
'theme': 'dark',
|
||||
}),
|
||||
'success': True,
|
||||
|
||||
@@ -98,17 +98,27 @@ def test_validate_rejects_switched_off_sub_type():
|
||||
assert handler._validate_civitai_model_type("Upscaler") is False
|
||||
|
||||
|
||||
def test_page_context_reports_feature_state():
|
||||
def test_page_context_reports_feature_state(monkeypatch):
|
||||
from py.config import config
|
||||
from py.services.settings_manager import get_settings_manager
|
||||
|
||||
manager = get_settings_manager()
|
||||
handler = OtherRoutes()
|
||||
provider = handler._get_page_context_provider()
|
||||
|
||||
assert provider(None) == {"other_disabled": False}
|
||||
monkeypatch.setattr(config, "other_roots", ["/models/vae"], raising=False)
|
||||
context = provider(None)
|
||||
assert context["other_disabled"] is False
|
||||
assert context["other_no_paths"] is False
|
||||
|
||||
# Enabled but nothing resolved: the page must explain how to fix it.
|
||||
monkeypatch.setattr(config, "other_roots", [], raising=False)
|
||||
context = provider(None)
|
||||
assert context["other_disabled"] is False
|
||||
assert context["other_no_paths"] is True
|
||||
|
||||
manager.set("enable_other_models", False)
|
||||
assert provider(None) == {"other_disabled": True}
|
||||
assert provider(None) == {"other_disabled": True, "other_no_paths": False}
|
||||
|
||||
|
||||
def test_get_expected_model_types_mentions_supported_types():
|
||||
|
||||
Reference in New Issue
Block a user