Files
AeroFetch/test/retention.test.ts
T
debont80 0956bd536a feat(pinchflat): Retention & quality-upgrade per Library source
On-demand maintenance panel (SourceRetention) on the source detail —
not a persisted schedule (roadmap's niche-for-desktop scope).

Prune: keep the newest N downloaded files, delete the rest's files
(download archive prevents silent re-download); dry-run count + two-step
confirm before any deletion. Upgrade: re-enqueue items whose recorded
downloadedQuality ranks below the source's current target (profile or
global). Selection is pure + injected-now in @shared/retention.ts
(selectPruneCandidates/selectUpgradeCandidates + qualityRank), unit-
tested; pruneMediaItems (main) deletes best-effort and clears state.
MediaItem.downloadedQuality now recorded through the completion path.
Live-verified the panel renders. 366 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:14:23 -04:00

123 lines
4.4 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { selectPruneCandidates, selectUpgradeCandidates } from '../src/shared/retention'
import { qualityRank, type MediaItem } from '@shared/ipc'
const DAY = 86_400_000
const NOW = 1_700_000_000_000
function item(over: Partial<MediaItem>): MediaItem {
return {
id: over.id ?? 'x',
sourceId: 's',
videoId: over.id ?? 'x',
title: over.title ?? 'V',
url: `https://y/${over.id ?? 'x'}`,
playlistTitle: 'Uploads',
playlistIndex: 1,
downloaded: true,
downloadedAt: NOW,
filePath: `C:/out/${over.id ?? 'x'}.mp4`,
...over
}
}
describe('selectPruneCandidates (retention)', () => {
it('no policy → nothing pruned', () => {
const items = [item({ id: 'a', downloadedAt: NOW - 100 * DAY })]
expect(selectPruneCandidates(items, {}, NOW)).toEqual([])
})
it('maxAgeDays prunes items older than the cutoff', () => {
const items = [
item({ id: 'fresh', downloadedAt: NOW - 5 * DAY }),
item({ id: 'old', downloadedAt: NOW - 40 * DAY })
]
const pruned = selectPruneCandidates(items, { maxAgeDays: 30 }, NOW).map((i) => i.id)
expect(pruned).toEqual(['old'])
})
it('maxCount keeps the N most-recent, prunes the rest', () => {
const items = [
item({ id: 'a', downloadedAt: NOW - 1 * DAY }),
item({ id: 'b', downloadedAt: NOW - 2 * DAY }),
item({ id: 'c', downloadedAt: NOW - 3 * DAY }),
item({ id: 'd', downloadedAt: NOW - 4 * DAY })
]
const pruned = selectPruneCandidates(items, { maxCount: 2 }, NOW)
.map((i) => i.id)
.sort()
expect(pruned).toEqual(['c', 'd'])
})
it('age + count combine — an item over EITHER limit is pruned', () => {
const items = [
item({ id: 'a', downloadedAt: NOW - 1 * DAY }), // within both
item({ id: 'b', downloadedAt: NOW - 2 * DAY }), // within age, over count(1)
item({ id: 'c', downloadedAt: NOW - 40 * DAY }) // over age
]
const pruned = selectPruneCandidates(items, { maxAgeDays: 30, maxCount: 1 }, NOW)
.map((i) => i.id)
.sort()
expect(pruned).toEqual(['b', 'c'])
})
it('never prunes un-downloaded items or ones without a filePath', () => {
const items = [
item({ id: 'pending', downloaded: false, filePath: undefined, downloadedAt: undefined }),
item({ id: 'nofile', filePath: undefined }),
item({ id: 'real', downloadedAt: NOW - 99 * DAY })
]
expect(selectPruneCandidates(items, { maxAgeDays: 30 }, NOW).map((i) => i.id)).toEqual(['real'])
})
})
describe('selectUpgradeCandidates (retention)', () => {
it('selects items downloaded below the target quality', () => {
const items = [
item({ id: 'lo', downloadedQuality: '480p' }),
item({ id: 'ok', downloadedQuality: '1080p' }),
item({ id: 'hi', downloadedQuality: 'Best available' })
]
const up = selectUpgradeCandidates(items, '1080p', qualityRank).map((i) => i.id)
expect(up).toEqual(['lo'])
})
it('skips items with no recorded downloadedQuality (unknown, not re-churned)', () => {
const items = [
item({ id: 'unknown', downloadedQuality: undefined }),
item({ id: 'lo', downloadedQuality: '360p' })
]
expect(selectUpgradeCandidates(items, '720p', qualityRank).map((i) => i.id)).toEqual(['lo'])
})
it('skips un-downloaded items', () => {
const items = [item({ id: 'pending', downloaded: false, downloadedQuality: '360p' })]
expect(selectUpgradeCandidates(items, 'Best available', qualityRank)).toEqual([])
})
it('a "Best" target upgrades anything not already Best', () => {
const items = [
item({ id: 'a', downloadedQuality: '1080p' }),
item({ id: 'b', downloadedQuality: 'Best available' })
]
expect(selectUpgradeCandidates(items, 'Best', qualityRank).map((i) => i.id)).toEqual(['a'])
})
})
describe('qualityRank', () => {
it('ranks Best above all numeric qualities', () => {
expect(qualityRank('Best available')).toBeGreaterThan(qualityRank('1080p'))
expect(qualityRank('Best')).toBeGreaterThan(qualityRank('320 kbps'))
})
it('orders numeric qualities bigger-is-better', () => {
expect(qualityRank('1080p')).toBeGreaterThan(qualityRank('720p'))
expect(qualityRank('320 kbps')).toBeGreaterThan(qualityRank('128 kbps'))
})
it('unknown labels rank lowest', () => {
expect(qualityRank('mystery')).toBe(0)
expect(qualityRank('mystery')).toBeLessThan(qualityRank('360p'))
})
})