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:
Will Miao
2026-09-13 21:47:16 +08:00
parent adeb40bfff
commit 84146b62fd
22 changed files with 357 additions and 7 deletions
+19
View File
@@ -27,6 +27,12 @@ Locales: `en`, `zh-CN`, `zh-TW`, `ja`, `ko`, `fr`, `de`, `es`, `ru`, `he` (RTL).
> default set — keep all three enumerating the full five (`VAE / upscaler / text encoder /
> CLIP vision / ControlNet` in `en`; locale slash-list casing follows each file's existing
> `VAE / Upscaler / Text Encoder / …` style, de compounds as `CLIP-Vision- und ControlNet-Ordner`).
>
> **Status (2026-09, "no folders found" state):** the Other Models page gained an *enabled but
> nothing to scan* empty state with 6 new keys (`other.noPaths.*`); translated in all 9 locales
> in the same pass. The `folder_paths` JSON snippet shown in that state lives in
> `templates/other.html`, **not** in the locale files, so it is never translated — only the
> surrounding prose is. Terminology added in §2.
---
@@ -273,6 +279,19 @@ In prose these names sit next to localized nouns the same way `Diffusion Model`
each locale's `header.filter.modelTypes` rendering (zh `管理的模型类型`, ja `管理するモデルタイプ`,
de `Verwaltete Modelltypen`, …).
The "no folders found" empty state (`other.noPaths.*`) uses two phrases that must stay
consistent whenever that copy is edited. `folder key` means the `folder_paths` key name
(`vae`, `upscale_models`, … — Latin per the table above); `on disk` means the folder must
physically exist:
| Phrase | Rendering |
|---|---|
| folder key | zh-CN 文件夹键 · zh-TW 資料夾鍵 · ja フォルダーキー · ko 폴더 키 · fr clé de dossier · de Ordnerschlüssel · es clave de carpeta · ru ключ папки · he מפתח תיקייה |
| on disk | zh-CN 在磁盘上 · zh-TW 在磁碟上 · ja ディスク上 · ko 디스크에 · fr sur le disque · de auf dem Datenträger · es en el disco · ru на диске · he בדיסק |
`settings.json` and `ComfyUI` stay verbatim in every locale; "reload this page" / "restart
LoRA Manager" reuse each locale's existing restart wording (`settings.extraFolderPaths.*`).
---
## 3. Cross-cutting confusion hot-spots (must-fix list)
+8
View File
@@ -1232,6 +1232,14 @@
"enableFailed": "Aktivierung weiterer Modelle fehlgeschlagen",
"downloadBlocked": "Die Verwaltung weiterer Modelle ist für diesen Modelltyp deaktiviert. Aktivieren Sie sie unter Einstellungen > Bibliothek, um diese Datei herunterzuladen.",
"enableAction": "Weitere Modelle aktivieren"
},
"noPaths": {
"title": "Keine Ordner für weitere Modelle gefunden",
"descriptionStandalone": "Die Verwaltung weiterer Modelle ist aktiviert, aber keiner der konfigurierten Modellordner existiert auf dem Datenträger. Fügen Sie die unten stehenden Ordnerpfade zu settings.json hinzu und starten Sie LoRA Manager neu.",
"hintStandalone": "Nur die oben aufgeführten Ordnerschlüssel werden gescannt; nicht benötigte Schlüssel können weggelassen werden.",
"descriptionComfyUI": "Die Verwaltung weiterer Modelle ist aktiviert, aber keiner der konfigurierten Modellordner existiert auf dem Datenträger. Fügen Sie die entsprechenden Modellordner zu Ihren ComfyUI-Modellpfaden hinzu und laden Sie diese Seite neu.",
"hintComfyUI": "Weitere Modelle werden aus den Ordnern vae, upscale_models, text_encoders, clip_vision und controlnet von ComfyUI gelesen.",
"openSettings": "Einstellungen öffnen"
}
},
"sidebar": {
+8
View File
@@ -1232,6 +1232,14 @@
"enableFailed": "Failed to enable Other Models",
"downloadBlocked": "Other Models management is disabled for this model type. Enable it in Settings > Library to download this file.",
"enableAction": "Enable Other Models"
},
"noPaths": {
"title": "No other-model folders found",
"descriptionStandalone": "Other Models management is on, but none of the configured model folders exist on disk. Add the folder paths below to settings.json and restart LoRA Manager.",
"hintStandalone": "Only the folder keys listed above are scanned; keys you do not need can be omitted.",
"descriptionComfyUI": "Other Models management is on, but none of the configured model folders exist on disk. Add the matching model folders to your ComfyUI model paths, then reload this page.",
"hintComfyUI": "Other models are read from ComfyUI's vae, upscale_models, text_encoders, clip_vision and controlnet folders.",
"openSettings": "Open Settings"
}
},
"sidebar": {
+8
View File
@@ -1232,6 +1232,14 @@
"enableFailed": "No se pudieron activar los otros modelos",
"downloadBlocked": "La gestión de otros modelos está desactivada para este tipo de modelo. Actívala en Configuración > Biblioteca para descargar este archivo.",
"enableAction": "Activar otros modelos"
},
"noPaths": {
"title": "No se encontraron carpetas de otros modelos",
"descriptionStandalone": "La gestión de otros modelos está activada, pero ninguna de las carpetas de modelos configuradas existe en el disco. Añade las rutas de carpetas de abajo a settings.json y reinicia LoRA Manager.",
"hintStandalone": "Solo se escanean las claves de carpeta listadas arriba; las claves que no necesites puedes omitirlas.",
"descriptionComfyUI": "La gestión de otros modelos está activada, pero ninguna de las carpetas de modelos configuradas existe en el disco. Añade las carpetas de modelos correspondientes a tus rutas de modelos de ComfyUI y recarga esta página.",
"hintComfyUI": "Los otros modelos se leen de las carpetas vae, upscale_models, text_encoders, clip_vision y controlnet de ComfyUI.",
"openSettings": "Abrir configuración"
}
},
"sidebar": {
+8
View File
@@ -1232,6 +1232,14 @@
"enableFailed": "Échec de lactivation des autres modèles",
"downloadBlocked": "La gestion des autres modèles est désactivée pour ce type de modèle. Activez-la dans Paramètres > Bibliothèque pour télécharger ce fichier.",
"enableAction": "Activer les autres modèles"
},
"noPaths": {
"title": "Aucun dossier dautres modèles trouvé",
"descriptionStandalone": "La gestion des autres modèles est activée, mais aucun des dossiers de modèles configurés nexiste sur le disque. Ajoutez les chemins de dossiers ci-dessous à settings.json, puis redémarrez LoRA Manager.",
"hintStandalone": "Seules les clés de dossiers listées ci-dessus sont analysées ; les clés inutiles peuvent être omises.",
"descriptionComfyUI": "La gestion des autres modèles est activée, mais aucun des dossiers de modèles configurés nexiste sur le disque. Ajoutez les dossiers de modèles correspondants à vos chemins de modèles ComfyUI, puis rechargez cette page.",
"hintComfyUI": "Les autres modèles sont lus depuis les dossiers vae, upscale_models, text_encoders, clip_vision et controlnet de ComfyUI.",
"openSettings": "Ouvrir les paramètres"
}
},
"sidebar": {
+8
View File
@@ -1232,6 +1232,14 @@
"enableFailed": "הפעלת המודלים האחרים נכשלה",
"downloadBlocked": "ניהול המודלים האחרים מושבת עבור סוג מודל זה. הפעל אותו בהגדרות > ספרייה כדי להוריד קובץ זה.",
"enableAction": "הפעל מודלים אחרים"
},
"noPaths": {
"title": "לא נמצאו תיקיות של מודלים אחרים",
"descriptionStandalone": "ניהול המודלים האחרים פועל, אך אף אחת מתיקיות המודלים המוגדרות אינה קיימת בדיסק. הוסף את נתיבי התיקיות שלמטה ל-settings.json והפעל מחדש את LoRA Manager.",
"hintStandalone": "רק מפתחות התיקיות המפורטים למעלה נסרקים; ניתן להשמיט מפתחות שאינך צריך.",
"descriptionComfyUI": "ניהול המודלים האחרים פועל, אך אף אחת מתיקיות המודלים המוגדרות אינה קיימת בדיסק. הוסף את תיקיות המודלים המתאימות לנתיבי המודלים של ComfyUI וטען מחדש עמוד זה.",
"hintComfyUI": "מודלים אחרים נקראים מתיקיות vae, upscale_models, text_encoders, clip_vision ו-controlnet של ComfyUI.",
"openSettings": "פתח הגדרות"
}
},
"sidebar": {
+8
View File
@@ -1232,6 +1232,14 @@
"enableFailed": "その他のモデルの有効化に失敗しました",
"downloadBlocked": "このモデルタイプではその他のモデル管理が無効です。このファイルをダウンロードするには「設定 > ライブラリ」で有効にしてください。",
"enableAction": "その他のモデルを有効にする"
},
"noPaths": {
"title": "その他のモデルのフォルダーが見つかりません",
"descriptionStandalone": "その他のモデル管理はオンですが、設定されたモデルフォルダーがディスク上に存在しません。以下のフォルダーパスをsettings.jsonに追加し、LoRA Managerを再起動してください。",
"hintStandalone": "スキャンされるのは上記のフォルダーキーのみです。不要なキーは省略できます。",
"descriptionComfyUI": "その他のモデル管理はオンですが、設定されたモデルフォルダーがディスク上に存在しません。該当するモデルフォルダーをComfyUIのモデルパスに追加し、このページを再読み込みしてください。",
"hintComfyUI": "その他のモデルは、ComfyUIのvae、upscale_models、text_encoders、clip_vision、controlnetフォルダーから読み込まれます。",
"openSettings": "設定を開く"
}
},
"sidebar": {
+8
View File
@@ -1232,6 +1232,14 @@
"enableFailed": "기타 모델 활성화 실패",
"downloadBlocked": "이 모델 유형에 대해서는 기타 모델 관리가 비활성화되어 있습니다. 이 파일을 다운로드하려면 설정 > 라이브러리에서 활성화하세요.",
"enableAction": "기타 모델 활성화"
},
"noPaths": {
"title": "기타 모델 폴더를 찾을 수 없습니다",
"descriptionStandalone": "기타 모델 관리가 켜져 있지만, 설정된 모델 폴더가 디스크에 존재하지 않습니다. 아래 폴더 경로를 settings.json에 추가한 뒤 LoRA Manager를 재시작하세요.",
"hintStandalone": "위에 나열된 폴더 키만 스캔됩니다. 필요 없는 키는 생략할 수 있습니다.",
"descriptionComfyUI": "기타 모델 관리가 켜져 있지만, 설정된 모델 폴더가 디스크에 존재하지 않습니다. 해당 모델 폴더를 ComfyUI 모델 경로에 추가한 뒤 이 페이지를 새로 고침하세요.",
"hintComfyUI": "기타 모델은 ComfyUI의 vae, upscale_models, text_encoders, clip_vision, controlnet 폴더에서 읽어옵니다.",
"openSettings": "설정 열기"
}
},
"sidebar": {
+8
View File
@@ -1232,6 +1232,14 @@
"enableFailed": "Не удалось включить другие модели",
"downloadBlocked": "Управление другими моделями отключено для этого типа моделей. Включите его в разделе «Настройки > Библиотека», чтобы загрузить этот файл.",
"enableAction": "Включить другие модели"
},
"noPaths": {
"title": "Папки других моделей не найдены",
"descriptionStandalone": "Управление другими моделями включено, но ни одна из настроенных папок моделей не существует на диске. Добавьте указанные ниже пути к папкам в settings.json и перезапустите LoRA Manager.",
"hintStandalone": "Сканируются только перечисленные выше ключи папок; ненужные ключи можно опустить.",
"descriptionComfyUI": "Управление другими моделями включено, но ни одна из настроенных папок моделей не существует на диске. Добавьте соответствующие папки моделей в пути к моделям ComfyUI и перезагрузите эту страницу.",
"hintComfyUI": "Другие модели читаются из папок vae, upscale_models, text_encoders, clip_vision и controlnet в ComfyUI.",
"openSettings": "Открыть настройки"
}
},
"sidebar": {
+8
View File
@@ -1232,6 +1232,14 @@
"enableFailed": "启用其他模型失败",
"downloadBlocked": "其他模型管理已对此模型类型禁用。请在“设置 > 库”中启用以下载此文件。",
"enableAction": "启用其他模型"
},
"noPaths": {
"title": "未找到其他模型文件夹",
"descriptionStandalone": "其他模型管理已开启,但配置的模型文件夹在磁盘上都不存在。请将下面的文件夹路径添加到 settings.json,然后重启 LoRA Manager。",
"hintStandalone": "只会扫描上面列出的文件夹键;不需要的键可以省略。",
"descriptionComfyUI": "其他模型管理已开启,但配置的模型文件夹在磁盘上都不存在。请将对应的模型文件夹添加到 ComfyUI 的模型路径,然后重新加载此页面。",
"hintComfyUI": "其他模型从 ComfyUI 的 vae、upscale_models、text_encoders、clip_vision 和 controlnet 文件夹中读取。",
"openSettings": "打开设置"
}
},
"sidebar": {
+8
View File
@@ -1232,6 +1232,14 @@
"enableFailed": "啟用其他模型失敗",
"downloadBlocked": "其他模型管理已對此模型類型停用。請在「設定 > 模型庫」中啟用以下載此檔案。",
"enableAction": "啟用其他模型"
},
"noPaths": {
"title": "找不到其他模型資料夾",
"descriptionStandalone": "其他模型管理已開啟,但設定的模型資料夾在磁碟上都不存在。請將下方的資料夾路徑加入 settings.json,然後重新啟動 LoRA Manager。",
"hintStandalone": "只會掃描上方列出的資料夾鍵;不需要的鍵可以省略。",
"descriptionComfyUI": "其他模型管理已開啟,但設定的模型資料夾在磁碟上都不存在。請將對應的模型資料夾加入 ComfyUI 的模型路徑,然後重新載入此頁面。",
"hintComfyUI": "其他模型會從 ComfyUI 的 vae、upscale_models、text_encoders、clip_vision 和 controlnet 資料夾讀取。",
"openSettings": "開啟設定"
}
},
"sidebar": {
+48
View File
@@ -1494,6 +1494,54 @@ class Config:
self.other_roots = self._init_other_paths()
self._rebuild_preview_roots()
def get_other_models_availability(self) -> Dict[str, Any]:
"""Report the other-model folders the host can actually expose.
Independent of the opt-in ``enable_other_models`` toggle: this answers
"could Other Models management work here at all?". ComfyUI mode almost
always has these folder keys registered, while standalone mode only
knows the keys present in ``settings.json.folder_paths`` - so the UI
uses this to decide whether announcing the feature would be actionable.
Returns:
``{"available": bool, "sub_types": {sub_type: [existing roots]}}``.
A folder only counts when it exists on disk; an empty folder still
counts because CivitAI downloads can target it.
"""
sub_types: Dict[str, List[str]] = {}
try:
keys = self._collapse_legacy_folder_keys(
list(OTHER_MODEL_FOLDER_SUBTYPES.keys())
)
except Exception: # pragma: no cover - defensive
keys = list(OTHER_MODEL_FOLDER_SUBTYPES.keys())
for key in keys:
sub_type = OTHER_MODEL_FOLDER_SUBTYPES.get(key)
if not sub_type:
continue
try:
raw_paths = folder_paths.get_folder_paths(key)
except Exception as exc:
logger.debug("Error probing folder paths for '%s': %s", key, exc)
continue
bucket = sub_types.setdefault(sub_type, [])
for root in sorted(
self._dedupe_existing_paths(raw_paths or []).values(),
key=lambda path: path.lower(),
):
if root not in bucket:
bucket.append(root)
available_sub_types = {
sub_type: roots for sub_type, roots in sub_types.items() if roots
}
return {
"available": bool(available_sub_types),
"sub_types": available_sub_types,
}
def get_preview_static_url(self, preview_path: str) -> str:
if not preview_path:
return ""
+16
View File
@@ -1559,6 +1559,22 @@ class SettingsHandler:
response_data["civitai_api_key_set"] = bool(raw_key)
raw_llm_key = self._settings.get("llm_api_key")
response_data["llm_api_key_set"] = bool(raw_llm_key)
# Derived capability flag (not persisted): whether the host exposes
# any other-model folder at all. Standalone installs only know the
# folder_paths keys present in settings.json, so the announcement
# banner uses this to avoid promising a page that cannot list
# anything.
try:
availability = config.get_other_models_availability()
response_data["other_models_paths_available"] = bool(
availability.get("available")
)
except Exception as availability_error: # pragma: no cover - defensive
logger.debug(
"Could not resolve Other Models availability: %s",
availability_error,
)
response_data["other_models_paths_available"] = None
settings_file = getattr(self._settings, "settings_file", None)
if settings_file:
response_data["settings_file"] = settings_file
+13 -1
View File
@@ -1,4 +1,5 @@
import logging
import os
from typing import Any, Dict, List
from aiohttp import web
@@ -75,7 +76,18 @@ class OtherRoutes(BaseModelRoutes):
return self._page_context_for_other
def _page_context_for_other(self, request: web.Request) -> Dict[str, Any]:
return {"other_disabled": not self._settings.is_other_models_enabled()}
if not self._settings.is_other_models_enabled():
return {"other_disabled": True, "other_no_paths": False}
# Enabled but nothing to scan: folder paths for the managed sub_types
# resolved to no existing folder. Render an actionable empty state
# instead of an apparently broken empty grid.
standalone_mode = os.environ.get("LORA_MANAGER_STANDALONE", "0") == "1"
return {
"other_disabled": False,
"other_no_paths": not bool(config.other_roots),
"standalone_mode": standalone_mode,
}
def _get_expected_model_types(self) -> str:
"""Get expected model types string for error messages"""
+7
View File
@@ -553,6 +553,13 @@ class BannerService {
if (state.global.settings.enable_other_models) {
return;
}
// Only announce when the host can actually resolve other-model folders.
// Standalone installs only know the folder_paths keys present in
// settings.json, so announcing there would land the user on an empty
// page. `=== false` (not falsy) keeps older payloads working.
if (state.global.settings.other_models_paths_available === false) {
return;
}
if (this.isBannerDismissed(OTHER_MODELS_BANNER_ID)) {
return;
}
+18 -1
View File
@@ -1,11 +1,14 @@
import { appCore } from './core.js';
import { showToast } from './utils/uiHelpers.js';
import { enableOtherModels } from './utils/otherModels.js';
import { enableOtherModels, openOtherModelsSettings } from './utils/otherModels.js';
/**
* Other Models is an opt-in feature. While it is disabled this page renders an
* empty state whose button turns the feature on; the backend then rebuilds the
* other-model roots and starts scanning, so a reload lands on the real page.
*
* The same module backs the "enabled but no folders found" state, where the
* only useful action is jumping to Settings instead of enabling anything.
*/
async function handleEnableClick() {
const button = document.getElementById('enableOtherModelsBtn');
@@ -20,6 +23,15 @@ async function handleEnableClick() {
}
}
/**
* Open Settings on the Library section for the "no folders found" state, so a
* misconfigured install can be fixed without hand-editing unknown keys.
*/
function handleOpenSettingsClick(event) {
event.preventDefault();
openOtherModelsSettings();
}
async function initializeOtherDisabledPage() {
// appCore.initialize() wires the shared header (theme, settings modal,
// language) so this page is not a dead end.
@@ -29,6 +41,11 @@ async function initializeOtherDisabledPage() {
if (button) {
button.addEventListener('click', handleEnableClick);
}
const settingsButton = document.getElementById('openOtherModelsSettingsBtn');
if (settingsButton) {
settingsButton.addEventListener('click', handleOpenSettingsClick);
}
}
document.addEventListener('DOMContentLoaded', initializeOtherDisabledPage);
+35 -1
View File
@@ -51,6 +51,18 @@
opacity: 0.6;
cursor: default;
}
.other-no-paths-config {
margin: 4px 0 0;
padding: 12px 16px;
max-width: 520px;
overflow-x: auto;
text-align: left;
font-size: 12px;
line-height: 1.5;
border-radius: 6px;
background: rgba(127, 127, 127, 0.15);
border: 1px solid rgba(127, 127, 127, 0.25);
}
</style>
{% endblock %}
@@ -109,6 +121,28 @@
</button>
<p class="other-disabled-hint">{{ t('other.disabled.hint') }}</p>
</div>
{% elif other_no_paths %}
<div class="other-disabled">
<i class="fas fa-folder-open"></i>
<h2>{{ t('other.noPaths.title') }}</h2>
{% if standalone_mode %}
<p>{{ t('other.noPaths.descriptionStandalone') }}</p>
<pre class="other-no-paths-config"><code>"folder_paths": {
"vae": ["/path/to/vae"],
"upscale_models": ["/path/to/upscale_models"],
"text_encoders": ["/path/to/text_encoders"],
"clip_vision": ["/path/to/clip_vision"],
"controlnet": ["/path/to/controlnet"]
}</code></pre>
<p class="other-disabled-hint">{{ t('other.noPaths.hintStandalone') }}</p>
{% else %}
<p>{{ t('other.noPaths.descriptionComfyUI') }}</p>
<p class="other-disabled-hint">{{ t('other.noPaths.hintComfyUI') }}</p>
{% endif %}
<button id="openOtherModelsSettingsBtn" type="button">
<i class="fas fa-cog"></i> {{ t('other.noPaths.openSettings') }}
</button>
</div>
{% else %}
<div class="sticky-topbar">
{% include 'components/controls.html' %}
@@ -129,7 +163,7 @@
{% endblock %}
{% block main_script %}
{% if other_disabled %}
{% if other_disabled or other_no_paths %}
<script type="module" src="/loras_static/js/other_disabled.js?v={{ version }}"></script>
{% else %}
<script type="module" src="/loras_static/js/other.js?v={{ version }}"></script>
+71
View File
@@ -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;
+16 -1
View File
@@ -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,
+13 -3
View File
@@ -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():