258 lines
9.0 KiB
JavaScript
258 lines
9.0 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { searchSourceMethods } from '../web/resolver/search/search_source_methods.js';
|
|
import {
|
|
getSourceDisplayLabel,
|
|
normalizeSourceKey,
|
|
normalizeSourceList,
|
|
} from '../web/resolver/utils/source_labels.js';
|
|
|
|
const {
|
|
getSearchSourceLabel,
|
|
getSearchSourceErrorMessage,
|
|
getSearchSourceErrorTooltip,
|
|
isSearchSourceRetryable,
|
|
getSearchSourceDefinitions,
|
|
getSearchResultKeysForSources,
|
|
getHashLookupSourcesForSearchSources,
|
|
clearSearchResultsForSources,
|
|
isSearchSourceEnabled,
|
|
getSearchSourceDefinition,
|
|
getSearchSourceEnabledMap,
|
|
getEnabledSearchSources,
|
|
getSearchSourcesForSelection,
|
|
} = searchSourceMethods;
|
|
|
|
function installStorage(values = {}) {
|
|
const previous = Object.getOwnPropertyDescriptor(globalThis, 'localStorage');
|
|
const storage = {
|
|
getItem(key) {
|
|
return Object.prototype.hasOwnProperty.call(values, key) ? values[key] : null;
|
|
},
|
|
setItem() {},
|
|
removeItem() {},
|
|
};
|
|
Object.defineProperty(globalThis, 'localStorage', {
|
|
configurable: true,
|
|
value: storage,
|
|
});
|
|
return () => {
|
|
if (previous) Object.defineProperty(globalThis, 'localStorage', previous);
|
|
else delete globalThis.localStorage;
|
|
};
|
|
}
|
|
|
|
test('search source labels and definitions remain stable', () => {
|
|
const dialog = {};
|
|
|
|
assert.equal(getSearchSourceLabel.call(dialog, 'all'), 'Everything');
|
|
assert.equal(getSearchSourceLabel.call(dialog, 'lora_manager_archive'), 'LoRA Manager Archive');
|
|
assert.equal(getSearchSourceLabel.call(dialog, 'unknown'), 'unknown');
|
|
assert.deepEqual(
|
|
getSearchSourceDefinitions.call(dialog).map(({ source, storageKey }) => ({ source, storageKey })),
|
|
[
|
|
{ source: 'local', storageKey: 'ModelResolver.searchSource.localEnabled' },
|
|
{ source: 'huggingface', storageKey: 'ModelResolver.searchSource.huggingFaceEnabled' },
|
|
{ source: 'civitai', storageKey: 'ModelResolver.searchSource.civitaiEnabled' },
|
|
{ source: 'civarchive', storageKey: 'ModelResolver.searchSource.civArchiveEnabled' },
|
|
{ source: 'lora_manager_archive', storageKey: 'ModelResolver.searchSource.loraManagerArchiveEnabled' },
|
|
]
|
|
);
|
|
});
|
|
|
|
test('shared source labels preserve search and hash comparison contexts', () => {
|
|
assert.equal(getSourceDisplayLabel('lora_manager_archive', { context: 'search' }), 'LoRA Manager Archive');
|
|
assert.equal(getSourceDisplayLabel('lora_manager_archive'), 'LoRA Archive');
|
|
assert.equal(getSourceDisplayLabel('lora-archive'), 'LoRA Archive');
|
|
assert.equal(getSourceDisplayLabel('download-source'), 'Selected source');
|
|
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'),
|
|
'HTTP 502'
|
|
);
|
|
});
|
|
|
|
test('unstructured provider failures preserve their technical message', () => {
|
|
const dialog = { getSearchSourceLabel, getSearchSourceErrorMessage };
|
|
const temporaryFailure = getSearchSourceErrorMessage.call(
|
|
dialog,
|
|
'civarchive',
|
|
'CivArchive search failed: CivArchive search request failed (network error or timeout)'
|
|
);
|
|
|
|
assert.equal(
|
|
temporaryFailure,
|
|
'CivArchive search failed: CivArchive search request failed (network error or timeout)'
|
|
);
|
|
assert.equal(
|
|
getSearchSourceErrorMessage.call(dialog, 'civarchive', 'CivArchive search failed: HTTP 502'),
|
|
'CivArchive search failed: HTTP 502'
|
|
);
|
|
assert.equal(
|
|
getSearchSourceErrorMessage.call(dialog, 'civarchive', 'CivArchive search failed: HTTP 400'),
|
|
'CivArchive search failed: HTTP 400'
|
|
);
|
|
assert.equal(
|
|
getSearchSourceErrorMessage.call(dialog, 'civitai', 'CivAI request timed out'),
|
|
'CivAI request timed out'
|
|
);
|
|
assert.equal(
|
|
getSearchSourceErrorTooltip.call(dialog, 'civarchive', 'CivArchive search failed: HTTP 522'),
|
|
'CivArchive search failed: HTTP 522'
|
|
);
|
|
});
|
|
|
|
test('structured CivArchive status uses safe messages and HTTP details', () => {
|
|
const dialog = { getSearchSourceLabel, getSearchSourceErrorMessage, getSearchSourceErrorTooltip };
|
|
const status = {
|
|
state: 'unavailable',
|
|
code: 'provider_unavailable',
|
|
retryable: true,
|
|
http_status: 522,
|
|
message: 'CivArchive may be overloaded or temporarily unavailable. Please try again.'
|
|
};
|
|
|
|
assert.equal(
|
|
getSearchSourceErrorMessage.call(dialog, 'civarchive', 'CivArchive search failed: HTTP 522', status),
|
|
status.message
|
|
);
|
|
assert.equal(
|
|
getSearchSourceErrorTooltip.call(dialog, 'civarchive', '', status),
|
|
`${status.message} Details: HTTP 522`
|
|
);
|
|
assert.equal(
|
|
getSearchSourceErrorMessage.call(dialog, 'civarchive', '', {
|
|
state: 'rate_limited',
|
|
code: 'rate_limited',
|
|
retryable: true
|
|
}),
|
|
'CivArchive is rate limited. Please try again later.'
|
|
);
|
|
});
|
|
|
|
test('CivArchive transport failures are marked retryable without broadening other sources', () => {
|
|
assert.equal(isSearchSourceRetryable('civarchive', 'CivArchive search failed: HTTP 522'), true);
|
|
assert.equal(isSearchSourceRetryable('civarchive', 'CivArchive search failed: HTTP 400'), false);
|
|
assert.equal(isSearchSourceRetryable('civitai', 'request timed out'), false);
|
|
assert.equal(isSearchSourceRetryable('civarchive', '', { retryable: false }), false);
|
|
});
|
|
|
|
test('search source selection maps source ids to result and hash keys', () => {
|
|
assert.deepEqual(
|
|
getSearchResultKeysForSources('local'),
|
|
['popular', 'model_list']
|
|
);
|
|
assert.deepEqual(
|
|
getSearchResultKeysForSources('all'),
|
|
['popular', 'model_list', 'huggingface', 'civitai', 'civarchive', 'lora_manager_archive']
|
|
);
|
|
assert.deepEqual(
|
|
[...getHashLookupSourcesForSearchSources(['civitai', 'custom'])],
|
|
['civitai']
|
|
);
|
|
assert.deepEqual(
|
|
[...getHashLookupSourcesForSearchSources('all')],
|
|
['huggingface', 'civitai', 'civarchive']
|
|
);
|
|
});
|
|
|
|
test('clearing selected search sources preserves unrelated results and hash matches', () => {
|
|
const results = {
|
|
popular: { items: ['popular'] },
|
|
model_list: { items: ['local'] },
|
|
huggingface: { items: ['hf'] },
|
|
civitai: { items: ['civitai'] },
|
|
civarchive: { items: ['archive'] },
|
|
lora_manager_archive: { items: ['lora'] },
|
|
custom: [{ url: 'https://example.test/model' }],
|
|
local_hash_matches: [
|
|
{ hash_lookup_source: 'civitai', id: 'remove' },
|
|
{ hash_lookup_source: 'civarchive', id: 'keep' },
|
|
],
|
|
};
|
|
|
|
assert.deepEqual(clearSearchResultsForSources.call({
|
|
getSearchResultKeysForSources,
|
|
getHashLookupSourcesForSearchSources,
|
|
}, results, 'civitai'), {
|
|
popular: results.popular,
|
|
model_list: results.model_list,
|
|
huggingface: results.huggingface,
|
|
civitai: null,
|
|
civarchive: results.civarchive,
|
|
lora_manager_archive: results.lora_manager_archive,
|
|
custom: results.custom,
|
|
local_hash_matches: [{ hash_lookup_source: 'civarchive', id: 'keep' }],
|
|
});
|
|
});
|
|
|
|
test('source enablement defaults to enabled and honors persisted false flags', () => {
|
|
const restoreStorage = installStorage({
|
|
'ModelResolver.searchSource.civitaiEnabled': 'false',
|
|
});
|
|
try {
|
|
const dialog = {
|
|
getSearchSourceDefinitions,
|
|
getSearchSourceDefinition,
|
|
isSearchSourceEnabled,
|
|
};
|
|
assert.equal(isSearchSourceEnabled.call(dialog, 'civitai'), false);
|
|
assert.equal(isSearchSourceEnabled.call(dialog, 'huggingface'), true);
|
|
assert.equal(isSearchSourceEnabled.call(dialog, 'all'), true);
|
|
assert.deepEqual(getSearchSourceEnabledMap.call(dialog), {
|
|
local: true,
|
|
huggingface: true,
|
|
civitai: false,
|
|
civarchive: true,
|
|
lora_manager_archive: true,
|
|
});
|
|
} finally {
|
|
restoreStorage();
|
|
}
|
|
});
|
|
|
|
test('enabled source selection falls back to local and respects available sources', () => {
|
|
const dialog = {
|
|
getSearchSourceDefinitions,
|
|
getEnabledSearchSources,
|
|
isSearchSourceUsable: source => source !== 'civitai',
|
|
};
|
|
|
|
assert.deepEqual(getEnabledSearchSources.call(dialog), [
|
|
'local',
|
|
'huggingface',
|
|
'civarchive',
|
|
'lora_manager_archive',
|
|
]);
|
|
assert.deepEqual(getSearchSourcesForSelection.call(dialog, 'all'), [
|
|
'local',
|
|
'huggingface',
|
|
'civarchive',
|
|
'lora_manager_archive',
|
|
]);
|
|
assert.deepEqual(getSearchSourcesForSelection.call(dialog, 'civitai'), []);
|
|
|
|
const noSourcesDialog = {
|
|
getSearchSourceDefinitions,
|
|
getEnabledSearchSources,
|
|
isSearchSourceUsable: () => false,
|
|
};
|
|
assert.deepEqual(getEnabledSearchSources.call(noSourcesDialog), ['local']);
|
|
});
|