320 lines
9.8 KiB
JavaScript
320 lines
9.8 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import { queueStorageMethods } from '../web/resolver/actions/queue_storage_methods.js';
|
|
|
|
function createStorage() {
|
|
const values = new Map();
|
|
return {
|
|
getItem(key) {
|
|
return values.has(key) ? values.get(key) : null;
|
|
},
|
|
setItem(key, value) {
|
|
values.set(key, String(value));
|
|
},
|
|
removeItem(key) {
|
|
values.delete(key);
|
|
},
|
|
};
|
|
}
|
|
|
|
function installStorage(storage) {
|
|
const previousDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'localStorage');
|
|
Object.defineProperty(globalThis, 'localStorage', {
|
|
configurable: true,
|
|
enumerable: true,
|
|
value: storage,
|
|
writable: true,
|
|
});
|
|
return () => {
|
|
if (previousDescriptor) {
|
|
Object.defineProperty(globalThis, 'localStorage', previousDescriptor);
|
|
} else {
|
|
delete globalThis.localStorage;
|
|
}
|
|
};
|
|
}
|
|
|
|
test('queue storage persists collapsed state and splitter width immediately', () => {
|
|
const storage = createStorage();
|
|
const restoreStorage = installStorage(storage);
|
|
|
|
try {
|
|
const context = { ...queueStorageMethods };
|
|
context.persistQueueCollapsedState(true, 0);
|
|
context.persistQueueSplitWidth(421, 0);
|
|
|
|
assert.equal(storage.getItem('model_resolver_queue_collapsed'), '1');
|
|
assert.equal(storage.getItem('model_resolver_split_w'), '421');
|
|
} finally {
|
|
restoreStorage();
|
|
}
|
|
});
|
|
|
|
test('queue storage persists active download recovery without transient DOM state', () => {
|
|
const storage = createStorage();
|
|
const restoreStorage = installStorage(storage);
|
|
|
|
try {
|
|
const context = {
|
|
...queueStorageMethods,
|
|
activeDownloadsStorageKey: 'active-download-recovery',
|
|
};
|
|
const progressDiv = { nodeType: 1 };
|
|
const downloadBtn = { nodeType: 1 };
|
|
context.persistActiveDownloadRecovery('download-1', {
|
|
missing: {
|
|
missing_key: 'model-1',
|
|
original_path: 'vae/model.safetensors',
|
|
matches: [{ path: 'old-match' }],
|
|
},
|
|
progressDiv,
|
|
downloadBtn,
|
|
category: 'vae',
|
|
filename: 'model.safetensors',
|
|
sourceUrl: 'https://huggingface.co/example/model/resolve/main/model.safetensors',
|
|
workflowKey: 'workflow-1',
|
|
});
|
|
|
|
const persisted = JSON.parse(storage.getItem('active-download-recovery'));
|
|
assert.equal(persisted['download-1'].filename, 'model.safetensors');
|
|
assert.equal(persisted['download-1'].missing.missing_key, 'model-1');
|
|
assert.equal('matches' in persisted['download-1'].missing, false);
|
|
assert.equal('progressDiv' in persisted['download-1'], false);
|
|
assert.equal('downloadBtn' in persisted['download-1'], false);
|
|
|
|
context.removeActiveDownloadRecovery('download-1');
|
|
assert.deepEqual(JSON.parse(storage.getItem('active-download-recovery')), {});
|
|
} finally {
|
|
restoreStorage();
|
|
}
|
|
});
|
|
|
|
test('download history replaces duplicate identities and keeps newest entries first', () => {
|
|
const storage = createStorage();
|
|
const restoreStorage = installStorage(storage);
|
|
|
|
try {
|
|
const context = {
|
|
...queueStorageMethods,
|
|
downloadHistoryLimit: 10,
|
|
downloadHistoryStorageKey: 'test-download-history',
|
|
updateQueuePanel() {},
|
|
};
|
|
|
|
const first = context.addDownloadHistoryEntry({
|
|
filename: 'model.safetensors',
|
|
path: 'models/model.safetensors',
|
|
category: 'checkpoints',
|
|
sourceUrl: 'https://example.test/model',
|
|
completedAt: '2026-01-01T00:00:00.000Z',
|
|
});
|
|
const second = context.addDownloadHistoryEntry({
|
|
filename: 'model.safetensors',
|
|
path: 'models/model.safetensors',
|
|
category: 'checkpoints',
|
|
sourceUrl: 'https://example.test/model',
|
|
completedAt: '2026-01-02T00:00:00.000Z',
|
|
});
|
|
|
|
assert.notEqual(first.id, second.id);
|
|
assert.deepEqual(context.getDownloadHistory().map(entry => entry.completedAt), [
|
|
'2026-01-02T00:00:00.000Z',
|
|
]);
|
|
|
|
const persisted = JSON.parse(storage.getItem('test-download-history'));
|
|
assert.equal(persisted.length, 1);
|
|
assert.equal(persisted[0].id, second.id);
|
|
} finally {
|
|
restoreStorage();
|
|
}
|
|
});
|
|
|
|
test('queue storage debounces delayed persistence and rejects invalid splitter widths', async () => {
|
|
const storage = createStorage();
|
|
const restoreStorage = installStorage(storage);
|
|
|
|
try {
|
|
const context = { ...queueStorageMethods };
|
|
context.persistQueueCollapsedState(true, 5);
|
|
context.persistQueueCollapsedState(false, 0);
|
|
context.persistQueueSplitWidth(0, 0);
|
|
context.persistQueueSplitWidth('not-a-number', 0);
|
|
context.persistQueueSplitWidth(422.4, 5);
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 15));
|
|
|
|
assert.equal(storage.getItem('model_resolver_queue_collapsed'), '0');
|
|
assert.equal(storage.getItem('model_resolver_split_w'), '422');
|
|
} finally {
|
|
restoreStorage();
|
|
}
|
|
});
|
|
|
|
test('queue storage loads only object history entries and recovers from invalid JSON', () => {
|
|
const storage = createStorage();
|
|
const restoreStorage = installStorage(storage);
|
|
|
|
try {
|
|
storage.setItem('test-history', JSON.stringify([null, 'invalid', 4, { filename: 'kept.safetensors' }]));
|
|
const context = {
|
|
...queueStorageMethods,
|
|
downloadHistoryStorageKey: 'test-history',
|
|
};
|
|
|
|
assert.deepEqual(context.loadDownloadHistory(), [{ filename: 'kept.safetensors' }]);
|
|
assert.equal(context.getDownloadHistory(), context.downloadHistory);
|
|
|
|
storage.setItem('test-history', '{invalid');
|
|
const brokenContext = {
|
|
...queueStorageMethods,
|
|
downloadHistoryStorageKey: 'test-history',
|
|
};
|
|
assert.deepEqual(brokenContext.loadDownloadHistory(), []);
|
|
} finally {
|
|
restoreStorage();
|
|
}
|
|
});
|
|
|
|
test('queue storage saves with a limit and ignores empty history entries', () => {
|
|
const storage = createStorage();
|
|
const restoreStorage = installStorage(storage);
|
|
|
|
try {
|
|
const context = {
|
|
...queueStorageMethods,
|
|
downloadHistoryLimit: 1,
|
|
downloadHistoryStorageKey: 'limited-history',
|
|
downloadHistory: [
|
|
{ filename: 'new.safetensors' },
|
|
{ filename: 'old.safetensors' },
|
|
],
|
|
_downloadHistoryLoaded: true,
|
|
};
|
|
|
|
context.saveDownloadHistory();
|
|
assert.deepEqual(context.downloadHistory, [{ filename: 'new.safetensors' }]);
|
|
assert.equal(context.addDownloadHistoryEntry({}), null);
|
|
} finally {
|
|
restoreStorage();
|
|
}
|
|
});
|
|
|
|
test('queue storage falls back when local storage operations fail', () => {
|
|
const failingStorage = {
|
|
getItem() {
|
|
throw new Error('read failed');
|
|
},
|
|
setItem() {
|
|
throw new Error('write failed');
|
|
},
|
|
removeItem() {
|
|
throw new Error('remove failed');
|
|
},
|
|
};
|
|
const restoreStorage = installStorage(failingStorage);
|
|
|
|
try {
|
|
const context = {
|
|
...queueStorageMethods,
|
|
downloadHistoryStorageKey: 'failing-history',
|
|
updateQueuePanel() {},
|
|
};
|
|
|
|
assert.deepEqual(context.loadDownloadHistory(), []);
|
|
context.saveDownloadHistory();
|
|
context.clearDownloadHistory();
|
|
assert.deepEqual(context.downloadHistory, []);
|
|
} finally {
|
|
restoreStorage();
|
|
}
|
|
});
|
|
|
|
test('queue storage remembers completed downloads using progress and missing-model fallbacks', () => {
|
|
const storage = createStorage();
|
|
const restoreStorage = installStorage(storage);
|
|
|
|
try {
|
|
const context = {
|
|
...queueStorageMethods,
|
|
downloadHistoryStorageKey: 'completed-history',
|
|
updateQueuePanel() {},
|
|
getFilenameFromPath(value = '') {
|
|
return String(value).split(/[\\/]/).at(-1) || '';
|
|
},
|
|
getDownloadWorkflowLabel: () => 'Workflow A',
|
|
getCategoryDisplayName: category => category.toUpperCase(),
|
|
};
|
|
|
|
const entry = context.rememberCompletedDownloadHistory(
|
|
'download-1',
|
|
{
|
|
missing: {
|
|
original_path: 'loras\\fallback.safetensors',
|
|
category: 'loras',
|
|
node_id: 7,
|
|
widget_index: 2,
|
|
node_type: 'LoraLoader',
|
|
},
|
|
workflowId: 'workflow-1',
|
|
sourceUrl: 'https://example.test/source',
|
|
},
|
|
{ path: 'loras/fallback.safetensors', directory: 'loras', total_size: 12 }
|
|
);
|
|
|
|
assert.equal(entry.filename, 'fallback.safetensors');
|
|
assert.equal(entry.categoryLabel, 'LORAS');
|
|
assert.equal(entry.workflowLabel, 'Workflow A');
|
|
assert.equal(entry.status, 'completed');
|
|
assert.equal(context.rememberCompletedDownloadHistory('download-2', {}, {}), null);
|
|
} finally {
|
|
restoreStorage();
|
|
}
|
|
});
|
|
|
|
test('queue storage formats dates and returns folder context only when actionable', () => {
|
|
const context = {
|
|
...queueStorageMethods,
|
|
isLikelyWorkflowId: value => value === 'workflow-id',
|
|
canSwitchToDownloadWorkflow: value => value.workflow_id === 'workflow-id',
|
|
updateQueuePanel() {},
|
|
};
|
|
|
|
assert.notEqual(context.formatDownloadHistoryTime('2026-01-01T00:00:00.000Z'), '');
|
|
assert.equal(context.formatDownloadHistoryTime('invalid-date'), '');
|
|
assert.equal(context.formatDownloadHistoryTime(''), '');
|
|
assert.equal(context.getDownloadHistoryFolderContext({}), null);
|
|
|
|
const contextData = context.getDownloadHistoryFolderContext({
|
|
filename: 'model.safetensors',
|
|
workflowLabel: 'workflow-id',
|
|
});
|
|
assert.equal(contextData.workflow_id, 'workflow-id');
|
|
assert.equal(contextData.open_folder_label, '');
|
|
});
|
|
|
|
test('queue storage removes entries by id or fallback index', () => {
|
|
const storage = createStorage();
|
|
const restoreStorage = installStorage(storage);
|
|
|
|
try {
|
|
const context = {
|
|
...queueStorageMethods,
|
|
downloadHistoryStorageKey: 'remove-history',
|
|
downloadHistory: [
|
|
{ id: 'first', filename: 'first.safetensors' },
|
|
{ id: 'second', filename: 'second.safetensors' },
|
|
],
|
|
_downloadHistoryLoaded: true,
|
|
updateQueuePanel() {},
|
|
};
|
|
|
|
context.removeDownloadHistoryEntry('second');
|
|
context.removeDownloadHistoryEntry('', 0);
|
|
context.removeDownloadHistoryEntry('missing', 99);
|
|
assert.deepEqual(context.downloadHistory, []);
|
|
} finally {
|
|
restoreStorage();
|
|
}
|
|
});
|