refactor: unify frontend source normalization

This commit is contained in:
Dariusz L
2026-08-05 04:31:30 +02:00
parent 0d08bbceaa
commit 7269347c00
11 changed files with 85 additions and 35 deletions
+1 -3
View File
@@ -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';
},
@@ -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();
+2 -1
View File
@@ -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']
+25 -1
View File
@@ -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(
@@ -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
+3 -9
View File
@@ -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;
+5 -2
View File
@@ -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',
+9 -9
View File
@@ -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 || ''));
},
+14 -1
View File
@@ -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;
@@ -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));
}
@@ -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, ' '),