mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-29 17:01:26 -03:00
fix(recipes): resolve stale LoRA hash on import and add hashInvalid state
- import: prefer A1111 Lora hashes (12-char AutoV3) over conflicting Hashes JSON values; recover the quote-wrapped AutoV3 from CivitAI image API meta; merge EXIF-parsed LoRAs when the API-only parse yields none (meta=null) - rematch: treat entries whose hash failed CivitAI resolution (hashInvalid) as unresolved candidates; clear the flag on rematch/reconnect write-back - download: persist hashInvalid and show a distinct toast when hash lookup returns "Model not found", so unresolvable entries become recoverable - ui: add Unresolvable Hash badge styling and reconnect affordance - i18n: translate the new keys across all 10 locales
This commit is contained in:
@@ -142,6 +142,14 @@ const hashOnlyLora = {
|
||||
hash: 'deadbeefcafe',
|
||||
};
|
||||
|
||||
const hashInvalidLora = {
|
||||
name: 'invalid-hash-lora',
|
||||
modelName: 'Invalid Hash LoRA',
|
||||
inLibrary: false,
|
||||
hash: 'a2a12bfa01',
|
||||
hashInvalid: true,
|
||||
};
|
||||
|
||||
const recipeWithResources = {
|
||||
id: 'recipe-resources',
|
||||
file_path: '/recipes/resources.json',
|
||||
@@ -157,6 +165,7 @@ const recipeWithResources = {
|
||||
{ name: 'present-lora', modelName: 'Present LoRA', inLibrary: true, hash: 'ABC123' },
|
||||
missingLora,
|
||||
{ name: 'deleted-lora', modelName: 'Deleted LoRA', inLibrary: false, isDeleted: true },
|
||||
hashInvalidLora,
|
||||
{ name: 'mystery-lora', modelName: 'Mystery LoRA', inLibrary: false },
|
||||
hashOnlyLora,
|
||||
],
|
||||
@@ -321,11 +330,67 @@ describe('RecipeModal resource item interactions', () => {
|
||||
expect(container.classList.contains('active')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders hash-invalid LoRAs with a dedicated badge and reconnect instead of download', async () => {
|
||||
const recipeModal = await createRecipeModal();
|
||||
recipeModal.showRecipeDetails(recipeWithResources);
|
||||
await flushWiring();
|
||||
|
||||
const invalidItem = document.querySelector('[data-lora-index="3"]');
|
||||
const badge = invalidItem.querySelector('.invalid-hash-badge');
|
||||
expect(badge).not.toBeNull();
|
||||
expect(badge.title).toContain('cannot be resolved on CivitAI');
|
||||
expect(badge.textContent).toContain('Unresolvable Hash');
|
||||
|
||||
expect(invalidItem.querySelector('.lora-download')).toBeNull();
|
||||
expect(invalidItem.querySelector('.lora-reconnect')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('marks the entry hash-invalid when hash resolution returns Model not found', async () => {
|
||||
const recipeModal = await createRecipeModal();
|
||||
const requests = [];
|
||||
// Deep copy so the mark step mutating loras[5].hashInvalid does not
|
||||
// leak into the shared fixture used by later tests.
|
||||
const isolatedRecipe = JSON.parse(JSON.stringify(recipeWithResources));
|
||||
fetchRecipeDetailsMock.mockResolvedValue(isolatedRecipe);
|
||||
global.fetch = vi.fn(async (url, options) => {
|
||||
requests.push({ url: String(url), options });
|
||||
const urlStr = String(url);
|
||||
if (urlStr.includes('/civitai/model/hash/')) {
|
||||
return { ok: false, json: async () => ({ success: false, error: 'Model not found' }) };
|
||||
}
|
||||
return { ok: true, json: async () => ({}) };
|
||||
});
|
||||
recipeModal.showRecipeDetails(isolatedRecipe);
|
||||
await flushWiring();
|
||||
|
||||
const hashItem = document.querySelector('[data-lora-index="5"]');
|
||||
const downloadButton = hashItem.querySelector('.lora-download');
|
||||
downloadButton.click();
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(
|
||||
requests.some(r => r.url.includes('/recipe/lora/mark-hash-invalid'))
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
const markRequest = requests.find(r => r.url.includes('/mark-hash-invalid'));
|
||||
expect(JSON.parse(markRequest.options.body)).toEqual({
|
||||
recipe_id: 'recipe-resources',
|
||||
lora_index: 5,
|
||||
});
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
'toast.recipes.hashNotFoundOnCivitai',
|
||||
{},
|
||||
'error'
|
||||
);
|
||||
expect(downloadVersionWithDefaultsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders no action row when neither identifiers nor hash are available', async () => {
|
||||
const recipeModal = await createRecipeModal();
|
||||
recipeModal.showRecipeDetails(recipeWithResources);
|
||||
|
||||
const mysteryItem = document.querySelector('[data-lora-index="3"]');
|
||||
const mysteryItem = document.querySelector('[data-lora-index="4"]');
|
||||
expect(mysteryItem.querySelector('.lora-download')).toBeNull();
|
||||
// No actions at all -> no empty action row taking vertical space
|
||||
expect(mysteryItem.querySelector('.recipe-lora-actions')).toBeNull();
|
||||
@@ -348,7 +413,7 @@ describe('RecipeModal resource item interactions', () => {
|
||||
recipeModal.showRecipeDetails(recipeWithResources);
|
||||
await flushWiring();
|
||||
|
||||
const hashItem = document.querySelector('[data-lora-index="4"]');
|
||||
const hashItem = document.querySelector('[data-lora-index="5"]');
|
||||
const downloadButton = hashItem.querySelector('.lora-download');
|
||||
expect(downloadButton).not.toBeNull();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user