diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index 9cf9c43..42a9e76 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -20,7 +20,7 @@ import { import { startSplitterDrag } from '../web/resolver/utils/splitter_drag.js'; import { normalizeDownloadCategoryValue } from '../web/resolver/utils/category_utils.js'; import { getSha256Field, normalizeSha256 } from '../web/resolver/utils/hash_utils.js'; -import { getSourceDisplayLabel } from '../web/resolver/utils/source_labels.js'; +import { getSourceDisplayLabel, normalizeSourceKey } from '../web/resolver/utils/source_labels.js'; import { baseModelAliasMethods } from '../web/resolver/search/base_model_alias_methods.js'; import { searchHashMethods } from '../web/resolver/search/search_hash_methods.js'; import { missingModelStateMethods } from '../web/resolver/views/missing_model_state_methods.js'; @@ -5767,14 +5767,12 @@ test('local hash identities link search results across sources by sha', () => { const { getLocalMatchHash, getSearchResultSha256, - normalizeHashLookupSourceKey, getLocalHashMatchIdentitiesForResult, } = searchHashMethods; const sha256 = 'e'.repeat(64); const dialog = { getLocalMatchHash, getSearchResultSha256, - normalizeHashLookupSourceKey, getLocalMatchIdentity() { return 'local-match-id'; }, diff --git a/tests/test_missing_model_state_methods.mjs b/tests/test_missing_model_state_methods.mjs index 0cd9a48..1cc6ed7 100644 --- a/tests/test_missing_model_state_methods.mjs +++ b/tests/test_missing_model_state_methods.mjs @@ -78,6 +78,8 @@ test('known local download sources are hidden until local search was attempted', const state = { explicitSearchSources: [] }; assert.equal(dialog.isLocalDatabaseDownloadSource(source), true); + assert.equal(dialog.isLocalDatabaseDownloadSource({ source: 'POPULAR' }), true); + assert.equal(dialog.isLocalDatabaseDownloadSource({ source: 'model-list' }), true); assert.equal(dialog.shouldDisplayKnownDownloadSource({}, source, state), false); assert.equal( dialog.shouldDisplayKnownDownloadSource({}, source, { explicitSearchSources: ['local'] }), @@ -89,6 +91,18 @@ test('known local download sources are hidden until local search was attempted', ); }); +test('custom provider results use normalized source identities', () => { + const dialog = createDialog(); + const missing = { download_source: null }; + const state = { + results: { + custom: [{ source: 'CivArchive', url: 'https://example.test/archive' }], + }, + }; + + assert.equal(dialog.getMissingSourceResultStatus(missing, 'civarchive', state), 'found'); +}); + test('search result status and missing model counters expose stable state', () => { const dialog = createDialog(); diff --git a/tests/test_search_hash_methods.mjs b/tests/test_search_hash_methods.mjs index e19fba7..0bbd5f2 100644 --- a/tests/test_search_hash_methods.mjs +++ b/tests/test_search_hash_methods.mjs @@ -3,6 +3,7 @@ import test from 'node:test'; import { searchHashMethods } from '../web/resolver/search/search_hash_methods.js'; import { getSha256Field, normalizeSha256 } from '../web/resolver/utils/hash_utils.js'; +import { normalizeSourceKey } from '../web/resolver/utils/source_labels.js'; function createDialog(overrides = {}) { return { ...searchHashMethods, ...overrides }; @@ -108,7 +109,7 @@ test('hash source keys and local identities match by hash or normalized source', null, ]; - assert.equal(dialog.normalizeHashLookupSourceKey(' Civit-AI '), 'civit_ai'); + assert.equal(normalizeSourceKey(' Civit-AI '), 'civit_ai'); assert.deepEqual( dialog.getLocalHashMatchIdentitiesForResult(matches, 'civit-ai', { sha256: hashA }), ['local-a'] diff --git a/tests/test_search_source_methods.mjs b/tests/test_search_source_methods.mjs index b96335d..25800ef 100644 --- a/tests/test_search_source_methods.mjs +++ b/tests/test_search_source_methods.mjs @@ -1,7 +1,11 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { searchSourceMethods } from '../web/resolver/search/search_source_methods.js'; -import { getSourceDisplayLabel } from '../web/resolver/utils/source_labels.js'; +import { + getSourceDisplayLabel, + normalizeSourceKey, + normalizeSourceList, +} from '../web/resolver/utils/source_labels.js'; const { getSearchSourceLabel, @@ -64,6 +68,26 @@ test('shared source labels preserve search and hash comparison contexts', () => assert.equal(getSourceDisplayLabel('unknown-source', { fallback: 'Fallback' }), 'Fallback'); }); +test('source normalization preserves labels, trimmed selections, and provider errors', () => { + const dialog = { getSearchSourceLabel, getSearchSourceErrorMessage }; + + assert.equal(normalizeSourceKey(' Civit-AI '), 'civit_ai'); + assert.equal(normalizeSourceKey(' Civit-AI ', { trim: false }), ' civit_ai '); + assert.deepEqual([...normalizeSourceList([' ', ' local ', null, 'civitai'])], ['local', 'civitai']); + assert.equal( + getSourceDisplayLabel(' Civit-AI ', { fallback: 'Fallback' }), + 'Fallback' + ); + assert.deepEqual( + getSearchResultKeysForSources([' ', ' local ', null]), + ['popular', 'model_list'] + ); + assert.equal( + getSearchSourceErrorMessage.call(dialog, ' CivArchive ', 'HTTP 502'), + 'CivArchive may be overloaded or temporarily unavailable. Please try again.' + ); +}); + test('CivArchive availability failures receive a user-facing retry message', () => { const dialog = { getSearchSourceLabel, getSearchSourceErrorMessage }; const temporaryFailure = getSearchSourceErrorMessage.call( diff --git a/web/resolver/actions/resolve_download_methods.js b/web/resolver/actions/resolve_download_methods.js index 36cb9b9..7b4363c 100644 --- a/web/resolver/actions/resolve_download_methods.js +++ b/web/resolver/actions/resolve_download_methods.js @@ -5,6 +5,7 @@ import { getCivitaiModelUrl } from "../globals.js"; import { normalizeSha256 } from "../utils/hash_utils.js"; import { normalizePathIdentity } from "../utils/html_utils.js"; import { bindEventOnce, bindInstantAction, syncElementAttributes } from "../utils/dom_patch_utils.js"; +import { normalizeSourceKey } from "../utils/source_labels.js"; const log = createModuleLogger('resolve_download_methods'); export const resolveDownloadMethods = { @@ -739,13 +740,13 @@ export const resolveDownloadMethods = { return existingMatches; } - const sourceKey = String( + const sourceKey = normalizeSourceKey( options.sourceKey || result.sourceKey || result.source || result.details_source || 'download_source' - ).trim().toLowerCase().replace(/-/g, '_'); + ); const category = options.category || result.category || result.directory diff --git a/web/resolver/search/search_hash_methods.js b/web/resolver/search/search_hash_methods.js index d2963dd..1feca4d 100644 --- a/web/resolver/search/search_hash_methods.js +++ b/web/resolver/search/search_hash_methods.js @@ -1,4 +1,5 @@ import { normalizeSha256 } from '../utils/hash_utils.js'; +import { normalizeSourceKey } from '../utils/source_labels.js'; export const searchHashMethods = { getSearchResultSha256(result = {}) { @@ -97,13 +98,6 @@ export const searchHashMethods = { : ''; }, - normalizeHashLookupSourceKey(source = '') { - return String(source || '') - .trim() - .toLowerCase() - .replace(/-/g, '_'); - }, - getLocalHashMatchIdentitiesForResult(hashMatches = [], sourceKey = '', sourceResult = {}) { const identities = []; const seen = new Set(); @@ -118,7 +112,7 @@ export const searchHashMethods = { sourceResult.hash_verified_local_match_identities.forEach(addIdentity); } - const normalizedSource = this.normalizeHashLookupSourceKey(sourceKey); + const normalizedSource = normalizeSourceKey(sourceKey); const sourceHash = this.getSearchResultSha256(sourceResult); if (!Array.isArray(hashMatches) || !hashMatches.length || !normalizedSource) { return identities; @@ -127,7 +121,7 @@ export const searchHashMethods = { hashMatches.forEach(match => { if (!match || typeof match !== 'object') return; - const matchSource = this.normalizeHashLookupSourceKey(match.hash_lookup_source || ''); + const matchSource = normalizeSourceKey(match.hash_lookup_source || ''); const matchHash = normalizeSha256(this.getLocalMatchHash(match)); if (sourceHash) { if (matchHash !== sourceHash) return; diff --git a/web/resolver/search/search_panel.js b/web/resolver/search/search_panel.js index c5042a0..0f7a1d6 100644 --- a/web/resolver/search/search_panel.js +++ b/web/resolver/search/search_panel.js @@ -1,6 +1,6 @@ import { getSvgIcon } from "../../utils/icon_utils.js"; import { getModelCardUrl, parseHuggingFaceFileUrl } from "../utils/url_utils.js"; -import { getSourceDisplayLabel } from "../utils/source_labels.js"; +import { getSourceDisplayLabel, normalizeSourceKey } from "../utils/source_labels.js"; import { getCivitaiModelUrl } from "../globals.js"; import { safeStorage, normalizePathIdentity } from "../utils/html_utils.js"; import { bindEventOnce } from "../utils/dom_patch_utils.js"; @@ -1710,7 +1710,10 @@ export const searchPanelMethods = { getCustomUrlResultTableRow(missing, result = {}, hashLabelMap = null, localHashMatches = []) { if (!result || typeof result !== 'object') return null; - const source = String(result.source || result.details_source || 'custom').toLowerCase().replace(/-/g, '_'); + const source = normalizeSourceKey( + result.source || result.details_source || 'custom', + { trim: false } + ); const sourceKeys = { huggingface: 'huggingface', civitai: 'civitai', diff --git a/web/resolver/search/search_source_methods.js b/web/resolver/search/search_source_methods.js index 548b46b..6d3d259 100644 --- a/web/resolver/search/search_source_methods.js +++ b/web/resolver/search/search_source_methods.js @@ -1,13 +1,15 @@ import { safeStorage } from "../utils/html_utils.js"; -import { getSourceDisplayLabel } from "../utils/source_labels.js"; +import { + getSourceDisplayLabel, + normalizeSourceKey, + normalizeSourceList, +} from "../utils/source_labels.js"; const localStorage = safeStorage; export const searchSourceMethods = { getSearchResultKeysForSources(sources = []) { - const normalized = new Set((Array.isArray(sources) ? sources : [sources]) - .map(source => String(source || '').trim()) - .filter(Boolean)); + const normalized = normalizeSourceList(sources); if (normalized.has('all')) { return ['popular', 'model_list', 'huggingface', 'civitai', 'civarchive', 'lora_manager_archive']; } @@ -25,9 +27,7 @@ export const searchSourceMethods = { }, getHashLookupSourcesForSearchSources(sources = []) { - const normalized = new Set((Array.isArray(sources) ? sources : [sources]) - .map(source => String(source || '').trim()) - .filter(Boolean)); + const normalized = normalizeSourceList(sources); const hashSources = ['huggingface', 'civitai', 'civarchive']; if (normalized.has('all')) return new Set(hashSources); return new Set(hashSources.filter(source => normalized.has(source))); @@ -75,7 +75,7 @@ export const searchSourceMethods = { } const message = String(error || '').trim(); - const normalizedSource = String(source || '').trim().toLowerCase().replace(/-/g, '_'); + const normalizedSource = normalizeSourceKey(source); const statusCode = String(status?.code || '').trim().toLowerCase(); if (normalizedSource === 'civarchive') { const messagesByCode = { @@ -116,7 +116,7 @@ export const searchSourceMethods = { isSearchSourceRetryable(source, error = '', status = null) { if (typeof status?.retryable === 'boolean') return status.retryable; - const normalizedSource = String(source || '').trim().toLowerCase().replace(/-/g, '_'); + const normalizedSource = normalizeSourceKey(source); if (normalizedSource !== 'civarchive') return false; return /network error|timeout|timed out|connection (?:error|reset|refused)|temporarily unavailable|\bHTTP\s+(?:408|429|5\d{2})\b/i.test(String(error || '')); }, diff --git a/web/resolver/utils/source_labels.js b/web/resolver/utils/source_labels.js index d3cb55a..35c51e7 100644 --- a/web/resolver/utils/source_labels.js +++ b/web/resolver/utils/source_labels.js @@ -23,6 +23,19 @@ const SOURCE_LABEL_CONTEXT_OVERRIDES = Object.freeze({ }), }); +export function normalizeSourceKey(value, { trim = true } = {}) { + const text = String(value ?? ''); + return (trim ? text.trim() : text).toLowerCase().replace(/-/g, '_'); +} + +export function normalizeSourceList(sources = []) { + return new Set( + (Array.isArray(sources) ? sources : [sources]) + .map(source => String(source ?? '').trim()) + .filter(Boolean) + ); +} + export function getSourceDisplayLabel( source, { @@ -32,7 +45,7 @@ export function getSourceDisplayLabel( } = {} ) { const sourceKey = normalize - ? String(source ?? '').toLowerCase().replace(/-/g, '_') + ? normalizeSourceKey(source, { trim: false }) : source; const contextLabels = SOURCE_LABEL_CONTEXT_OVERRIDES[context] || {}; return contextLabels[sourceKey] || COMMON_SOURCE_LABELS[sourceKey] || fallback; diff --git a/web/resolver/views/missing_model_state_methods.js b/web/resolver/views/missing_model_state_methods.js index 85e4c2a..f9abdc9 100644 --- a/web/resolver/views/missing_model_state_methods.js +++ b/web/resolver/views/missing_model_state_methods.js @@ -1,3 +1,5 @@ +import { normalizeSourceKey } from '../utils/source_labels.js'; + export const missingModelStateMethods = { getBestLocalMatch(missing = {}, minConfidence = 0) { const matches = Array.isArray(missing.matches) ? missing.matches : []; @@ -44,10 +46,7 @@ export const missingModelStateMethods = { }, isLocalDatabaseDownloadSource(downloadSource = {}) { - const source = String(downloadSource?.source || '') - .trim() - .toLowerCase() - .replace(/-/g, '_'); + const source = normalizeSourceKey(downloadSource?.source || ''); return source === 'local' || source === 'model_list' || source === 'popular'; }, @@ -78,7 +77,7 @@ export const missingModelStateMethods = { if (['huggingface', 'civitai', 'civarchive'].includes(source)) { const customResults = Array.isArray(results.custom) ? results.custom : []; customResults - .filter(result => String(result?.source || '').toLowerCase().replace(/-/g, '_') === source) + .filter(result => normalizeSourceKey(result?.source || '', { trim: false }) === source) .forEach(result => candidates.push(result)); } diff --git a/web/resolver/views/model_hash_compare_methods.js b/web/resolver/views/model_hash_compare_methods.js index 936a90c..d02b946 100644 --- a/web/resolver/views/model_hash_compare_methods.js +++ b/web/resolver/views/model_hash_compare_methods.js @@ -1,5 +1,5 @@ import { normalizeSha256 } from '../utils/hash_utils.js'; -import { getSourceDisplayLabel } from '../utils/source_labels.js'; +import { getSourceDisplayLabel, normalizeSourceKey } from '../utils/source_labels.js'; export const modelHashCompareMethods = { formatSha256Short(value = '') { @@ -113,7 +113,10 @@ export const modelHashCompareMethods = { const explicitLabel = result?.sourceLabel || result?.source_label || ''; if (explicitLabel) return String(explicitLabel); - const rawSource = String(result?.source || result?.details_source || sourceKey || '').toLowerCase().replace(/-/g, '_'); + const rawSource = normalizeSourceKey( + result?.source || result?.details_source || sourceKey || '', + { trim: false } + ); return getSourceDisplayLabel(rawSource, { context: 'hash_compare', fallback: String(sourceKey || 'Source').replace(/[_-]+/g, ' '),