diff --git a/lib/queue-dedup.js b/lib/queue-dedup.js index 4b97799..70a2254 100644 --- a/lib/queue-dedup.js +++ b/lib/queue-dedup.js @@ -76,7 +76,8 @@ const keyUnambiguous = k !== null && filesPerKey.get(k).size <= 1; const uploadedAfterSnapshot = savedAtFloor !== null && k !== null && keyUnambiguous && logMaxTs.has(k) && logMaxTs.get(k) >= savedAtFloor; - if (doneInLog || uploadedAfterSnapshot) { + const hasTerminalResult = job && job.status === 'done' && job.result && typeof job.result === 'object'; + if (!hasTerminalResult && (doneInLog || uploadedAfterSnapshot)) { removed.push(job); } else { kept.push(job); diff --git a/tests/queue-dedup.test.js b/tests/queue-dedup.test.js index c567d5b..8065724 100644 --- a/tests/queue-dedup.test.js +++ b/tests/queue-dedup.test.js @@ -73,6 +73,32 @@ test('empty/missing inputs do not throw', () => { const T = (s) => Date.parse(s.replace(' ', 'T')); +test('history fallback keeps a serialized successful terminal result through log deduplication', () => { + const result = { + download_url: 'https://voe.sx/e/exact-link', + embed_url: 'https://voe.sx/e/exact-embed', + file_code: 'exact-code' + }; + const restored = JSON.parse(JSON.stringify({ + savedAt: T('2026-08-13 12:00:00'), + queueJobs: [{ + id: 'history-fallback-done', + file: 'C:/dl/episode.mkv', + fileName: 'episode.mkv', + hoster: 'voe.sx', + status: 'done', + result + }] + })); + const log = [{ fileName: 'episode.mkv', hoster: 'voe.sx', ts: T('2026-08-13 12:00:05') }]; + + const { kept, removed } = partitionRestoredJobsByLog(restored.queueJobs, log, restored.savedAt); + + assert.deepEqual(removed, []); + assert.deepEqual(kept, restored.queueJobs); + assert.deepEqual(kept[0].result, result); +}); + test('ts-gate: preview job uploaded AFTER the snapshot is dropped (the ghost bug)', () => { const jobs = [job('preview', 'a.mkv', 'voe.sx')]; const log = [{ fileName: 'a.mkv', hoster: 'voe.sx', ts: T('2026-06-19 12:00:05') }];