e19f988c72
The watched live session for the six deferred lifecycle items, each verified against real yt-dlp/Electron behavior on Windows: - R4: cancel now deletes the download's orphaned partials. The engine captures the final output path via a new `--print before_dl:dest|%(filename)s` (empirically %(filepath)s is still 'NA' that early) and the close handler sweeps only unambiguous intermediates (.part / .part-Frag* / .ytdl / <stem>.fNNN.<ext>) via the pure, unit-tested lib/partials.ts — never a bare <stem>.<ext> or another download's files. Live-verified with decoys. - L65: verified-acceptable, no code change — a taskkill /F pause leaves the .part byte-intact and yt-dlp --continue resumes at the exact byte offset. - L139: spawn↔self-update interlock (new ytdlpLock.ts). The updater refuses while any yt-dlp spawn is live; download/terminal IPC handlers hold (await) behind an in-progress exe rewrite instead of failing. - B2: source indexing is cancellable — AbortSignal through the probe walk (kills the in-flight child, live-verified via tasklist), sources:index-cancel IPC, and a Cancel button in the Library add-source row. - B6: app-update download is cancellable — app:update-cancel routes through the existing finish() teardown (abort + destroy + unlink, live-verified on Electron net.request); Cancel button under the update progress bar. The checksum phase is cancelable too. - L143: closing the window mid-download (non-tray mode) now offers a native tray-independent "Quit anyway / Keep downloading" dialog, replacing the passive "use the tray to quit" notification. Peer-review pass caught and fixed: a non-reentrant update lock (concurrent begin clobbered the settle promise), a dead Cancel window during the B6 checksum fetch + a canceller slot-ownership bug (L140 shape), and a persist-after-cancel hole in the index walk. typecheck + 304 tests + eslint + production build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
75 lines
3.0 KiB
TypeScript
75 lines
3.0 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
// Pure module — no electron import chain (L37), same as lib/formatters.
|
|
import { orphanPartials } from '../src/main/lib/partials'
|
|
|
|
// The resolved FINAL output path the before_dl `dest|` print gives download.ts.
|
|
const OUT = 'C:\\Users\\me\\Videos\\Despacito.webm'
|
|
|
|
describe('orphanPartials (R4 cancel cleanup)', () => {
|
|
it('deletes the in-progress .part stream file', () => {
|
|
expect(orphanPartials(OUT, ['Despacito.f399.mp4.part'])).toEqual(['Despacito.f399.mp4.part'])
|
|
})
|
|
|
|
it('deletes a finished per-stream intermediate (no .part) awaiting merge', () => {
|
|
// e.g. the video half of a video+audio download whose audio was still in flight.
|
|
expect(orphanPartials(OUT, ['Despacito.f251.webm'])).toEqual(['Despacito.f251.webm'])
|
|
})
|
|
|
|
it('deletes fragment pieces and the .ytdl fragment-state file', () => {
|
|
const entries = [
|
|
'Despacito.f140.m4a.part-Frag0',
|
|
'Despacito.f140.m4a.part-Frag12',
|
|
'Despacito.f140.m4a.ytdl'
|
|
]
|
|
expect(orphanPartials(OUT, entries)).toEqual(entries)
|
|
})
|
|
|
|
it('KEEPS a pre-existing same-title completed file (video and audio)', () => {
|
|
// The audit's "delete the wrong file" risk: a bare <stem>.<ext> is never ours.
|
|
expect(orphanPartials(OUT, ['Despacito.webm', 'Despacito.mp3', 'Despacito.mkv'])).toEqual([])
|
|
})
|
|
|
|
it('KEEPS a different download in the same folder (different stem)', () => {
|
|
expect(orphanPartials(OUT, ['Other Video.f399.mp4.part', 'Other Video.f251.webm'])).toEqual([])
|
|
})
|
|
|
|
it('does not match a stem that is only a prefix of a longer name', () => {
|
|
// "Despacito 2.f399.mp4.part" must NOT be swept by the "Despacito" download —
|
|
// startsWith('Despacito.') excludes it (the dot boundary matters).
|
|
expect(orphanPartials(OUT, ['Despacito 2.f399.mp4.part', 'Despacito2.webm.part'])).toEqual([])
|
|
})
|
|
|
|
it('handles a title containing dots (only the final extension is the boundary)', () => {
|
|
const out = 'C:\\Users\\me\\Music\\Track 3.14 - Pi.mp3'
|
|
const entries = [
|
|
'Track 3.14 - Pi.webm.part',
|
|
'Track 3.14 - Pi.mp3',
|
|
'Track 3.14 - Pi.f251.webm'
|
|
]
|
|
// .part + intermediate deleted; the completed .mp3 kept.
|
|
expect(orphanPartials(out, entries)).toEqual([
|
|
'Track 3.14 - Pi.webm.part',
|
|
'Track 3.14 - Pi.f251.webm'
|
|
])
|
|
})
|
|
|
|
it('sorts nothing / preserves order and mixes correctly in a realistic listing', () => {
|
|
const entries = [
|
|
'Despacito.f399.mp4.part', // delete: in-progress video
|
|
'Despacito.f251.webm', // delete: finished audio intermediate
|
|
'Despacito.webm', // keep: pre-existing final
|
|
'Unrelated.f140.m4a.part', // keep: other download
|
|
'Despacito.f140.m4a.ytdl' // delete: fragment state
|
|
]
|
|
expect(orphanPartials(OUT, entries)).toEqual([
|
|
'Despacito.f399.mp4.part',
|
|
'Despacito.f251.webm',
|
|
'Despacito.f140.m4a.ytdl'
|
|
])
|
|
})
|
|
|
|
it('returns [] when the path has no usable stem', () => {
|
|
expect(orphanPartials('', ['x.part'])).toEqual([])
|
|
})
|
|
})
|