158 lines
7.3 KiB
JavaScript
158 lines
7.3 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { modelHashCompareMethods } from '../web/resolver/views/model_hash_compare_methods.js';
|
|
import { normalizeSha256 } from '../web/resolver/utils/hash_utils.js';
|
|
|
|
const methods = modelHashCompareMethods;
|
|
|
|
function createDialog() {
|
|
return {
|
|
...methods,
|
|
getFilenameFromPath: value => String(value || '').split(/[\\/]/).pop() || '',
|
|
getVersionedModelName: (name, version) => version ? `${name} ${version}` : name,
|
|
truncateText: value => value,
|
|
getLocalMatchIdentity: () => 'local-identity',
|
|
};
|
|
}
|
|
|
|
test('hash comparison normalizes and formats SHA-256 values', () => {
|
|
const dialog = createDialog();
|
|
const hash = 'A'.repeat(64);
|
|
|
|
assert.equal(normalizeSha256(`sha256:${hash}`), hash.toLowerCase());
|
|
assert.equal(normalizeSha256('not-a-hash'), '');
|
|
assert.equal(dialog.formatSha256Short(hash), 'aaaaaaaa...aaaaaaaa');
|
|
});
|
|
|
|
test('hash candidates are collected recursively with source labels and deduplicated', () => {
|
|
const dialog = createDialog();
|
|
const first = 'a'.repeat(64);
|
|
const second = 'b'.repeat(64);
|
|
const candidates = dialog.collectHashCandidatesForCompare({
|
|
sha256: first,
|
|
hashes: { SHA256: first },
|
|
file_info: { hash: second },
|
|
files: [{ fileHash: second }],
|
|
});
|
|
|
|
assert.deepEqual(candidates.map(candidate => candidate.hash), [first, second]);
|
|
assert.equal(candidates[0].source, 'metadata');
|
|
assert.match(candidates[1].source, /file_info/);
|
|
assert.deepEqual(dialog.getLocalHashCandidatesForCompare({ sha256: first }), [{
|
|
hash: first,
|
|
source: 'local model metadata',
|
|
}]);
|
|
});
|
|
|
|
test('local hash compare metadata resolves path, filename, labels, and URLs', () => {
|
|
const dialog = createDialog();
|
|
const model = { path: 'models/checkpoints/example.safetensors' };
|
|
|
|
assert.equal(dialog.getLocalHashComparePath(model), model.path);
|
|
assert.equal(dialog.getHashCompareFilename(model), 'example.safetensors');
|
|
assert.equal(dialog.getHashCompareSourceLabel('civitai'), 'CivitAI');
|
|
assert.equal(dialog.getHashCompareSourceLabel('unknown_source'), 'unknown source');
|
|
assert.equal(dialog.getHashCompareResultName({ name: 'Model', version_name: 'v2' }), 'Model v2');
|
|
assert.equal(dialog.getHashCompareResultUrl({ version_url: 'https://example.test/version' }), 'https://example.test/version');
|
|
});
|
|
|
|
test('hash compare matches are deduplicated and local identity prefers explicit values', () => {
|
|
const dialog = createDialog();
|
|
const matches = [
|
|
{ sourceLabel: 'CivitAI', name: 'Model', url: 'https://example.test', sha256: 'a'.repeat(64) },
|
|
{ sourceLabel: 'CivitAI', name: 'Model', url: 'https://example.test', sha256: 'a'.repeat(64) },
|
|
{ sourceLabel: 'HuggingFace', name: 'Model', url: 'https://example.test', sha256: 'b'.repeat(64) },
|
|
];
|
|
|
|
assert.equal(dialog.dedupeHashCompareMatches(matches).length, 2);
|
|
assert.equal(dialog.getHashCompareLocalMatchIdentity({ local_match_identity: 'explicit' }), 'explicit');
|
|
assert.equal(dialog.getHashCompareLocalMatchIdentity({}), 'local-identity');
|
|
});
|
|
|
|
test('hash compare metadata update stores normalized SHA-256 fields', () => {
|
|
const dialog = createDialog();
|
|
const model = {};
|
|
|
|
dialog.updateHashCompareModelMetadata(model, {
|
|
sha256: `sha256:${'c'.repeat(64)}`,
|
|
metadata_path: 'model.json',
|
|
});
|
|
|
|
assert.equal(model.sha256, 'c'.repeat(64));
|
|
assert.equal(model.hash, 'c'.repeat(64));
|
|
assert.equal(model.hashes.SHA256, 'c'.repeat(64));
|
|
assert.equal(model.metadata_path, 'model.json');
|
|
});
|
|
|
|
test('hash candidate collection handles primitive, array, null, and deeply nested metadata', () => {
|
|
const dialog = createDialog();
|
|
const hash = 'd'.repeat(64);
|
|
const nested = { sha256: hash };
|
|
for (let depth = 0; depth < 7; depth += 1) {
|
|
Object.assign(nested, { metadata: { sha256: hash } });
|
|
}
|
|
|
|
assert.deepEqual(dialog.collectHashCandidatesForCompare(hash, 'direct'), [{ hash, source: 'direct' }]);
|
|
assert.deepEqual(dialog.collectHashCandidatesForCompare([hash, null, 12], 'array'), [{ hash, source: 'array' }]);
|
|
assert.deepEqual(dialog.collectHashCandidatesForCompare(null), []);
|
|
assert.deepEqual(dialog.collectHashCandidatesForCompare(true), []);
|
|
assert.equal(dialog.collectHashCandidatesForCompare(nested).length, 1);
|
|
});
|
|
|
|
test('hash compare metadata uses path and filename fallbacks', () => {
|
|
const dialog = createDialog();
|
|
|
|
assert.equal(dialog.getLocalHashComparePath({ open_path: 'open/model.safetensors', path: 'path/model.safetensors' }), 'open/model.safetensors');
|
|
assert.equal(dialog.getLocalHashComparePath({ resolved_path: 'resolved/model.safetensors' }), 'resolved/model.safetensors');
|
|
assert.equal(dialog.getLocalHashComparePath({ file_path: 'file/model.safetensors' }), 'file/model.safetensors');
|
|
assert.equal(dialog.getLocalHashComparePath({}), '');
|
|
assert.equal(dialog.getHashCompareFilename({ filename: 'explicit.safetensors', path: 'ignored.safetensors' }), 'explicit.safetensors');
|
|
assert.equal(dialog.getHashCompareFilename({ name: 'named-model' }), 'named-model');
|
|
assert.equal(dialog.getHashCompareFilename({}), 'Selected local model');
|
|
});
|
|
|
|
test('hash compare source labels cover explicit, known, and unknown providers', () => {
|
|
const dialog = createDialog();
|
|
|
|
assert.equal(dialog.getHashCompareSourceLabel('ignored', { sourceLabel: 'Custom source' }), 'Custom source');
|
|
assert.equal(dialog.getHashCompareSourceLabel('download-source'), 'Selected source');
|
|
assert.equal(dialog.getHashCompareSourceLabel('model-list'), 'Local Database');
|
|
assert.equal(dialog.getHashCompareSourceLabel('huggingface'), 'HuggingFace');
|
|
assert.equal(dialog.getHashCompareSourceLabel('lora-archive'), 'LoRA Archive');
|
|
assert.equal(dialog.getHashCompareSourceLabel('unknown-source'), 'unknown source');
|
|
assert.equal(dialog.getHashCompareSourceLabel('', {}), 'Source');
|
|
});
|
|
|
|
test('hash compare result names and URLs use ordered fallbacks and truncation', () => {
|
|
const dialog = createDialog();
|
|
dialog.truncateText = value => value.slice(0, 5);
|
|
|
|
assert.equal(dialog.getHashCompareResultName({ model_name: 'Model', version: 'v1' }), 'Model');
|
|
assert.equal(dialog.getHashCompareResultName({ repo_id: 'owner/repo' }), 'owner');
|
|
assert.equal(dialog.getHashCompareResultName({}), 'Model');
|
|
assert.equal(dialog.getHashCompareResultUrl({ model_url: 'https://model.test' }), 'https://model.test');
|
|
assert.equal(dialog.getHashCompareResultUrl({ download_url: 'https://download.test' }), 'https://download.test');
|
|
assert.equal(dialog.getHashCompareResultUrl({}), '');
|
|
});
|
|
|
|
test('hash compare metadata update ignores invalid values and normalizes hash containers', () => {
|
|
const dialog = createDialog();
|
|
const model = { hashes: ['invalid'] };
|
|
|
|
dialog.updateHashCompareModelMetadata(model, { sha256: 'invalid' });
|
|
assert.deepEqual(model, { hashes: ['invalid'] });
|
|
|
|
dialog.updateHashCompareModelMetadata(model, { hash: 'e'.repeat(64) });
|
|
assert.deepEqual(model.hashes, { SHA256: 'e'.repeat(64) });
|
|
assert.equal(model.metadata_path, undefined);
|
|
dialog.updateHashCompareModelMetadata(null, { sha256: 'f'.repeat(64) });
|
|
});
|
|
|
|
test('hash compare deduplication and local identity handle empty and missing values', () => {
|
|
const dialog = createDialog();
|
|
dialog.getLocalMatchIdentity = () => '';
|
|
|
|
assert.deepEqual(dialog.dedupeHashCompareMatches([null, undefined, {}]), [{}]);
|
|
assert.equal(dialog.getHashCompareLocalMatchIdentity({}), '');
|
|
});
|