mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-29 08:51:27 -03:00
fix(recipes): distinguish unobtainable LoRAs in recipe status and skip them in syntax
The recipe card pill counted LoRAs deleted from the source (isDeleted) as available, showing a green 'ready 2/2' for recipes that cannot be fully reproduced. LoRAs with an unresolvable hash (hashInvalid) were counted as missing/downloadable even though downloads always fail, and recipe syntax generation emitted broken tokens for them. - Four-state status on RecipeCard pill and RecipeTab badge: ready (all in library), missing (downloadable, red, keeps the action cue), partial (unobtainable entries skipped when used, amber, fa-circle-minus), unavailable (nothing usable, gray, fa-ban) - Pill numerator is now the real in-library count; tooltips spell out missing vs unavailable (deleted from source or unresolvable hash) - get_recipe_syntax_tokens skips hashInvalid entries like deleted ones instead of emitting tokens pointing at nonexistent files - Bulk missing-download manager and recipe context menu exclude hashInvalid LoRAs, matching the modal's per-item download block - New locale keys loraStatus.missingAndUnavailable/partial/noneUsable, translated for all 9 non-en locales
This commit is contained in:
@@ -130,14 +130,61 @@ describe('RecipeCard LoRA status pill', () => {
|
||||
expect(card.element.querySelector('.lora-count.missing')).toBeNull();
|
||||
});
|
||||
|
||||
it('does not count deleted LoRAs as missing', async () => {
|
||||
it('marks deleted-from-source LoRAs as partial instead of ready', async () => {
|
||||
const card = await createCard([
|
||||
{ name: 'a', inLibrary: true },
|
||||
{ name: 'b', inLibrary: false, isDeleted: true },
|
||||
]);
|
||||
|
||||
expect(card.element.querySelector('.lora-count.missing')).toBeNull();
|
||||
expect(card.element.querySelector('.lora-count.ready')).not.toBeNull();
|
||||
expect(card.element.querySelector('.lora-count.ready')).toBeNull();
|
||||
|
||||
const pill = card.element.querySelector('.lora-count.partial');
|
||||
expect(pill).not.toBeNull();
|
||||
expect(pill.querySelector('.fa-circle-minus')).not.toBeNull();
|
||||
expect(pill.textContent).toContain('1/2');
|
||||
expect(pill.title).toBe('1 of 2 LoRAs unavailable (deleted from source or unresolvable hash) - skipped when recipe is used');
|
||||
});
|
||||
|
||||
it('treats an unresolvable hash as unobtainable, not missing', async () => {
|
||||
const card = await createCard([
|
||||
{ name: 'a', inLibrary: true },
|
||||
{ name: 'b', inLibrary: false, hashInvalid: true },
|
||||
]);
|
||||
|
||||
expect(card.element.querySelector('.lora-count.missing')).toBeNull();
|
||||
|
||||
const pill = card.element.querySelector('.lora-count.partial');
|
||||
expect(pill).not.toBeNull();
|
||||
expect(pill.textContent).toContain('1/2');
|
||||
});
|
||||
|
||||
it('shows 0/n unavailable when every LoRA is deleted and none is in the library', async () => {
|
||||
const card = await createCard([
|
||||
{ name: 'a', inLibrary: false, isDeleted: true },
|
||||
{ name: 'b', inLibrary: false, isDeleted: true },
|
||||
]);
|
||||
|
||||
const pill = card.element.querySelector('.lora-count.unavailable');
|
||||
expect(pill).not.toBeNull();
|
||||
expect(pill.querySelector('.fa-ban')).not.toBeNull();
|
||||
expect(pill.textContent).toContain('0/2');
|
||||
expect(pill.title).toBe('No usable LoRAs - 2 of 2 deleted from source or unresolvable hash');
|
||||
expect(card.element.querySelector('.lora-count.ready')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps the actionable missing state when LoRAs are both missing and deleted', async () => {
|
||||
const card = await createCard([
|
||||
{ name: 'a', inLibrary: true },
|
||||
{ name: 'b', inLibrary: false },
|
||||
{ name: 'c', inLibrary: false, isDeleted: true },
|
||||
]);
|
||||
|
||||
const pill = card.element.querySelector('.lora-count.missing');
|
||||
expect(pill).not.toBeNull();
|
||||
expect(pill.textContent).toContain('1/3');
|
||||
expect(pill.title).toBe('1 of 3 LoRAs missing, 1 unavailable (deleted from source or unresolvable hash)');
|
||||
expect(card.element.querySelector('.lora-count.partial')).toBeNull();
|
||||
});
|
||||
|
||||
it('shows a neutral layers icon with a bare 0 when the recipe has no LoRAs', async () => {
|
||||
|
||||
@@ -374,6 +374,34 @@ async def test_set_lora_entry_hash_invalid_persists_flag(tmp_path: Path, recipe_
|
||||
assert cleared_lora["hashInvalid"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_recipe_syntax_tokens_skips_unobtainable_loras(tmp_path: Path, recipe_scanner):
|
||||
scanner, _ = recipe_scanner
|
||||
recipes_dir = Path(config.loras_roots[0]) / "recipes"
|
||||
recipes_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
recipe_id = "syntax-skip-1"
|
||||
recipe_path = recipes_dir / f"{recipe_id}.recipe.json"
|
||||
recipe_data = {
|
||||
"id": recipe_id,
|
||||
"file_path": str(tmp_path / "image.png"),
|
||||
"title": "Syntax skip",
|
||||
"modified": 0.0,
|
||||
"created_date": 0.0,
|
||||
"loras": [
|
||||
{"file_name": "usable_lora", "strength": 0.8, "hash": ""},
|
||||
{"file_name": "deleted_lora", "strength": 1.0, "hash": "", "isDeleted": True},
|
||||
{"file_name": "invalid_hash_lora", "strength": 1.0, "hash": "", "hashInvalid": True},
|
||||
],
|
||||
}
|
||||
recipe_path.write_text(json.dumps(recipe_data))
|
||||
await scanner.add_recipe(dict(recipe_data))
|
||||
|
||||
tokens = await scanner.get_recipe_syntax_tokens(recipe_id)
|
||||
|
||||
assert tokens == ["<lora:usable_lora:0.8>"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_recipe_rewrites_missing_image_path(tmp_path: Path, recipe_scanner):
|
||||
scanner, _ = recipe_scanner
|
||||
|
||||
Reference in New Issue
Block a user