Unify metadata workflow cache and category handling

This commit is contained in:
Dariusz L
2026-09-13 23:09:22 +02:00
parent 21245569c7
commit 552ef683da
11 changed files with 318 additions and 111 deletions
@@ -2096,6 +2096,40 @@ test('background Loaded Models refresh keeps the current view until new data is
assert.equal(contentElement.scrollTop, 48);
});
test('Loaded Models renders the shared cache without starting another scan', async () => {
const loadLoadedModels = eval(`(${extractMethod(tabsLoadedMethodsSource, 'loadLoadedModels')})`);
const getCachedLoadedModelsForSignature = eval(`(${extractMethod(
workflowStateMethodsSource,
'getCachedLoadedModelsForSignature'
)})`);
const cachedData = { loaded_models: [{ strength: 0.5 }], total: 1 };
const contentElement = { innerHTML: '' };
let fetchCount = 0;
let renderCount = 0;
const dialog = {
activeTab: 'loaded',
contentElement,
cachedLoadedModelsSignature: 'workflow-a',
cachedLoadedModelsData: cachedData,
syncWorkflowScopedQueue() {},
getWorkflowSignature: () => 'workflow-a',
getCachedLoadedModelsForSignature,
fetchJson: async () => {
fetchCount += 1;
throw new Error('The shared cache was not used');
},
displayLoadedModels: (_container, data) => {
renderCount += 1;
assert.equal(data, cachedData);
},
};
await loadLoadedModels.call(dialog, { nodes: [] });
assert.equal(fetchCount, 0);
assert.equal(renderCount, 1);
});
test('Loaded Models progress patches the stable progress container', () => {
const pollWorkflowProgress = extractMethod(renderFormatMethodsSource, 'pollWorkflowProgress');
const patchLoadedModelsProgress = extractMethod(
@@ -2730,6 +2764,31 @@ test('workflow analysis and loaded model caches stay independent and cloned', ()
assert.equal(dialog.workflowAnalysisCaches.get(dialog.workflowKey).data.missing_models.length, 1);
});
test('shared loaded model cache requires a matching workflow signature unless forced', () => {
const getCachedLoadedModelsForSignature = eval(`(${extractMethod(
workflowStateMethodsSource,
'getCachedLoadedModelsForSignature'
)})`);
const cachedData = { loaded_models: [{ filename: 'loaded.safetensors' }] };
const dialog = {
cachedLoadedModelsSignature: 'workflow-a',
cachedLoadedModelsData: cachedData,
};
assert.equal(
getCachedLoadedModelsForSignature.call(dialog, 'workflow-a'),
cachedData
);
assert.equal(
getCachedLoadedModelsForSignature.call(dialog, 'workflow-b'),
null
);
assert.equal(
getCachedLoadedModelsForSignature.call(dialog, 'workflow-a', { force: true }),
null
);
});
test('Missing Models filter toggles persist state and rerender once per change', () => {
const bindMissingFilterToggle = eval(
`(${extractMethod(missingBrowserMethodsSource, 'bindMissingFilterToggle')})`
+117
View File
@@ -147,6 +147,123 @@ function createLoraTransferState(sourceModels, activityScope = 'all') {
};
}
test('metadata transfer persists only mode and activity scope preferences', () => {
const context = createTransferContext();
assert.deepEqual(context.getMetadataTransferPreferences(), {
mode: 'replace',
activityScope: 'all',
});
assert.equal(context.setMetadataTransferMode('merge'), 'merge');
assert.equal(context.setMetadataTransferActivityScope('inactive'), 'inactive');
assert.deepEqual(context.getMetadataTransferPreferences(), {
mode: 'merge',
activityScope: 'inactive',
});
});
test('metadata tab reuses transfer state for the unchanged workflow', () => {
const context = createTransferContext();
context.getWorkflowSignature = workflow => workflow.signature;
const state = { transfer: { workflowSignature: 'workflow-a' } };
assert.equal(
context.isMetadataTransferCurrentWorkflow(state, { signature: 'workflow-a' }),
true
);
assert.equal(
context.isMetadataTransferCurrentWorkflow(state, { signature: 'workflow-b' }),
false
);
assert.equal(
context.isMetadataTransferCurrentWorkflow({ transfer: { workflowSignature: '' } }, null),
true
);
assert.equal(context.isMetadataTransferCurrentWorkflow(null, null), false);
});
test('metadata transfer reuses the shared Loaded Models cache for the same workflow', async () => {
const cachedData = {
loaded_models: [{
category: 'text_encoders',
node_id: 1,
node_type: 'CLIPLoader',
widget_index: 0,
original_path: 'current.safetensors',
active: true,
connected: true,
is_top_level: true,
}],
total: 1,
};
let fetchCount = 0;
const context = {
...imageMetadataMethods,
getCurrentWorkflow: () => ({ signature: 'workflow-a' }),
getWorkflowSignature: workflow => workflow.signature,
cachedLoadedModelsSignature: 'workflow-a',
cachedLoadedModelsData: cachedData,
getCachedLoadedModelsForSignature(signature, { force = false } = {}) {
return !force && signature === this.cachedLoadedModelsSignature
? this.cachedLoadedModelsData
: null;
},
fetchJson: async () => {
fetchCount += 1;
throw new Error('The shared cache was not used');
},
renderImageMetadataResult() {},
};
const result = await context.openMetadataTransfer();
assert.equal(fetchCount, 0);
assert.equal(result, cachedData);
assert.equal(context.imageInspectorState.transfer.targetModels, cachedData);
});
test('metadata transfer stores a fresh target scan in the shared Loaded Models cache', async () => {
const freshData = { loaded_models: [], total: 0 };
let saveCount = 0;
const context = {
...imageMetadataMethods,
getCurrentWorkflow: () => ({ signature: 'workflow-b' }),
getWorkflowSignature: workflow => workflow.signature,
cachedLoadedModelsSignature: null,
cachedLoadedModelsData: null,
getCachedLoadedModelsForSignature() {
return null;
},
fetchJson: async () => freshData,
saveLoadedModelsCacheForActiveWorkflow() {
saveCount += 1;
},
renderImageMetadataResult() {},
};
const result = await context.openMetadataTransfer();
assert.equal(result, freshData);
assert.equal(context.cachedLoadedModelsSignature, 'workflow-b');
assert.equal(context.cachedLoadedModelsData, freshData);
assert.equal(saveCount, 1);
});
test('metadata transfer uses the shared canonical category aliases', () => {
const context = createTransferContext();
const state = createTransferState([
{ category: 'clip_gguf', original_path: 'clip.safetensors' },
]);
state.transfer.targetGroups[0].category = 'clips';
state.transfer.targetGroups[0].nodes[0].refs[0].category = 'clips';
const [row] = context.getMetadataTransferPreviewRows(state);
assert.equal(row.sourceUnavailable, false);
assert.equal(row.nextValue, 'clip.safetensors');
});
test('metadata transfer hides imported models with an unsupported file extension', () => {
const context = createTransferContext();
const state = createTransferState([
+7
View File
@@ -4,6 +4,7 @@ from types import SimpleNamespace
import pytest
from core.contracts import ModelReference
from core.type_utils import CATEGORY_MAP
from core.workflow_transfer import (
normalize_transfer_category,
transfer_models_to_workflow,
@@ -48,9 +49,15 @@ def _selector(reference):
def test_normalize_transfer_category_covers_workflow_aliases():
assert normalize_transfer_category("UPSCALE-MODEL") == "upscale_models"
assert normalize_transfer_category("checkpoint") == "checkpoints"
assert normalize_transfer_category("CLIP / GGUF") == "text_encoders"
assert normalize_transfer_category("unknown") == ""
def test_normalize_transfer_category_matches_the_canonical_category_map():
for raw_category, canonical_category in CATEGORY_MAP.items():
assert normalize_transfer_category(raw_category) == canonical_category
def test_replace_fans_one_source_to_selected_slots_and_syncs_named_values():
workflow = {
"nodes": [