diff --git a/locales/de.json b/locales/de.json index 231dc3bb..65dde105 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1563,6 +1563,10 @@ "selectRootDirectory": "Wählen Sie ein Stammverzeichnis", "selectModelRoot": "Modell-Stammverzeichnis auswählen:", "selectTypeRoot": "{type}-Stammverzeichnis auswählen:", + "routingOverride": { + "label": "[TODO: Translate] Destination type:", + "tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download." + }, "targetFolderPath": "Zielordnerpfad:", "browseFolders": "Ordner durchsuchen:", "createNewFolder": "Neuen Ordner erstellen", diff --git a/locales/en.json b/locales/en.json index e28de79d..9b83bf13 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1563,6 +1563,10 @@ "selectRootDirectory": "Select a root directory", "selectModelRoot": "Select Model Root:", "selectTypeRoot": "Select {type} Root:", + "routingOverride": { + "label": "Destination type:", + "tooltip": "Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download." + }, "targetFolderPath": "Target Folder Path:", "browseFolders": "Browse Folders:", "createNewFolder": "Create new folder", diff --git a/locales/es.json b/locales/es.json index a4af9c81..20ba0a9d 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1563,6 +1563,10 @@ "selectRootDirectory": "Selecciona un directorio raíz", "selectModelRoot": "Seleccionar raíz del modelo:", "selectTypeRoot": "Seleccionar raíz de {type}:", + "routingOverride": { + "label": "[TODO: Translate] Destination type:", + "tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download." + }, "targetFolderPath": "Ruta de carpeta de destino:", "browseFolders": "Explorar carpetas:", "createNewFolder": "Crear nueva carpeta", diff --git a/locales/fr.json b/locales/fr.json index c87f7bbd..cbf4e1d2 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1563,6 +1563,10 @@ "selectRootDirectory": "Sélectionner un répertoire racine", "selectModelRoot": "Sélectionner la racine du modèle :", "selectTypeRoot": "Sélectionner la racine {type} :", + "routingOverride": { + "label": "[TODO: Translate] Destination type:", + "tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download." + }, "targetFolderPath": "Chemin du dossier cible :", "browseFolders": "Parcourir les dossiers :", "createNewFolder": "Créer un nouveau dossier", diff --git a/locales/he.json b/locales/he.json index f6c9915e..88dfbe9a 100644 --- a/locales/he.json +++ b/locales/he.json @@ -1563,6 +1563,10 @@ "selectRootDirectory": "בחר ספריית שורש", "selectModelRoot": "בחר שורש מודל:", "selectTypeRoot": "בחר שורש {type}:", + "routingOverride": { + "label": "[TODO: Translate] Destination type:", + "tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download." + }, "targetFolderPath": "נתיב תיקיית יעד:", "browseFolders": "דפדף בתיקיות:", "createNewFolder": "צור תיקייה חדשה", diff --git a/locales/ja.json b/locales/ja.json index 47372056..3314c791 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -1563,6 +1563,10 @@ "selectRootDirectory": "ルートディレクトリを選択", "selectModelRoot": "モデルルートを選択:", "selectTypeRoot": "{type}ルートを選択:", + "routingOverride": { + "label": "[TODO: Translate] Destination type:", + "tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download." + }, "targetFolderPath": "ターゲットフォルダパス:", "browseFolders": "フォルダを参照:", "createNewFolder": "新しいフォルダを作成", diff --git a/locales/ko.json b/locales/ko.json index dfe20317..b56aedd1 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -1563,6 +1563,10 @@ "selectRootDirectory": "루트 디렉토리를 선택하세요", "selectModelRoot": "모델 루트 선택:", "selectTypeRoot": "{type} 루트 선택:", + "routingOverride": { + "label": "[TODO: Translate] Destination type:", + "tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download." + }, "targetFolderPath": "대상 폴더 경로:", "browseFolders": "폴더 탐색:", "createNewFolder": "새 폴더 만들기", diff --git a/locales/ru.json b/locales/ru.json index 1c92c656..93f92f7b 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1563,6 +1563,10 @@ "selectRootDirectory": "Выберите корневую папку", "selectModelRoot": "Выберите корень моделей:", "selectTypeRoot": "Выберите корень {type}:", + "routingOverride": { + "label": "[TODO: Translate] Destination type:", + "tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download." + }, "targetFolderPath": "Путь к целевой папке:", "browseFolders": "Обзор папок:", "createNewFolder": "Создать новую папку", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 9a5013b4..b8db4a3a 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -1563,6 +1563,10 @@ "selectRootDirectory": "选择根目录", "selectModelRoot": "选择模型根目录:", "selectTypeRoot": "选择 {type} 根目录:", + "routingOverride": { + "label": "[TODO: Translate] Destination type:", + "tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download." + }, "targetFolderPath": "目标文件夹路径:", "browseFolders": "浏览文件夹:", "createNewFolder": "新建文件夹", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index f0d5f544..d8216de7 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -1563,6 +1563,10 @@ "selectRootDirectory": "選擇根目錄", "selectModelRoot": "選擇模型根目錄:", "selectTypeRoot": "選擇 {type} 根目錄:", + "routingOverride": { + "label": "[TODO: Translate] Destination type:", + "tooltip": "[TODO: Translate] Auto-detected from the model metadata. Switch if it was misdetected; switching turns off \"Use Default Path\" for this download." + }, "targetFolderPath": "目標資料夾路徑:", "browseFolders": "瀏覽資料夾:", "createNewFolder": "建立新資料夾", diff --git a/static/css/components/modal/download-modal.css b/static/css/components/modal/download-modal.css index 63386a5b..1ea968ff 100644 --- a/static/css/components/modal/download-modal.css +++ b/static/css/components/modal/download-modal.css @@ -1088,3 +1088,56 @@ [data-theme="dark"] .batch-preview-group-body { background: var(--border-color); } + + +/* Routing override segmented toggle (Checkpoint / Diffusion Model) shown on + the checkpoints page of the download modal when auto-routing can misdetect. */ +.routing-override-group { + margin-bottom: var(--space-2); +} + +.routing-override-group label { + display: block; + margin-bottom: 8px; + color: var(--text-color); +} + +.routing-override-toggle { + display: flex; + gap: 0; +} + +.routing-override-option { + flex: 1; + padding: 6px 8px; + border: 1px solid var(--border-color); + background: var(--bg-color); + color: var(--text-color); + opacity: 0.75; + cursor: pointer; + transition: var(--transition-base); + font-size: 0.9em; +} + +.routing-override-option:first-child { + border-radius: var(--border-radius-xs) 0 0 var(--border-radius-xs); + border-right: none; +} + +.routing-override-option:last-child { + border-radius: 0 var(--border-radius-xs) var(--border-radius-xs) 0; +} + +.routing-override-option:not(.active):hover, +.routing-override-option:not(.active):focus-visible { + border-color: var(--lora-accent); + opacity: 1; +} + +.routing-override-option.active { + background: var(--lora-accent); + color: white; + border-color: var(--lora-accent); + opacity: 1; + cursor: default; +} diff --git a/static/js/managers/DownloadManager.js b/static/js/managers/DownloadManager.js index ff4a8e23..7520a958 100644 --- a/static/js/managers/DownloadManager.js +++ b/static/js/managers/DownloadManager.js @@ -34,6 +34,9 @@ export class DownloadManager { this.selectedFolder = ''; this.apiClient = null; this.useDefaultPath = false; + // Manual checkpoint/diffusion root override on the location step: + // null follows the auto classification, true/false is user-picked. + this._routingOverride = null; // Multi-file selection state: selectedFile stays the first selected // file for backward compatibility with single-file flows (#1058). @@ -127,6 +130,11 @@ export class DownloadManager { // Default path toggle handler document.getElementById('useDefaultPath').addEventListener('change', this.handleToggleDefaultPath); + // Checkpoint/diffusion routing override toggle (checkpoints page only) + document.querySelectorAll('#routingOverrideGroup .routing-override-option').forEach(btn => { + btn.addEventListener('click', () => this.handleRoutingOverrideChange(btn.dataset.routing === 'diffusion')); + }); + // Auto-append newline after pasting a URL so users can paste multiple URLs in succession setupAutoNewlineOnPaste('modelUrl'); } @@ -180,6 +188,7 @@ export class DownloadManager { this.selectedFiles = []; this._lastDownloadError = null; this._isDiffusionModel = false; + this._routingOverride = null; this.selectedFolder = ''; this.batchModels = []; @@ -974,48 +983,12 @@ export class DownloadManager { try { this._isDiffusionModel = await this._resolveIsDiffusionModel(); this._otherSubType = await this._resolveOtherSubType(); + // Each location-step entry starts from the auto classification; + // the user can override it via the checkpoint/diffusion toggle. + this._routingOverride = null; + this._syncRoutingOverrideToggle(); - let rootsData; - if (this._isDiffusionModel && this.apiClient.modelType === 'checkpoints') { - rootsData = await this.apiClient.fetchModelRoots('diffusion_model'); - } else if (this.apiClient.modelType === 'other' && this._otherSubType) { - rootsData = await this.apiClient.fetchModelRoots(this._otherSubType); - } else { - // An undecidable other sub_type (null) intentionally lands - // here: fetchModelRoots() lists all other roots so the user - // can pick manually. - rootsData = await this.apiClient.fetchModelRoots(); - } - const modelRoot = document.getElementById('modelRoot'); - modelRoot.innerHTML = rootsData.roots.map(root => - `` - ).join(''); - - let defaultRoot; - let subtypeDisplay; - if (this.apiClient.modelType === 'other') { - const otherDefaultRoots = state.global.settings.default_other_roots || {}; - defaultRoot = this._otherSubType ? (otherDefaultRoots[this._otherSubType] || '') : ''; - subtypeDisplay = this._otherSubType - ? (MODEL_SUBTYPE_DISPLAY_NAMES[this._otherSubType] || this._otherSubType) - : this.apiClient.apiConfig.config.displayName; - } else { - const singularType = this._isDiffusionModel - ? 'unet' - : this.apiClient.modelType.replace(/s$/, ''); - const defaultRootKey = `default_${singularType}_root`; - defaultRoot = state.global.settings[defaultRootKey]; - subtypeDisplay = this._isDiffusionModel ? 'Diffusion Model' : this.apiClient.apiConfig.config.displayName; - } - console.log('Default root:', defaultRoot); - console.log('Available roots:', rootsData.roots); - if (defaultRoot && rootsData.roots.includes(defaultRoot)) { - console.log(`Setting default root: ${defaultRoot}`); - modelRoot.value = defaultRoot; - } - - document.getElementById('modelRootLabel').textContent = - translate('modals.download.selectTypeRoot', { type: subtypeDisplay }); + await this._populateModelRoots(); // Set autocomplete="off" on folderPath input const folderPathInput = document.getElementById('folderPath'); @@ -1035,6 +1008,7 @@ export class DownloadManager { }); // Setup model root change handler + const modelRoot = document.getElementById('modelRoot'); modelRoot.addEventListener('change', async () => { await this.initializeFolderTree(); this.updateTargetPath(); @@ -1049,6 +1023,116 @@ export class DownloadManager { } } + /** + * Effective checkpoint/diffusion routing for the location step: the + * user's manual override when set, otherwise the auto classification. + */ + _getEffectiveIsDiffusionModel() { + return this._routingOverride ?? this._isDiffusionModel; + } + + /** + * Fetch the root list for the effective routing group and repopulate + * the root dropdown, preselecting the matching default root and + * updating the root label. + */ + async _populateModelRoots() { + const isDiffusion = this._getEffectiveIsDiffusionModel(); + + let rootsData; + if (isDiffusion && this.apiClient.modelType === 'checkpoints') { + rootsData = await this.apiClient.fetchModelRoots('diffusion_model'); + } else if (this.apiClient.modelType === 'other' && this._otherSubType) { + rootsData = await this.apiClient.fetchModelRoots(this._otherSubType); + } else { + // An undecidable other sub_type (null) intentionally lands + // here: fetchModelRoots() lists all other roots so the user + // can pick manually. + rootsData = await this.apiClient.fetchModelRoots(); + } + const modelRoot = document.getElementById('modelRoot'); + modelRoot.innerHTML = rootsData.roots.map(root => + `` + ).join(''); + + let defaultRoot; + let subtypeDisplay; + if (this.apiClient.modelType === 'other') { + const otherDefaultRoots = state.global.settings.default_other_roots || {}; + defaultRoot = this._otherSubType ? (otherDefaultRoots[this._otherSubType] || '') : ''; + subtypeDisplay = this._otherSubType + ? (MODEL_SUBTYPE_DISPLAY_NAMES[this._otherSubType] || this._otherSubType) + : this.apiClient.apiConfig.config.displayName; + } else { + const singularType = isDiffusion + ? 'unet' + : this.apiClient.modelType.replace(/s$/, ''); + const defaultRootKey = `default_${singularType}_root`; + defaultRoot = state.global.settings[defaultRootKey]; + subtypeDisplay = isDiffusion ? 'Diffusion Model' : this.apiClient.apiConfig.config.displayName; + } + console.log('Default root:', defaultRoot); + console.log('Available roots:', rootsData.roots); + if (defaultRoot && rootsData.roots.includes(defaultRoot)) { + console.log(`Setting default root: ${defaultRoot}`); + modelRoot.value = defaultRoot; + } + + document.getElementById('modelRootLabel').textContent = + translate('modals.download.selectTypeRoot', { type: subtypeDisplay }); + } + + /** + * Show the checkpoint/diffusion override toggle on the checkpoints page + * (hidden elsewhere) and reflect the effective routing on its options. + */ + _syncRoutingOverrideToggle() { + const group = document.getElementById('routingOverrideGroup'); + if (!group) return; + const isCheckpoints = this.apiClient.modelType === 'checkpoints'; + group.style.display = isCheckpoints ? '' : 'none'; + if (!isCheckpoints) return; + + const effective = this._getEffectiveIsDiffusionModel(); + group.querySelectorAll('.routing-override-option').forEach(btn => { + const active = (btn.dataset.routing === 'diffusion') === effective; + btn.classList.toggle('active', active); + btn.setAttribute('aria-pressed', active ? 'true' : 'false'); + }); + } + + /** + * Handle the user switching the checkpoint/diffusion routing toggle. + * The override must never be silently ignored: the backend re-derives + * the routing group only under "use default path", so overriding turns + * that off for this session (the localStorage preference is left + * untouched) to guarantee the picked root is the actual destination. + */ + async handleRoutingOverrideChange(isDiffusion) { + if (this.apiClient.modelType !== 'checkpoints') return; + + const overridden = isDiffusion !== this._isDiffusionModel; + this._routingOverride = overridden ? isDiffusion : null; + + if (overridden && this.useDefaultPath) { + this.useDefaultPath = false; + const toggleInput = document.getElementById('useDefaultPath'); + if (toggleInput) { + toggleInput.checked = false; + } + } + + this._syncRoutingOverrideToggle(); + + try { + await this._populateModelRoots(); + await this.initializeFolderTree(); + this.updatePathSelectionUI(); + } catch (error) { + showToast('toast.downloads.loadError', { message: error.message }, 'error'); + } + } + /** * Decide whether this download routes to the diffusion model (unet) * roots rather than the checkpoint roots. The backend owns the routing @@ -2554,13 +2638,13 @@ export class DownloadManager { const modelRoot = document.getElementById('modelRoot').value; const config = this.apiClient.apiConfig.config; - const subtypeDisplay = this._isDiffusionModel ? 'Diffusion Model' : config.displayName; + const subtypeDisplay = this._getEffectiveIsDiffusionModel() ? 'Diffusion Model' : config.displayName; let fullPath = modelRoot || translate('modals.download.selectTypeRoot', { type: subtypeDisplay }); if (modelRoot) { if (this.useDefaultPath) { try { - const singularType = this._isDiffusionModel + const singularType = this._getEffectiveIsDiffusionModel() ? 'unet' : this.apiClient.modelType.replace(/s$/, ''); const templates = state.global?.settings?.download_path_templates; diff --git a/templates/components/modals/download_modal.html b/templates/components/modals/download_modal.html index f92f3230..45c2073c 100644 --- a/templates/components/modals/download_modal.html +++ b/templates/components/modals/download_modal.html @@ -80,6 +80,14 @@ +
diff --git a/tests/frontend/managers/downloadManager.routingOverride.test.js b/tests/frontend/managers/downloadManager.routingOverride.test.js new file mode 100644 index 00000000..a354e609 --- /dev/null +++ b/tests/frontend/managers/downloadManager.routingOverride.test.js @@ -0,0 +1,209 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const { + DOWNLOAD_MANAGER_MODULE, + MODAL_MANAGER_MODULE, + UI_HELPERS_MODULE, + STATE_MODULE, + LOADING_MANAGER_MODULE, + API_FACTORY_MODULE, + STORAGE_HELPERS_MODULE, + FOLDER_TREE_MANAGER_MODULE, + I18N_HELPERS_MODULE, + SUMMARY_MODULE, + OTHER_MODELS_MODULE, +} = vi.hoisted(() => ({ + DOWNLOAD_MANAGER_MODULE: new URL('../../../static/js/managers/DownloadManager.js', import.meta.url).pathname, + MODAL_MANAGER_MODULE: new URL('../../../static/js/managers/ModalManager.js', import.meta.url).pathname, + UI_HELPERS_MODULE: new URL('../../../static/js/utils/uiHelpers.js', import.meta.url).pathname, + STATE_MODULE: new URL('../../../static/js/state/index.js', import.meta.url).pathname, + LOADING_MANAGER_MODULE: new URL('../../../static/js/managers/LoadingManager.js', import.meta.url).pathname, + API_FACTORY_MODULE: new URL('../../../static/js/api/modelApiFactory.js', import.meta.url).pathname, + STORAGE_HELPERS_MODULE: new URL('../../../static/js/utils/storageHelpers.js', import.meta.url).pathname, + FOLDER_TREE_MANAGER_MODULE: new URL('../../../static/js/components/FolderTreeManager.js', import.meta.url).pathname, + I18N_HELPERS_MODULE: new URL('../../../static/js/utils/i18nHelpers.js', import.meta.url).pathname, + SUMMARY_MODULE: new URL('../../../static/js/components/DownloadBatchSummaryModal.js', import.meta.url).pathname, + OTHER_MODELS_MODULE: new URL('../../../static/js/utils/otherModels.js', import.meta.url).pathname, +})); + +vi.mock(MODAL_MANAGER_MODULE, () => ({ + modalManager: { showModal: vi.fn(), closeModal: vi.fn() }, +})); +vi.mock(UI_HELPERS_MODULE, () => ({ + showToast: vi.fn(), + showActionToast: vi.fn(), + setupAutoNewlineOnPaste: vi.fn(), +})); +vi.mock(STATE_MODULE, () => ({ + state: { global: { settings: {} }, loadingManager: {} }, +})); +vi.mock(LOADING_MANAGER_MODULE, () => ({ + LoadingManager: vi.fn(() => ({})), +})); +vi.mock(API_FACTORY_MODULE, () => ({ + getModelApiClient: vi.fn(), + resetAndReload: vi.fn(), +})); +vi.mock(STORAGE_HELPERS_MODULE, () => ({ + getStorageItem: vi.fn((_key, defaultValue) => defaultValue), + setStorageItem: vi.fn(), +})); +vi.mock(FOLDER_TREE_MANAGER_MODULE, () => ({ + FolderTreeManager: vi.fn(() => ({})), +})); +vi.mock(I18N_HELPERS_MODULE, () => ({ + translate: vi.fn((_key, _vars, fallback) => fallback ?? ''), +})); +vi.mock(SUMMARY_MODULE, () => ({ + showDownloadBatchSummary: vi.fn(), +})); +vi.mock(OTHER_MODELS_MODULE, () => ({ + enableOtherModels: vi.fn(), + openOtherModelsSettings: vi.fn(), +})); + +const { DownloadManager } = await import(DOWNLOAD_MANAGER_MODULE); +const { state } = await import(STATE_MODULE); +const { setStorageItem } = await import(STORAGE_HELPERS_MODULE); + +describe('DownloadManager checkpoint/diffusion routing override', () => { + let manager; + + function setupDom() { + document.body.innerHTML = ` + + + + + +
+
+ `; + } + + function createManager(modelType, displayName = 'Model') { + manager = new DownloadManager(); + manager.apiClient = { + modelType, + apiConfig: { config: { displayName } }, + fetchModelRoots: vi.fn(), + }; + manager.selectedFile = null; + manager.selectedFiles = []; + manager.currentVersion = null; + manager.initializeFolderTree = vi.fn().mockResolvedValue(); + manager.folderTreeManager = { init: vi.fn(), getSelectedPath: vi.fn(() => '') }; + manager.loadDefaultPathSetting = vi.fn(); + vi.spyOn(manager, '_resolveOtherSubType').mockResolvedValue(null); + } + + beforeEach(() => { + setupDom(); + state.global.settings = {}; + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('shows the toggle for diffusion-routed checkpoints and marks diffusion active', async () => { + createManager('checkpoints', 'Checkpoint'); + vi.spyOn(manager, '_resolveIsDiffusionModel').mockResolvedValue(true); + manager.apiClient.fetchModelRoots.mockResolvedValue({ + success: true, + roots: ['/models/unet'], + }); + state.global.settings.default_unet_root = '/models/unet'; + + await manager.proceedToLocationContent(); + + const group = document.getElementById('routingOverrideGroup'); + expect(group.style.display).not.toBe('none'); + const [checkpointBtn, diffusionBtn] = group.querySelectorAll('.routing-override-option'); + expect(diffusionBtn.classList.contains('active')).toBe(true); + expect(diffusionBtn.getAttribute('aria-pressed')).toBe('true'); + expect(checkpointBtn.classList.contains('active')).toBe(false); + expect(manager.apiClient.fetchModelRoots).toHaveBeenCalledWith('diffusion_model'); + expect(document.getElementById('modelRoot').value).toBe('/models/unet'); + }); + + it('keeps the toggle hidden on non-checkpoints pages', async () => { + createManager('loras', 'LoRA'); + manager.apiClient.fetchModelRoots.mockResolvedValue({ + success: true, + roots: ['/models/loras'], + }); + + await manager.proceedToLocationContent(); + + expect(document.getElementById('routingOverrideGroup').style.display).toBe('none'); + expect(manager.apiClient.fetchModelRoots).toHaveBeenCalledWith(); + }); + + it('re-fetches diffusion roots and preselects the unet default when overriding to diffusion', async () => { + createManager('checkpoints', 'Checkpoint'); + vi.spyOn(manager, '_resolveIsDiffusionModel').mockResolvedValue(false); + manager.apiClient.fetchModelRoots + .mockResolvedValueOnce({ success: true, roots: ['/models/checkpoints'] }) + .mockResolvedValueOnce({ success: true, roots: ['/models/unet'] }); + state.global.settings.default_unet_root = '/models/unet'; + + await manager.proceedToLocationContent(); + expect(document.getElementById('modelRoot').value).toBe('/models/checkpoints'); + + await manager.handleRoutingOverrideChange(true); + + expect(manager.apiClient.fetchModelRoots).toHaveBeenLastCalledWith('diffusion_model'); + const modelRoot = document.getElementById('modelRoot'); + expect(Array.from(modelRoot.options).map(o => o.value)).toEqual(['/models/unet']); + expect(modelRoot.value).toBe('/models/unet'); + const group = document.getElementById('routingOverrideGroup'); + const [checkpointBtn, diffusionBtn] = group.querySelectorAll('.routing-override-option'); + expect(checkpointBtn.classList.contains('active')).toBe(false); + expect(diffusionBtn.classList.contains('active')).toBe(true); + }); + + it('turns off Use Default Path for the session without persisting when overriding', async () => { + createManager('checkpoints', 'Checkpoint'); + vi.spyOn(manager, '_resolveIsDiffusionModel').mockResolvedValue(false); + manager.apiClient.fetchModelRoots.mockResolvedValue({ + success: true, + roots: ['/models/checkpoints'], + }); + + await manager.proceedToLocationContent(); + + manager.useDefaultPath = true; + document.getElementById('useDefaultPath').checked = true; + + await manager.handleRoutingOverrideChange(true); + + expect(manager.useDefaultPath).toBe(false); + expect(document.getElementById('useDefaultPath').checked).toBe(false); + expect(setStorageItem).not.toHaveBeenCalled(); + }); + + it('clears the override when switching back to the auto-classified side', async () => { + createManager('checkpoints', 'Checkpoint'); + vi.spyOn(manager, '_resolveIsDiffusionModel').mockResolvedValue(false); + manager.apiClient.fetchModelRoots.mockResolvedValue({ + success: true, + roots: ['/models/checkpoints'], + }); + + await manager.proceedToLocationContent(); + await manager.handleRoutingOverrideChange(true); + expect(manager._routingOverride).toBe(true); + + manager.apiClient.fetchModelRoots.mockClear(); + await manager.handleRoutingOverrideChange(false); + + expect(manager._routingOverride).toBeNull(); + // Back on the checkpoint group: no routing argument + expect(manager.apiClient.fetchModelRoots).toHaveBeenCalledWith(); + }); +});