0956bd536a
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>
123 lines
4.4 KiB
TypeScript
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'))
|
|
})
|
|
})
|