fix(stats): review fuzzy AniList duplicates before merging

- Preserve merged title aliases for future episodes
- Fail closed when queued writes cannot drain
This commit is contained in:
2026-08-11 23:11:39 -07:00
parent d82a4fd8c6
commit d8c5fc321a
28 changed files with 1625 additions and 44 deletions
@@ -161,3 +161,256 @@ test('AnimeTab merges the selected duplicate entries into the chosen keeper', as
uninstallDom();
}
});
test('AnimeTab keeps a suggested duplicate visible until it is reviewed and merged', async () => {
const uninstallDom = installDom();
const original = {
getAnimeLibrary: apiClient.getAnimeLibrary,
getAnimeMergeRecommendations: apiClient.getAnimeMergeRecommendations,
dismissAnimeMergeRecommendation: apiClient.dismissAnimeMergeRecommendation,
mergeAnime: apiClient.mergeAnime,
};
let entries = [libraryItem(1, 'Show', 2), libraryItem(2, 'Show Season 1', 1)];
let recommendations = [{ recommendationId: 41, animeIds: [1, 2] }];
let mergeCall: { targetAnimeId: number; sourceAnimeIds: number[] } | null = null;
apiClient.getAnimeLibrary = (async () => entries) as typeof apiClient.getAnimeLibrary;
apiClient.getAnimeMergeRecommendations = (async () => ({
recommendations,
})) as typeof apiClient.getAnimeMergeRecommendations;
apiClient.dismissAnimeMergeRecommendation = (async () =>
undefined) as typeof apiClient.dismissAnimeMergeRecommendation;
apiClient.mergeAnime = (async (targetAnimeId: number, sourceAnimeIds: number[]) => {
mergeCall = { targetAnimeId, sourceAnimeIds };
entries = [libraryItem(targetAnimeId, 'Show', 3)];
recommendations = [];
return {
ok: true,
animeId: targetAnimeId,
mergedAnimeIds: sourceAnimeIds,
movedVideos: 1,
} satisfies StatsMergeAnimeResponse;
}) as typeof apiClient.mergeAnime;
try {
const container = document.createElement('div');
document.body.append(container);
const root = createRoot(container);
await act(async () => {
root.render(<AnimeTab />);
});
assert.match(container.textContent ?? '', /Possible duplicate/);
assert.match(container.textContent ?? '', /Show/);
assert.match(container.textContent ?? '', /Show Season 1/);
await act(async () => {
findButton(container, 'Review merge').click();
});
assert.match(container.textContent ?? '', /Merge 2 Library Entries/);
const keeper = [...container.querySelectorAll('button[aria-pressed]')].find((button) =>
(button.textContent ?? '').includes('Show Season 1'),
) as HTMLButtonElement | undefined;
assert.ok(keeper);
await act(async () => {
keeper.click();
});
await act(async () => {
findButton(container, 'Merge Entries').click();
});
assert.deepEqual(mergeCall, { targetAnimeId: 2, sourceAnimeIds: [1] });
assert.doesNotMatch(container.textContent ?? '', /Possible duplicate/);
assert.doesNotMatch(container.textContent ?? '', /Show Season 1/);
await act(async () => root.unmount());
} finally {
Object.assign(apiClient, original);
uninstallDom();
}
});
test('AnimeTab dismisses a false-positive duplicate recommendation', async () => {
const uninstallDom = installDom();
const original = {
getAnimeLibrary: apiClient.getAnimeLibrary,
getAnimeMergeRecommendations: apiClient.getAnimeMergeRecommendations,
dismissAnimeMergeRecommendation: apiClient.dismissAnimeMergeRecommendation,
};
let dismissedId: number | null = null;
apiClient.getAnimeLibrary = (async () => [
libraryItem(1, 'Show', 2),
libraryItem(2, 'Different Show', 1),
]) as typeof apiClient.getAnimeLibrary;
apiClient.getAnimeMergeRecommendations = (async () => ({
recommendations: [{ recommendationId: 73, animeIds: [1, 2] }],
})) as typeof apiClient.getAnimeMergeRecommendations;
apiClient.dismissAnimeMergeRecommendation = (async (recommendationId: number) => {
dismissedId = recommendationId;
}) as typeof apiClient.dismissAnimeMergeRecommendation;
try {
const container = document.createElement('div');
document.body.append(container);
const root = createRoot(container);
await act(async () => root.render(<AnimeTab />));
await act(async () => {
findButton(container, 'Not duplicates').click();
});
assert.equal(dismissedId, 73);
assert.doesNotMatch(container.textContent ?? '', /Possible duplicate/);
await act(async () => root.unmount());
} finally {
Object.assign(apiClient, original);
uninstallDom();
}
});
test('AnimeTab refreshes the library and recommendations when the window regains focus', async () => {
const uninstallDom = installDom();
const original = {
getAnimeLibrary: apiClient.getAnimeLibrary,
getAnimeMergeRecommendations: apiClient.getAnimeMergeRecommendations,
};
let entries = [libraryItem(1, 'Show', 2), libraryItem(2, 'Show Season 1', 1)];
let libraryFetches = 0;
let recommendationFetches = 0;
apiClient.getAnimeLibrary = (async () => {
libraryFetches += 1;
return entries;
}) as typeof apiClient.getAnimeLibrary;
apiClient.getAnimeMergeRecommendations = (async () => {
recommendationFetches += 1;
return { recommendations: [] };
}) as typeof apiClient.getAnimeMergeRecommendations;
try {
const container = document.createElement('div');
document.body.append(container);
const root = createRoot(container);
await act(async () => root.render(<AnimeTab />));
entries = [libraryItem(1, 'Show', 3)];
await act(async () => {
window.dispatchEvent(new window.Event('focus'));
});
assert.equal(libraryFetches, 2);
assert.equal(recommendationFetches, 2);
assert.doesNotMatch(container.textContent ?? '', /Show Season 1/);
await act(async () => root.unmount());
} finally {
Object.assign(apiClient, original);
uninstallDom();
}
});
test('AnimeTab keeps a recommendation visible through a transient refresh failure', async () => {
const uninstallDom = installDom();
const original = {
getAnimeLibrary: apiClient.getAnimeLibrary,
getAnimeMergeRecommendations: apiClient.getAnimeMergeRecommendations,
};
let failRecommendations = false;
apiClient.getAnimeLibrary = (async () => [
libraryItem(1, 'Show', 2),
libraryItem(2, 'Show Season 1', 1),
]) as typeof apiClient.getAnimeLibrary;
apiClient.getAnimeMergeRecommendations = (async () => {
if (failRecommendations) throw new Error('temporary failure');
return { recommendations: [{ recommendationId: 41, animeIds: [1, 2] }] };
}) as typeof apiClient.getAnimeMergeRecommendations;
try {
const container = document.createElement('div');
document.body.append(container);
const root = createRoot(container);
await act(async () => root.render(<AnimeTab />));
assert.match(container.textContent ?? '', /Possible duplicate/);
failRecommendations = true;
await act(async () => window.dispatchEvent(new window.Event('focus')));
assert.match(container.textContent ?? '', /Possible duplicate/);
await act(async () => root.unmount());
} finally {
Object.assign(apiClient, original);
uninstallDom();
}
});
test('AnimeTab retains a recommendation and reports a failed dismissal', async () => {
const uninstallDom = installDom();
const original = {
getAnimeLibrary: apiClient.getAnimeLibrary,
getAnimeMergeRecommendations: apiClient.getAnimeMergeRecommendations,
dismissAnimeMergeRecommendation: apiClient.dismissAnimeMergeRecommendation,
};
apiClient.getAnimeLibrary = (async () => [
libraryItem(1, 'Show', 2),
libraryItem(2, 'Show Season 1', 1),
]) as typeof apiClient.getAnimeLibrary;
apiClient.getAnimeMergeRecommendations = (async () => ({
recommendations: [{ recommendationId: 41, animeIds: [1, 2] }],
})) as typeof apiClient.getAnimeMergeRecommendations;
apiClient.dismissAnimeMergeRecommendation = (async () => {
throw new Error('offline');
}) as typeof apiClient.dismissAnimeMergeRecommendation;
try {
const container = document.createElement('div');
document.body.append(container);
const root = createRoot(container);
await act(async () => root.render(<AnimeTab />));
await act(async () => findButton(container, 'Not duplicates').click());
assert.match(container.textContent ?? '', /Possible duplicate/);
assert.match(container.textContent ?? '', /Could not dismiss this suggestion/);
await act(async () => root.unmount());
} finally {
Object.assign(apiClient, original);
uninstallDom();
}
});
test('AnimeTab keeps an open recommendation review stable during background refresh', async () => {
const uninstallDom = installDom();
const original = {
getAnimeLibrary: apiClient.getAnimeLibrary,
getAnimeMergeRecommendations: apiClient.getAnimeMergeRecommendations,
};
let recommendations = [{ recommendationId: 41, animeIds: [1, 2] as [number, number] }];
apiClient.getAnimeLibrary = (async () => [
libraryItem(1, 'Show', 2),
libraryItem(2, 'Show Season 1', 1),
]) as typeof apiClient.getAnimeLibrary;
apiClient.getAnimeMergeRecommendations = (async () => ({
recommendations,
})) as typeof apiClient.getAnimeMergeRecommendations;
try {
const container = document.createElement('div');
document.body.append(container);
const root = createRoot(container);
await act(async () => root.render(<AnimeTab />));
await act(async () => findButton(container, 'Review merge').click());
assert.match(container.textContent ?? '', /Merge 2 Library Entries/);
recommendations = [];
await act(async () => window.dispatchEvent(new window.Event('focus')));
assert.match(container.textContent ?? '', /Merge 2 Library Entries/);
assert.match(container.textContent ?? '', /Show Season 1/);
await act(async () => root.unmount());
} finally {
Object.assign(apiClient, original);
uninstallDom();
}
});