mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-09 15:30:16 -03:00
feat(recipes): report rematch results with aggregate logs and toast feedback
This commit is contained in:
@@ -2225,7 +2225,7 @@ describe('Interaction-level regression coverage', () => {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
success: true,
|
||||
progress: { status: 'completed', rematched: 2, skipped: 1, total: 3 },
|
||||
progress: { status: 'completed', rematched: 2, skipped: 1, errors: 0, total: 3, matched_recipes: 2, matched_entries: 5, unresolved_recipes: 1, unresolved_entries: 1 },
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -2244,11 +2244,11 @@ describe('Interaction-level regression coverage', () => {
|
||||
expect(global.fetch).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(progressUI.showCancelButton).toHaveBeenCalledTimes(1);
|
||||
expect(progressUI.complete).toHaveBeenCalledWith('Rematched 2 recipes.');
|
||||
expect(progressUI.complete).toHaveBeenCalledWith('Matched 5 entries across 2 recipes.');
|
||||
// Oracle R4-F1 pin: count comes from `rematched`, a blind `repaired` mirror renders undefined
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
'globalContextMenu.rematchRecipes.success',
|
||||
{ count: 2 },
|
||||
{ count: 2, recipes: 2, entries: 5, failures: 0 },
|
||||
'success'
|
||||
);
|
||||
expect(window.recipesPage.refresh).toHaveBeenCalledTimes(1);
|
||||
@@ -2259,6 +2259,165 @@ describe('Interaction-level regression coverage', () => {
|
||||
delete stateStub.currentPageType;
|
||||
});
|
||||
|
||||
it('uses the warning toast variant when a global rematch completes with failures', async () => {
|
||||
document.body.innerHTML = `
|
||||
<div id="globalContextMenu" class="context-menu">
|
||||
<div class="context-menu-item" data-action="rematch-recipes"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const { GlobalContextMenu } = await import('../../../static/js/components/ContextMenu/GlobalContextMenu.js');
|
||||
const menu = new GlobalContextMenu();
|
||||
const rematchItem = document.querySelector('[data-action="rematch-recipes"]');
|
||||
|
||||
const progressUI = {
|
||||
updateProgress: vi.fn(),
|
||||
showCancelButton: vi.fn(),
|
||||
complete: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
loadingManagerStub.showEnhancedProgress = vi.fn(() => progressUI);
|
||||
window.recipesPage = { refresh: vi.fn() };
|
||||
|
||||
stateStub.currentPageType = 'recipes';
|
||||
menu.showMenu(100, 200);
|
||||
|
||||
global.fetch = vi.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ success: true }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
success: true,
|
||||
progress: { status: 'completed', rematched: 2, skipped: 0, errors: 2, total: 3, matched_recipes: 2, matched_entries: 5, unresolved_recipes: 0, unresolved_entries: 0 },
|
||||
}),
|
||||
});
|
||||
|
||||
rematchItem.dispatchEvent(new Event('click', { bubbles: true }));
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await flushAsyncTasks();
|
||||
}
|
||||
|
||||
expect(progressUI.complete).toHaveBeenCalledWith('Matched 5 entries across 2 recipes, 2 failed.');
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
'globalContextMenu.rematchRecipes.successErrors',
|
||||
{ count: 2, recipes: 2, entries: 5, failures: 2 },
|
||||
'warning'
|
||||
);
|
||||
expect(menu._rematchInProgress).toBe(false);
|
||||
|
||||
delete window.recipesPage;
|
||||
delete stateStub.currentPageType;
|
||||
});
|
||||
|
||||
it('toasts an error when every recipe in a global rematch failed', async () => {
|
||||
document.body.innerHTML = `
|
||||
<div id="globalContextMenu" class="context-menu">
|
||||
<div class="context-menu-item" data-action="rematch-recipes"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const { GlobalContextMenu } = await import('../../../static/js/components/ContextMenu/GlobalContextMenu.js');
|
||||
const menu = new GlobalContextMenu();
|
||||
const rematchItem = document.querySelector('[data-action="rematch-recipes"]');
|
||||
|
||||
const progressUI = {
|
||||
updateProgress: vi.fn(),
|
||||
showCancelButton: vi.fn(),
|
||||
complete: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
loadingManagerStub.showEnhancedProgress = vi.fn(() => progressUI);
|
||||
window.recipesPage = { refresh: vi.fn() };
|
||||
|
||||
stateStub.currentPageType = 'recipes';
|
||||
menu.showMenu(100, 200);
|
||||
|
||||
global.fetch = vi.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ success: true }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
success: true,
|
||||
progress: { status: 'completed', rematched: 0, skipped: 0, errors: 3, total: 3, matched_recipes: 0, matched_entries: 0, unresolved_recipes: 0, unresolved_entries: 0 },
|
||||
}),
|
||||
});
|
||||
|
||||
rematchItem.dispatchEvent(new Event('click', { bubbles: true }));
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await flushAsyncTasks();
|
||||
}
|
||||
|
||||
expect(progressUI.complete).toHaveBeenCalledWith('Rematch failed for 3 of 3 recipes.');
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
'globalContextMenu.rematchRecipes.allFailed',
|
||||
{ total: 3, recipes: 0, entries: 0, failures: 3 },
|
||||
'error'
|
||||
);
|
||||
expect(menu._rematchInProgress).toBe(false);
|
||||
|
||||
delete window.recipesPage;
|
||||
delete stateStub.currentPageType;
|
||||
});
|
||||
|
||||
it('toasts an info message when a global rematch found no local matches', async () => {
|
||||
document.body.innerHTML = `
|
||||
<div id="globalContextMenu" class="context-menu">
|
||||
<div class="context-menu-item" data-action="rematch-recipes"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const { GlobalContextMenu } = await import('../../../static/js/components/ContextMenu/GlobalContextMenu.js');
|
||||
const menu = new GlobalContextMenu();
|
||||
const rematchItem = document.querySelector('[data-action="rematch-recipes"]');
|
||||
|
||||
const progressUI = {
|
||||
updateProgress: vi.fn(),
|
||||
showCancelButton: vi.fn(),
|
||||
complete: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
loadingManagerStub.showEnhancedProgress = vi.fn(() => progressUI);
|
||||
window.recipesPage = { refresh: vi.fn() };
|
||||
|
||||
stateStub.currentPageType = 'recipes';
|
||||
menu.showMenu(100, 200);
|
||||
|
||||
global.fetch = vi.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ success: true }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
success: true,
|
||||
progress: { status: 'completed', rematched: 0, skipped: 2, errors: 0, total: 3, matched_recipes: 0, matched_entries: 0, unresolved_recipes: 1, unresolved_entries: 2 },
|
||||
}),
|
||||
});
|
||||
|
||||
rematchItem.dispatchEvent(new Event('click', { bubbles: true }));
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await flushAsyncTasks();
|
||||
}
|
||||
|
||||
expect(progressUI.complete).toHaveBeenCalledWith('No local match found for 2 entries in 1 recipes.');
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
'globalContextMenu.rematchRecipes.noMatch',
|
||||
{ entries: 2, recipes: 1, total: 3, failures: 0 },
|
||||
'info'
|
||||
);
|
||||
expect(menu._rematchInProgress).toBe(false);
|
||||
|
||||
delete window.recipesPage;
|
||||
delete stateStub.currentPageType;
|
||||
});
|
||||
|
||||
it('toasts the rematched count when a global rematch is cancelled', async () => {
|
||||
document.body.innerHTML = `
|
||||
<div id="globalContextMenu" class="context-menu">
|
||||
@@ -2289,7 +2448,7 @@ describe('Interaction-level regression coverage', () => {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
success: true,
|
||||
progress: { status: 'cancelled', rematched: 1, skipped: 0, total: 3 },
|
||||
progress: { status: 'cancelled', rematched: 1, skipped: 0, errors: 0, total: 3, matched_recipes: 1, matched_entries: 2, unresolved_recipes: 0, unresolved_entries: 0 },
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -2299,10 +2458,10 @@ describe('Interaction-level regression coverage', () => {
|
||||
await flushAsyncTasks();
|
||||
}
|
||||
|
||||
expect(progressUI.complete).toHaveBeenCalledWith('Rematch cancelled. 1 recipes were rematched.');
|
||||
expect(progressUI.complete).toHaveBeenCalledWith('Rematch cancelled. 1 recipes updated (2 entries).');
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
'globalContextMenu.rematchRecipes.cancelled',
|
||||
{ count: 1 },
|
||||
{ count: 1, recipes: 1, entries: 2 },
|
||||
'info'
|
||||
);
|
||||
expect(menu._rematchInProgress).toBe(false);
|
||||
|
||||
@@ -78,7 +78,7 @@ describe('RecipeContextMenu.rematchRecipe', () => {
|
||||
global.fetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ success: true, rematched: 2, skipped: 0 }),
|
||||
json: async () => ({ success: true, rematched: 2, skipped: 0, matched_recipes: 1, matched_entries: 2 }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
@@ -96,7 +96,7 @@ describe('RecipeContextMenu.rematchRecipe', () => {
|
||||
});
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
'toast.recipes.rematchComplete',
|
||||
{ rematched: 2, skipped: 0, total: 1 },
|
||||
{ rematched: 2, skipped: 0, total: 1, entries: 2, recipes: 1, failures: 0 },
|
||||
'success'
|
||||
);
|
||||
expect(showToastMock).not.toHaveBeenCalledWith(
|
||||
@@ -111,6 +111,35 @@ describe('RecipeContextMenu.rematchRecipe', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('toasts an info message when the entries had no local match', async () => {
|
||||
const menu = await createMenu();
|
||||
const card = document.getElementById('card');
|
||||
menu.showMenu(100, 100, card);
|
||||
|
||||
global.fetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ success: true, rematched: 0, skipped: 0, unresolved_recipes: 1, unresolved_entries: 2 }),
|
||||
});
|
||||
|
||||
document
|
||||
.querySelector('[data-action="rematch"]')
|
||||
.dispatchEvent(new Event('click', { bubbles: true }));
|
||||
|
||||
await flushAsyncTasks();
|
||||
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
'toast.recipes.rematchUnmatched',
|
||||
{ entries: 2, recipes: 1, total: 1 },
|
||||
'info'
|
||||
);
|
||||
expect(showToastMock).not.toHaveBeenCalledWith(
|
||||
'toast.recipes.rematchSkipped',
|
||||
expect.anything(),
|
||||
expect.anything()
|
||||
);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('toasts the skipped message when nothing was rematched', async () => {
|
||||
const menu = await createMenu();
|
||||
const card = document.getElementById('card');
|
||||
|
||||
@@ -103,9 +103,13 @@ describe('BulkManager.rematchSelectedRecipes', () => {
|
||||
rematchBulkModelsMock.mockResolvedValue({
|
||||
success: true,
|
||||
total: 3,
|
||||
rematched: 2,
|
||||
rematched: 4,
|
||||
skipped: 1,
|
||||
errors: 0,
|
||||
matched_recipes: 2,
|
||||
matched_entries: 4,
|
||||
unresolved_recipes: 1,
|
||||
unresolved_entries: 1,
|
||||
recipes: [rematchedRecipe],
|
||||
});
|
||||
|
||||
@@ -118,7 +122,7 @@ describe('BulkManager.rematchSelectedRecipes', () => {
|
||||
]);
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
'toast.recipes.rematchComplete',
|
||||
{ rematched: 2, skipped: 1, total: 3 },
|
||||
{ rematched: 4, skipped: 1, total: 3, entries: 4, recipes: 2, failures: 0 },
|
||||
'success'
|
||||
);
|
||||
expect(showToastMock).not.toHaveBeenCalledWith(
|
||||
@@ -132,6 +136,98 @@ describe('BulkManager.rematchSelectedRecipes', () => {
|
||||
expect(loadingManagerStub.restoreProgressBar).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses the errors toast variant when the bulk rematch has failures', async () => {
|
||||
const bulk = await createBulkManager();
|
||||
stateStub.selectedModels.add('/recipes/a.webp');
|
||||
stateStub.selectedModels.add('/recipes/b.webp');
|
||||
|
||||
rematchBulkModelsMock.mockResolvedValue({
|
||||
success: true,
|
||||
total: 2,
|
||||
rematched: 3,
|
||||
skipped: 0,
|
||||
errors: 2,
|
||||
matched_recipes: 1,
|
||||
matched_entries: 3,
|
||||
unresolved_recipes: 0,
|
||||
unresolved_entries: 0,
|
||||
recipes: [],
|
||||
});
|
||||
|
||||
await bulk.rematchSelectedRecipes();
|
||||
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
'toast.recipes.rematchCompleteErrors',
|
||||
{ rematched: 3, skipped: 0, total: 2, entries: 3, recipes: 1, failures: 2 },
|
||||
'warning'
|
||||
);
|
||||
});
|
||||
|
||||
it('toasts an error when every selected recipe failed to rematch', async () => {
|
||||
const bulk = await createBulkManager();
|
||||
stateStub.selectedModels.add('/recipes/a.webp');
|
||||
stateStub.selectedModels.add('/recipes/b.webp');
|
||||
|
||||
rematchBulkModelsMock.mockResolvedValue({
|
||||
success: true,
|
||||
total: 2,
|
||||
rematched: 0,
|
||||
skipped: 0,
|
||||
errors: 2,
|
||||
matched_recipes: 0,
|
||||
matched_entries: 0,
|
||||
unresolved_recipes: 0,
|
||||
unresolved_entries: 0,
|
||||
recipes: [],
|
||||
});
|
||||
|
||||
await bulk.rematchSelectedRecipes();
|
||||
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
'toast.recipes.rematchAllFailed',
|
||||
{ total: 2, failures: 2 },
|
||||
'error'
|
||||
);
|
||||
expect(showToastMock).not.toHaveBeenCalledWith(
|
||||
'toast.recipes.rematchSkipped',
|
||||
expect.anything(),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('toasts an info message when entries had no local match', async () => {
|
||||
const bulk = await createBulkManager();
|
||||
stateStub.selectedModels.add('/recipes/a.webp');
|
||||
stateStub.selectedModels.add('/recipes/b.webp');
|
||||
stateStub.selectedModels.add('/recipes/c.webp');
|
||||
|
||||
rematchBulkModelsMock.mockResolvedValue({
|
||||
success: true,
|
||||
total: 3,
|
||||
rematched: 0,
|
||||
skipped: 2,
|
||||
errors: 0,
|
||||
matched_recipes: 0,
|
||||
matched_entries: 0,
|
||||
unresolved_recipes: 1,
|
||||
unresolved_entries: 2,
|
||||
recipes: [],
|
||||
});
|
||||
|
||||
await bulk.rematchSelectedRecipes();
|
||||
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
'toast.recipes.rematchUnmatched',
|
||||
{ entries: 2, recipes: 1, total: 3 },
|
||||
'info'
|
||||
);
|
||||
expect(showToastMock).not.toHaveBeenCalledWith(
|
||||
'toast.recipes.rematchSkipped',
|
||||
expect.anything(),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('toasts the skipped message when nothing was rematched', async () => {
|
||||
const bulk = await createBulkManager();
|
||||
stateStub.selectedModels.add('/recipes/a.webp');
|
||||
|
||||
@@ -2317,6 +2317,18 @@ async def test_rematch_recipe_by_id_lora_l1_write_back(tmp_path: Path, monkeypat
|
||||
assert result["success"] is True
|
||||
assert result["rematched"] == 1
|
||||
assert result["skipped"] == 0
|
||||
assert result["matched_recipes"] == 1
|
||||
assert result["matched_entries"] == 1
|
||||
assert result["unresolved_recipes"] == 0
|
||||
assert result["unresolved_entries"] == 0
|
||||
assert result["details"]["matched"] == [
|
||||
{
|
||||
"type": "lora",
|
||||
"entry": "old.safetensors",
|
||||
"file_name": "m.safetensors",
|
||||
"match_level": "L1",
|
||||
}
|
||||
]
|
||||
assert result["recipe"] is enriched
|
||||
assert result["recipe"]["file_url"] == "/loras_static/preview/enriched.png"
|
||||
|
||||
@@ -2874,12 +2886,20 @@ async def test_rematch_all_recipes_progress_sequence(tmp_path: Path, monkeypatch
|
||||
assert events[3]["skipped"] == 1
|
||||
assert events[3]["errors"] == 0
|
||||
assert events[3]["total"] == 2
|
||||
assert events[3]["matched_recipes"] == 1
|
||||
assert events[3]["matched_entries"] == 1
|
||||
assert events[3]["unresolved_recipes"] == 1
|
||||
assert events[3]["unresolved_entries"] == 1
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["rematched"] == 1
|
||||
assert result["skipped"] == 1
|
||||
assert result["errors"] == 0
|
||||
assert result["total"] == 2
|
||||
assert result["matched_recipes"] == 1
|
||||
assert result["matched_entries"] == 1
|
||||
assert result["unresolved_recipes"] == 1
|
||||
assert result["unresolved_entries"] == 1
|
||||
assert "status" not in result
|
||||
assert resort_calls == [True] # Metis F1 — exactly once per run
|
||||
|
||||
@@ -2953,10 +2973,10 @@ async def test_rematch_all_recipes_per_recipe_error_continues_loop(
|
||||
recipe: Dict[str, Any],
|
||||
local_cache: dict[str, Any],
|
||||
autov3_cache: dict[str, Any],
|
||||
) -> tuple[int, int]:
|
||||
) -> tuple[int, int, dict[str, Any]]:
|
||||
if recipe.get("id") == "boom":
|
||||
raise RuntimeError("kaboom")
|
||||
return (0, 0)
|
||||
return (0, 0, {"matched": [], "unresolved": []})
|
||||
|
||||
monkeypatch.setattr(scanner, "_rematch_single_recipe", fake_single)
|
||||
|
||||
@@ -3140,9 +3160,9 @@ async def test_rematch_bulk_generic_exception_continues(tmp_path: Path, monkeypa
|
||||
calls += 1
|
||||
if calls == 1:
|
||||
raise RuntimeError("match boom")
|
||||
return None
|
||||
return (None, None)
|
||||
|
||||
monkeypatch.setattr(scanner, "_match_rematch_entry", fake_match)
|
||||
monkeypatch.setattr(scanner, "_match_rematch_entry_with_level", fake_match)
|
||||
|
||||
result = await scanner.rematch_recipes_bulk(["r0", "r1"])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user