Files
DemonGatanjieu-Anomalous_Mo…/tests/model_source_components.mjs
T

93 lines
5.1 KiB
JavaScript

import assert from 'node:assert/strict';
import { fixture, all } from './ui_fixture.mjs';
const f = fixture();
f.app.canvas.setDirty = () => {};
const hub = await f.module('ui_model_sources.js');
const node = (id, type, name, value, values) => ({ id, type, widgets: [{ name, value, options: { values } }] });
f.app.graph._nodes = [
node(1, 'DualCLIPLoader', 'clip_name1', 'encoders/t5.gguf', ['encoders\\t5.gguf']),
node(2, 'CLIPLoader', 'clip_name', 'clip.pth', () => ['clip.pth']),
node(3, 'VAELoader', 'vae_name', 'taesd', ['taesd']),
node(4, 'VAELoader', 'vae_name', 'missing.safetensors', ['available.safetensors']),
node(5, 'CLIPVisionLoader', 'clip_name', 'vision.safetensors', ['vision.safetensors']),
node(6, 'DualCLIPLoader', 'device', 'cpu', ['cpu', 'cuda']),
node(7, 'VAELoader', 'vae_name', 'None', ['None']),
node(8, 'VAELoader', 'vae_name', 'uninstalled.safetensors', []),
];
f.app.graph.extra = {};
let entries = hub.collectWorkflowModels();
assert.equal(entries.length, 6, 'Include components without requiring a source URL or hash; skip non-model controls');
assert.equal(entries[0].isMissing, false, 'Native slash variants are not missing files');
assert.equal(entries[1].isMissing, false, 'Dynamic choices are not proof of a missing file');
assert.equal(entries[3].isMissing, true);
assert.equal(entries[5].isMissing, true, 'An empty native file list still identifies an unavailable selected model');
assert.deepEqual([...entries[4].folderTypes], ['clip_vision']);
f.fetch = async (url, options) => {
assert.equal(url, '/anomalous/resolve_paths_to_previews', 'Opening only reads local metadata');
const body = JSON.parse(options.body);
const context_models = {};
for (const request of body.context_requests || []) {
assert.equal(request.exact_only, true);
assert.ok(request.folder_types.length);
if (request.path === 'vision.safetensors') context_models[request.key] = {
type: 'clip_vision', subfolder: '', path_idx: 1, file_path: 'E:/fixtures/vision.safetensors',
metadata: { source_url: 'https://huggingface.co/example/vision' },
};
}
return { ok: true, json: async () => ({ context_models, models: {
// A same-named checkpoint must not be used for a VAE.
'missing.safetensors': { type: 'checkpoints', metadata: { source_url: 'https://example.com/wrong' } },
} }) };
};
await hub.resolveWorkflowModelsMetadata(entries);
assert.equal(entries[3].url, '');
assert.equal(entries[4].url, 'https://huggingface.co/example/vision');
assert.equal(entries[4].path_idx, 1);
assert.equal(await hub.autoDetectModelSource(entries[3]), '', 'Unknown component stays unresolved; no cloud search URL is treated as a source');
await assert.rejects(() => hub.saveSingleModelToLocalSidecar(entries[2], 'https://example.com/taesd'));
entries[2].url = 'https://example.com/taesd';
hub.syncWorkflowSources(entries);
assert.equal(f.app.graph.extra.anomalous_model_sources.taesd.url, entries[2].url, 'Unresolved local options can still carry workflow links');
const many = Array.from({ length: 19 }, (_, i) => ({ ...entries[4], key: `component-${i}` }));
const start = f.requests.length;
await hub.resolveWorkflowModelsMetadata(many);
const batches = f.requests.slice(start).map(([, o]) => JSON.parse(o.body)).filter(b => b.context_requests);
assert.deepEqual(batches.map(b => b.context_requests.length), [16, 3]);
// Exercise the actual UI renderer, including manual entry and both status badges.
f.app.graph.extra = {};
const close = hub.openModelSourcesModal('workflow');
await f.flush();
const rows = f.document.body.querySelectorAll('.anomalous-source-row-item');
assert.equal(rows.length, 6);
assert.ok(rows[0].textContent.includes('Text encoder'));
assert.ok(rows[4].textContent.includes('Vision encoder'));
const missing = rows[3];
assert.ok(missing.querySelector('.anomalous-source-badge-danger'));
const pending = all(missing).find(el => el.textContent === 'Source needed' && el.className === 'anomalous-source-badge-neutral');
assert.equal(pending.hidden, false);
const input = missing.querySelector('input');
assert.equal(input.readOnly, false);
input.value = 'https://example.com/vae';
input.oninput();
assert.equal(pending.hidden, true, 'Typing a source removes the pending-source hint without hiding missing-file warning');
assert.ok(missing.querySelector('.anomalous-source-badge-danger'));
assert.equal(rows[4].querySelector('input').readOnly, true, 'Resolved source is preserved');
close();
assert.equal(f.document.body.querySelector('.anomalous-model-sources-overlay'), null);
let saved = null;
f.fetch = async (url, options) => {
assert.equal(url, '/anomalous/update_metadata');
saved = JSON.parse(options.body);
return { ok: true, json: async () => ({ status: 'success' }) };
};
await hub.saveSingleModelToLocalSidecar(entries[4], 'https://example.com/vision');
assert.equal(saved.type, 'clip_vision');
assert.equal(saved.path_idx, 1);
assert.equal(saved.filename, 'vision.safetensors');
assert.equal(saved.custom_source_url, 'https://example.com/vision');
console.log('PASS component collection, category-scoped lookup, manual workflow links, batching and independent UI status.');