fix(watchlist): resolve 429 backoff + unique-exact-match acceptance

This commit is contained in:
afiqzudinhadi 2026-07-30 13:05:31 +08:00
parent 22a409ce9b
commit b77a189848
5 changed files with 70 additions and 16 deletions

View file

@ -34,4 +34,16 @@ describe('resolveNote', () => {
});
expect(seenYear).toBe('2024');
});
test('two exact matches (same title, different ids) → null (ambiguous)', async () => {
const RH1 = { id: 1, title: 'Robin Hood', original_title: 'Robin Hood' };
const RH2 = { id: 2, title: 'Robin Hood', original_title: 'Robin Hood' };
const r = await resolveNote({ media_type: 'Movie' }, 'Robin Hood.md', async () => [RH1, RH2]);
expect(r).toBeNull();
});
test('exact match at non-top index, unique → accepted', async () => {
const NOPE = { id: 9, title: 'Robin Hood Begins', original_title: 'Robin Hood Begins' };
const HIT2 = { id: 10, title: 'Robin Hood', original_title: 'Robin Hood' };
const r = await resolveNote({ media_type: 'Movie' }, 'Robin Hood.md', async () => [NOPE, HIT2]);
expect(r).toEqual({ tmdbId: '10', isMovie: true, matchedTitle: 'Robin Hood' });
});
});

View file

@ -1,5 +1,5 @@
import { describe, expect, test } from 'bun:test';
import { syncFolder, isActive, TmdbRateLimitError, type SyncDeps } from 'packages/obsidian/src/watchlist/SyncEngine';
import { syncFolder, isActive, TmdbRateLimitError, withRateLimitRetry, type SyncDeps } from 'packages/obsidian/src/watchlist/SyncEngine';
import tvDetail from 'tests/fixtures/tmdb-tv-loki.json';
const ENDED_NOTE = `---
@ -35,6 +35,46 @@ function makeDeps(notes: { path: string; content: string }[], detail: any = tvDe
return { deps, writes, fetches };
}
describe('withRateLimitRetry', () => {
test('429 → sleep(retryAfterMs) then retry once, returns result', async () => {
const slept: number[] = [];
let calls = 0;
const fn = async () => {
calls++;
if (calls === 1) {
const e = new TmdbRateLimitError('429');
e.retryAfterMs = 1500;
throw e;
}
return 'ok';
};
const result = await withRateLimitRetry(fn, async ms => { slept.push(ms); });
expect(slept).toEqual([1500]);
expect(calls).toBe(2);
expect(result).toBe('ok');
});
test('non-429 error → no retry, throws immediately', async () => {
let calls = 0;
const fn = async () => {
calls++;
throw new Error('boom');
};
await expect(withRateLimitRetry(fn, async () => {})).rejects.toThrow('boom');
expect(calls).toBe(1);
});
test('429 twice → second failure propagates (retry only once)', async () => {
let calls = 0;
const fn = async () => {
calls++;
const e = new TmdbRateLimitError('429');
e.retryAfterMs = 500;
throw e;
};
await expect(withRateLimitRetry(fn, async () => {})).rejects.toBeInstanceOf(TmdbRateLimitError);
expect(calls).toBe(2);
});
});
describe('isActive tiering', () => {
test('Returning Series → active', () => expect(isActive({ status: 'Returning Series' })).toBe(true));
test('Watching → active regardless of status', () => expect(isActive({ status: 'Ended', watch_status: 'Watching' })).toBe(true));