Harden audit findings: correctness, type-safety, Windows conventions & polish

Audit-pass over CODE-AUDIT.md (~48 items closed this pass; all verified —
typecheck + 234 tests + eslint + prettier green).

Correctness / bugs:
- B3: match the release checksum to the asset's filename line (no wrong-hash verify)
- B4: newline-safe metadata probe (one --print with a unit-separator delimiter)
- B5 / L88: guard the meta event against canceled items; progress no longer promotes
  a queued item outside pump()
- B7: cookie-login promise always resolves (handles destroy-without-close)
- L146: trim parser rejects >2 colon-group times; M36: Library selection counts only
  actionable rows
- L11 / L50 / L156 / L57 / L159 / L15 / L3: live queue count, empty-cookie message,
  schedule picker min, dead-code/comment cleanup

Type safety:
- Enable noUncheckedIndexedAccess + noFallthroughCasesInSwitch (15 real edge cases fixed)

Resilience / Windows / metadata:
- R5: settings write failure handled (no unhandled IPC rejection; reconciles to truth)
- W1 / W5 / W6: min window size, seeded folder picker, parented sign-in window;
  L147 dead macOS branches removed
- CL1: shared stdout markers; package/builder metadata (license, homepage, repository,
  copyright, tsbuildinfo glob)

Copy / docs / tests:
- M37 / SR9 dev-jargon cleanup in hints; M8 / M25 / M26 / L66 / L80 / L81 reconciled
- New unit tests for L35 (isValidMediaItem) and L36 (compareVersions)

This commit also checkpoints the previously-uncommitted feat/tray-background-clipboard
work it builds on: background running + auto-download, library clipboard detection,
tray, binary management & library scale, credential encryption at rest, the shared
jsonStore and ui/ primitives, and the eslint/prettier tooling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 13:02:54 -04:00
parent a6a8c5f578
commit 1376c2dee8
82 changed files with 4571 additions and 1276 deletions
+81 -24
View File
@@ -87,9 +87,9 @@ describe('network options', () => {
it('emits --downloader <path> --downloader-args when an aria2c path is given', () => {
const argv = build(opts(), dlo(), { ...NO_ACCESS, aria2cPath: 'C:/fake/bin/aria2c.exe' })
expect(hasSeq(argv, '--downloader', 'C:/fake/bin/aria2c.exe', '--downloader-args', ARIA2C_ARGS)).toBe(
true
)
expect(
hasSeq(argv, '--downloader', 'C:/fake/bin/aria2c.exe', '--downloader-args', ARIA2C_ARGS)
).toBe(true)
})
it('omits --downloader when aria2cPath is not set', () => {
@@ -144,7 +144,10 @@ describe('restrictFilenames / downloadArchivePath', () => {
})
it('emits --download-archive <path> when a path is given', () => {
const argv = build(opts(), dlo(), { ...NO_ACCESS, downloadArchivePath: 'C:/userdata/archive.txt' })
const argv = build(opts(), dlo(), {
...NO_ACCESS,
downloadArchivePath: 'C:/userdata/archive.txt'
})
expect(hasSeq(argv, '--download-archive', 'C:/userdata/archive.txt')).toBe(true)
})
@@ -162,7 +165,9 @@ describe('audio: -x --audio-format <fmt>', () => {
})
it('maps the quality label to an --audio-quality value', () => {
expect(hasSeq(build(audio({ quality: 'Best (MP3)' }), dlo()), '--audio-quality', '0')).toBe(true)
expect(hasSeq(build(audio({ quality: 'Best (MP3)' }), dlo()), '--audio-quality', '0')).toBe(
true
)
expect(hasSeq(build(audio({ quality: '320 kbps' }), dlo()), '--audio-quality', '320K')).toBe(
true
)
@@ -247,18 +252,18 @@ describe('sponsorBlock', () => {
describe('preferredVideoCodec → -S res,fps,vcodec:<token>', () => {
it('maps av1 to the av01 codec token', () => {
expect(hasSeq(build(opts(), dlo({ preferredVideoCodec: 'av1' })), '-S', 'res,fps,vcodec:av01')).toBe(
true
)
expect(
hasSeq(build(opts(), dlo({ preferredVideoCodec: 'av1' })), '-S', 'res,fps,vcodec:av01')
).toBe(true)
})
it('passes h264 / vp9 through verbatim', () => {
expect(hasSeq(build(opts(), dlo({ preferredVideoCodec: 'h264' })), '-S', 'res,fps,vcodec:h264')).toBe(
true
)
expect(hasSeq(build(opts(), dlo({ preferredVideoCodec: 'vp9' })), '-S', 'res,fps,vcodec:vp9')).toBe(
true
)
expect(
hasSeq(build(opts(), dlo({ preferredVideoCodec: 'h264' })), '-S', 'res,fps,vcodec:h264')
).toBe(true)
expect(
hasSeq(build(opts(), dlo({ preferredVideoCodec: 'vp9' })), '-S', 'res,fps,vcodec:vp9')
).toBe(true)
})
it('omits -S entirely when codec preference is "any"', () => {
@@ -468,9 +473,9 @@ describe('YouTube extractor-args', () => {
youtubePlayerClient: 'web_safari',
youtubePoToken: 'web.gvs+ABC'
})
expect(hasSeq(argv, '--extractor-args', 'youtube:player_client=web_safari;po_token=web.gvs+ABC')).toBe(
true
)
expect(
hasSeq(argv, '--extractor-args', 'youtube:player_client=web_safari;po_token=web.gvs+ABC')
).toBe(true)
})
it('emits only the fields that are set', () => {
@@ -485,7 +490,10 @@ describe('YouTube extractor-args', () => {
describe('format sorting (-S)', () => {
it('emits a raw -S string when formatSort is set, overriding the codec tiebreaker', () => {
const argv = build(opts(), dlo({ formatSort: 'res:1080,vcodec:av01', preferredVideoCodec: 'h264' }))
const argv = build(
opts(),
dlo({ formatSort: 'res:1080,vcodec:av01', preferredVideoCodec: 'h264' })
)
expect(hasSeq(argv, '-S', 'res:1080,vcodec:av01')).toBe(true)
expect(hasSeq(argv, '-S', 'res,fps,vcodec:h264')).toBe(false)
})
@@ -578,6 +586,15 @@ describe('parseTrimSections', () => {
expect(parseTrimSections('')).toEqual([])
expect(parseTrimSections(undefined)).toEqual([])
})
it('rejects times with more than two colon groups (L146)', () => {
// H:MM:SS is the deepest valid form; a fourth component is nonsense and used
// to slip through to yt-dlp, failing there instead of being rejected here.
expect(parseTrimSections('1:2:3:4-5:6:7:8')).toEqual([])
expect(parseTrimSections('0:00:00:01-0:00:00:02')).toEqual([])
// The deepest valid form still passes.
expect(parseTrimSections('1:02:03-1:02:04')).toEqual(['*1:02:03-1:02:04'])
})
})
describe('trim (--download-sections)', () => {
@@ -655,12 +672,12 @@ describe('collectionOutputTemplate', () => {
})
it('zero-pads the index to three digits and clamps bad values to 001', () => {
expect(collectionOutputTemplate('C:/o', { ...ctx, index: 42 }, 'f').replace(/\\/g, '/')).toContain(
'/042 - f'
)
expect(collectionOutputTemplate('C:/o', { ...ctx, index: 0 }, 'f').replace(/\\/g, '/')).toContain(
'/001 - f'
)
expect(
collectionOutputTemplate('C:/o', { ...ctx, index: 42 }, 'f').replace(/\\/g, '/')
).toContain('/042 - f')
expect(
collectionOutputTemplate('C:/o', { ...ctx, index: 0 }, 'f').replace(/\\/g, '/')
).toContain('/001 - f')
expect(
collectionOutputTemplate('C:/o', { ...ctx, index: NaN }, 'f').replace(/\\/g, '/')
).toContain('/001 - f')
@@ -675,3 +692,43 @@ describe('collectionOutputTemplate', () => {
expect(t.replace(/\\/g, '/')).toBe('C:/out/A B/Untitled/001 - %(title)s.%(ext)s')
})
})
describe('metadata overrides (--replace-in-metadata)', () => {
it('emits --embed-metadata and --replace-in-metadata for each set field', () => {
const argv = build(opts(), dlo({ embedMetadata: false, metadataTitle: 'My Track' }))
expect(argv).toContain('--embed-metadata')
expect(hasSeq(argv, '--replace-in-metadata', 'title', '^.*$', 'My Track')).toBe(true)
})
it('emits all three override fields when all are set', () => {
const argv = build(opts(), dlo({ metadataTitle: 'T', metadataArtist: 'A', metadataAlbum: 'B' }))
expect(hasSeq(argv, '--replace-in-metadata', 'title', '^.*$', 'T')).toBe(true)
expect(hasSeq(argv, '--replace-in-metadata', 'artist', '^.*$', 'A')).toBe(true)
expect(hasSeq(argv, '--replace-in-metadata', 'album', '^.*$', 'B')).toBe(true)
})
it('passes a value containing a colon as a single arg without splitting', () => {
const argv = build(opts(), dlo({ metadataTitle: 'Foo: Bar' }))
expect(hasSeq(argv, '--replace-in-metadata', 'title', '^.*$', 'Foo: Bar')).toBe(true)
})
it('escapes backslashes in the replacement string', () => {
const argv = build(opts(), dlo({ metadataTitle: 'A\\B' }))
expect(hasSeq(argv, '--replace-in-metadata', 'title', '^.*$', 'A\\\\B')).toBe(true)
})
it('skips blank or whitespace-only values', () => {
const argv = build(opts(), dlo({ metadataTitle: '', metadataArtist: ' ' }))
expect(argv).not.toContain('--replace-in-metadata')
})
it('forces --embed-metadata even when embedMetadata is false and an override is set', () => {
const argv = build(opts(), dlo({ embedMetadata: false, metadataAlbum: 'Soundtrack' }))
expect(argv).toContain('--embed-metadata')
})
it('emits no override flags when no fields are set', () => {
const argv = build(opts(), dlo({ embedMetadata: true }))
expect(argv).not.toContain('--replace-in-metadata')
})
})
+43
View File
@@ -0,0 +1,43 @@
import { describe, it, expect, vi, afterEach } from 'vitest'
import { relTime, formatWhen, fmtSchedule } from '../src/renderer/src/datetime'
afterEach(() => {
vi.useRealTimers()
})
describe('relTime', () => {
it('returns "never" for a missing timestamp', () => {
expect(relTime(undefined)).toBe('never')
expect(relTime(0)).toBe('never')
})
it('buckets into just now / minutes / hours / days', () => {
const now = new Date('2026-06-30T12:00:00Z').getTime()
vi.useFakeTimers()
vi.setSystemTime(now)
expect(relTime(now - 20 * 1000)).toBe('just now') // < 1 min
expect(relTime(now - 5 * 60_000)).toBe('5 min ago')
expect(relTime(now - 3 * 3_600_000)).toBe('3 h ago')
expect(relTime(now - 2 * 86_400_000)).toBe('2 d ago')
})
})
describe('formatWhen', () => {
it('labels today and yesterday with the time, older with a date', () => {
const now = new Date('2026-06-30T12:00:00Z').getTime()
vi.useFakeTimers()
vi.setSystemTime(now)
expect(formatWhen(now)).toMatch(/^Today, /)
expect(formatWhen(now - 86_400_000)).toMatch(/^Yesterday, /)
// A clearly-older timestamp falls through to an absolute date (with year).
expect(formatWhen(new Date('2025-01-05T09:00:00Z').getTime())).toMatch(/2025/)
})
})
describe('fmtSchedule', () => {
it('formats a valid timestamp and never throws', () => {
expect(fmtSchedule(new Date('2026-06-30T15:04:00Z').getTime())).toMatch(/2026/)
// Defensive: NaN can't be formatted but must not throw.
expect(typeof fmtSchedule(Number.NaN)).toBe('string')
})
})
+28
View File
@@ -0,0 +1,28 @@
import { describe, it, expect } from 'vitest'
import { newId } from '../src/renderer/src/id'
describe('newId', () => {
it('produces unique ids across many calls', () => {
const ids = new Set(Array.from({ length: 1000 }, () => newId('item')))
expect(ids.size).toBe(1000)
})
it('returns a non-empty string', () => {
expect(newId('tpl')).toBeTruthy()
expect(typeof newId('t')).toBe('string')
})
it('falls back to a prefixed, collision-free id when crypto.randomUUID is absent', () => {
const orig = globalThis.crypto
// Simulate a host without randomUUID; the counter suffix must keep same-ms ids distinct.
Object.defineProperty(globalThis, 'crypto', { value: {}, configurable: true })
try {
const a = newId('item')
const b = newId('item')
expect(a).toMatch(/^item-/)
expect(a).not.toBe(b)
} finally {
Object.defineProperty(globalThis, 'crypto', { value: orig, configurable: true })
}
})
})
+60
View File
@@ -0,0 +1,60 @@
import { describe, it, expect, afterEach } from 'vitest'
import { mkdtempSync, rmSync, writeFileSync, readdirSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { readJsonArraySafe, writeJsonAtomic } from '../src/main/jsonStore'
interface Row {
id: string
}
const isRow = (o: unknown): o is Row =>
typeof o === 'object' && o !== null && typeof (o as Row).id === 'string'
describe('jsonStore atomic write + safe read', () => {
const dirs: string[] = []
function tmp(): string {
const d = mkdtempSync(join(tmpdir(), 'aerofetch-jsonstore-'))
dirs.push(d)
return d
}
afterEach(() => {
for (const d of dirs.splice(0)) {
try {
rmSync(d, { recursive: true, force: true })
} catch {
/* ignore */
}
}
})
it('round-trips a valid array (R1 write → read)', () => {
const file = join(tmp(), 'data.json')
writeJsonAtomic(file, [{ id: 'a' }, { id: 'b' }])
expect(readJsonArraySafe(file, isRow)).toEqual([{ id: 'a' }, { id: 'b' }])
})
it('leaves no .tmp file behind (atomic temp+rename)', () => {
const d = tmp()
writeJsonAtomic(join(d, 'data.json'), [{ id: 'a' }])
expect(readdirSync(d)).toEqual(['data.json'])
})
it('returns [] for a missing file (first run)', () => {
expect(readJsonArraySafe(join(tmp(), 'nope.json'), isRow)).toEqual([])
})
it('drops rows that fail validation', () => {
const file = join(tmp(), 'data.json')
writeFileSync(file, JSON.stringify([{ id: 'a' }, { nope: 1 }, { id: 'c' }]))
expect(readJsonArraySafe(file, isRow)).toEqual([{ id: 'a' }, { id: 'c' }])
})
it('backs up a corrupt file instead of silently wiping it (R2)', () => {
const d = tmp()
const file = join(d, 'data.json')
writeFileSync(file, '{ truncated, not valid json')
expect(readJsonArraySafe(file, isRow)).toEqual([])
const backups = readdirSync(d).filter((f) => f.startsWith('data.json.corrupt-'))
expect(backups.length).toBe(1)
})
})
+1 -4
View File
@@ -73,10 +73,7 @@ describe('sameVideo', () => {
it('matches YouTube links by video id across watch/youtu.be/extra params', () => {
expect(
sameVideo(
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
'https://youtu.be/dQw4w9WgXcQ'
)
sameVideo('https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'https://youtu.be/dQw4w9WgXcQ')
).toBe(true)
expect(
sameVideo(
+255 -244
View File
@@ -72,10 +72,14 @@ function parseCoverDims(info: string): { w: number; h: number } | undefined {
}
function probeSourceDuration(url: string): number | undefined {
const r = spawnSync(YTDLP, ['--no-warnings', '--no-playlist', '--print', '%(duration)s', '--', url], {
encoding: 'utf8',
windowsHide: true
})
const r = spawnSync(
YTDLP,
['--no-warnings', '--no-playlist', '--print', '%(duration)s', '--', url],
{
encoding: 'utf8',
windowsHide: true
}
)
const n = Number((r.stdout || '').trim())
return Number.isFinite(n) ? n : undefined
}
@@ -95,7 +99,16 @@ function ffprobeJson(file: string): {
} {
const r = spawnSync(
FFPROBE,
['-v', 'quiet', '-print_format', 'json', '-show_format', '-show_streams', '-show_chapters', file],
[
'-v',
'quiet',
'-print_format',
'json',
'-show_format',
'-show_streams',
'-show_chapters',
file
],
{ encoding: 'utf8', windowsHide: true, maxBuffer: 32 * 1024 * 1024 }
)
try {
@@ -128,262 +141,260 @@ afterAll(() => {
})
describe.skipIf(!RUN)('real yt-dlp downloads (buildArgs end-to-end)', () => {
it(
'cropThumbnail: audio mp3 gets a SQUARE embedded cover (--ppa crop survives ffmpeg)',
() => {
const opts: StartDownloadOptions = {
id: 'crop',
url: 'https://www.youtube.com/watch?v=jNQXAC9IVRw', // "Me at the zoo", 19s
kind: 'audio',
quality: 'Best (MP3)'
}
const options = {
...DEFAULT_DOWNLOAD_OPTIONS,
audioFormat: 'mp3' as const,
embedThumbnail: true,
cropThumbnail: true
}
const argv = buildArgs(opts, join(outDir, '%(id)s.%(ext)s'), options, BIN_DIR, NO_ACCESS)
console.log('[crop] argv:', JSON.stringify(argv))
it('cropThumbnail: audio mp3 gets a SQUARE embedded cover (--ppa crop survives ffmpeg)', () => {
const opts: StartDownloadOptions = {
id: 'crop',
url: 'https://www.youtube.com/watch?v=jNQXAC9IVRw', // "Me at the zoo", 19s
kind: 'audio',
quality: 'Best (MP3)'
}
const options = {
...DEFAULT_DOWNLOAD_OPTIONS,
audioFormat: 'mp3' as const,
embedThumbnail: true,
cropThumbnail: true
}
const argv = buildArgs(opts, join(outDir, '%(id)s.%(ext)s'), options, BIN_DIR, NO_ACCESS)
console.log('[crop] argv:', JSON.stringify(argv))
const res = runYtdlp(argv)
if (res.status !== 0) console.error('[crop] stderr:\n' + res.stderr)
expect(res.status, 'yt-dlp should exit 0').toBe(0)
expect(res.filePath, 'after_move path should be printed').toBeTruthy()
expect(existsSync(res.filePath!)).toBe(true)
expect(statSync(res.filePath!).size).toBeGreaterThan(1000)
const res = runYtdlp(argv)
if (res.status !== 0) console.error('[crop] stderr:\n' + res.stderr)
expect(res.status, 'yt-dlp should exit 0').toBe(0)
expect(res.filePath, 'after_move path should be printed').toBeTruthy()
expect(existsSync(res.filePath!)).toBe(true)
expect(statSync(res.filePath!).size).toBeGreaterThan(1000)
const info = ffinfo(res.filePath!)
const dims = parseCoverDims(info)
console.log('[crop] output:', res.filePath, 'cover dims:', dims)
expect(dims, 'embedded cover image should be present').toBeTruthy()
expect(dims!.w, 'cover must be cropped to a square').toBe(dims!.h)
},
240_000
)
const info = ffinfo(res.filePath!)
const dims = parseCoverDims(info)
console.log('[crop] output:', res.filePath, 'cover dims:', dims)
expect(dims, 'embedded cover image should be present').toBeTruthy()
expect(dims!.w, 'cover must be cropped to a square').toBe(dims!.h)
}, 240_000)
it(
'sponsorBlock remove: output is shorter than source by the removed segments',
() => {
const url = 'https://www.youtube.com/watch?v=kJQP7kiw5Fk' // has ~54s of music_offtopic
const source = probeSourceDuration(url)
console.log('[sb] source duration:', source)
expect(source, 'should read source duration').toBeGreaterThan(60)
it('sponsorBlock remove: output is shorter than source by the removed segments', () => {
const url = 'https://www.youtube.com/watch?v=kJQP7kiw5Fk' // has ~54s of music_offtopic
const source = probeSourceDuration(url)
console.log('[sb] source duration:', source)
expect(source, 'should read source duration').toBeGreaterThan(60)
const opts: StartDownloadOptions = {
id: 'sb',
url,
kind: 'audio',
quality: 'Best (MP3)'
}
const options = {
...DEFAULT_DOWNLOAD_OPTIONS,
audioFormat: 'mp3' as const,
embedThumbnail: false,
sponsorBlock: true,
sponsorBlockMode: 'remove' as const,
sponsorBlockCategories: ['music_offtopic' as const]
}
const argv = buildArgs(opts, join(outDir, '%(id)s.%(ext)s'), options, BIN_DIR, NO_ACCESS)
console.log('[sb] argv:', JSON.stringify(argv))
const opts: StartDownloadOptions = {
id: 'sb',
url,
kind: 'audio',
quality: 'Best (MP3)'
}
const options = {
...DEFAULT_DOWNLOAD_OPTIONS,
audioFormat: 'mp3' as const,
embedThumbnail: false,
sponsorBlock: true,
sponsorBlockMode: 'remove' as const,
sponsorBlockCategories: ['music_offtopic' as const]
}
const argv = buildArgs(opts, join(outDir, '%(id)s.%(ext)s'), options, BIN_DIR, NO_ACCESS)
console.log('[sb] argv:', JSON.stringify(argv))
const res = runYtdlp(argv)
if (res.status !== 0) console.error('[sb] stderr:\n' + res.stderr)
expect(res.status, 'yt-dlp should exit 0').toBe(0)
expect(res.filePath).toBeTruthy()
expect(existsSync(res.filePath!)).toBe(true)
const res = runYtdlp(argv)
if (res.status !== 0) console.error('[sb] stderr:\n' + res.stderr)
expect(res.status, 'yt-dlp should exit 0').toBe(0)
expect(res.filePath).toBeTruthy()
expect(existsSync(res.filePath!)).toBe(true)
const outDur = parseDurationSec(ffinfo(res.filePath!))
console.log('[sb] output duration:', outDur, 'removed ≈', (source ?? 0) - (outDur ?? 0), 's')
expect(outDur, 'should read output duration').toBeGreaterThan(0)
// ~54s of segments are removed; require a clear, unambiguous shrink.
expect(source! - outDur!).toBeGreaterThan(30)
},
240_000
)
const outDur = parseDurationSec(ffinfo(res.filePath!))
console.log('[sb] output duration:', outDur, 'removed ≈', (source ?? 0) - (outDur ?? 0), 's')
expect(outDur, 'should read output duration').toBeGreaterThan(0)
// ~54s of segments are removed; require a clear, unambiguous shrink.
expect(source! - outDur!).toBeGreaterThan(30)
}, 240_000)
it(
'audioFormat opus: --audio-format re-encodes to a real .opus stream',
() => {
const opts: StartDownloadOptions = {
id: 'opus',
url: 'https://www.youtube.com/watch?v=jNQXAC9IVRw',
kind: 'audio',
quality: 'Best'
}
const options = {
...DEFAULT_DOWNLOAD_OPTIONS,
audioFormat: 'opus' as const,
embedThumbnail: false
}
const argv = buildArgs(opts, join(outDir, '%(id)s.%(ext)s'), options, BIN_DIR, NO_ACCESS)
const res = runYtdlp(argv)
if (res.status !== 0) console.error('[opus] stderr:\n' + res.stderr)
expect(res.status, 'yt-dlp should exit 0').toBe(0)
expect(res.filePath, 'after_move path should be printed').toBeTruthy()
expect(res.filePath!.toLowerCase().endsWith('.opus'), 'output ext should be .opus').toBe(true)
it('audioFormat opus: --audio-format re-encodes to a real .opus stream', () => {
const opts: StartDownloadOptions = {
id: 'opus',
url: 'https://www.youtube.com/watch?v=jNQXAC9IVRw',
kind: 'audio',
quality: 'Best'
}
const options = {
...DEFAULT_DOWNLOAD_OPTIONS,
audioFormat: 'opus' as const,
embedThumbnail: false
}
const argv = buildArgs(opts, join(outDir, '%(id)s.%(ext)s'), options, BIN_DIR, NO_ACCESS)
const res = runYtdlp(argv)
if (res.status !== 0) console.error('[opus] stderr:\n' + res.stderr)
expect(res.status, 'yt-dlp should exit 0').toBe(0)
expect(res.filePath, 'after_move path should be printed').toBeTruthy()
expect(res.filePath!.toLowerCase().endsWith('.opus'), 'output ext should be .opus').toBe(true)
const audio = streamsOfType(res.filePath!, 'audio')
console.log('[opus] audio codecs:', audio.map((s) => s.codec_name))
expect(audio.length, 'should have one audio stream').toBeGreaterThan(0)
expect(audio[0].codec_name, '--audio-format opus should produce opus').toBe('opus')
},
240_000
)
const audio = streamsOfType(res.filePath!, 'audio')
console.log(
'[opus] audio codecs:',
audio.map((s) => s.codec_name)
)
expect(audio.length, 'should have one audio stream').toBeGreaterThan(0)
expect(audio[0].codec_name, '--audio-format opus should produce opus').toBe('opus')
}, 240_000)
it(
'video mkv + vp9 preference: merged .mkv carries a vp9 video stream + audio',
() => {
const opts: StartDownloadOptions = {
id: 'mkv-vp9',
url: 'https://www.youtube.com/watch?v=kJQP7kiw5Fk', // serves both vp9 + av01 at 360p
kind: 'video',
quality: '360p'
}
const options = {
...DEFAULT_DOWNLOAD_OPTIONS,
videoContainer: 'mkv' as const,
preferredVideoCodec: 'vp9' as const,
embedThumbnail: false
}
const argv = buildArgs(opts, join(outDir, '%(id)s.%(ext)s'), options, BIN_DIR, NO_ACCESS)
console.log('[mkv-vp9] argv:', JSON.stringify(argv))
const res = runYtdlp(argv)
if (res.status !== 0) console.error('[mkv-vp9] stderr:\n' + res.stderr)
expect(res.status, 'yt-dlp should exit 0').toBe(0)
expect(res.filePath, 'after_move path should be printed').toBeTruthy()
expect(res.filePath!.toLowerCase().endsWith('.mkv'), 'output ext should be .mkv').toBe(true)
it('video mkv + vp9 preference: merged .mkv carries a vp9 video stream + audio', () => {
const opts: StartDownloadOptions = {
id: 'mkv-vp9',
url: 'https://www.youtube.com/watch?v=kJQP7kiw5Fk', // serves both vp9 + av01 at 360p
kind: 'video',
quality: '360p'
}
const options = {
...DEFAULT_DOWNLOAD_OPTIONS,
videoContainer: 'mkv' as const,
preferredVideoCodec: 'vp9' as const,
embedThumbnail: false
}
const argv = buildArgs(opts, join(outDir, '%(id)s.%(ext)s'), options, BIN_DIR, NO_ACCESS)
console.log('[mkv-vp9] argv:', JSON.stringify(argv))
const res = runYtdlp(argv)
if (res.status !== 0) console.error('[mkv-vp9] stderr:\n' + res.stderr)
expect(res.status, 'yt-dlp should exit 0').toBe(0)
expect(res.filePath, 'after_move path should be printed').toBeTruthy()
expect(res.filePath!.toLowerCase().endsWith('.mkv'), 'output ext should be .mkv').toBe(true)
const video = streamsOfType(res.filePath!, 'video')
const audio = streamsOfType(res.filePath!, 'audio')
console.log('[mkv-vp9] video:', video.map((s) => s.codec_name), 'audio:', audio.map((s) => s.codec_name))
expect(video.length, 'should have a video stream').toBeGreaterThan(0)
expect(audio.length, 'merge should pull in a separate audio stream').toBeGreaterThan(0)
expect(video[0].codec_name, 'the vcodec sort should steer to vp9').toBe('vp9')
},
240_000
)
const video = streamsOfType(res.filePath!, 'video')
const audio = streamsOfType(res.filePath!, 'audio')
console.log(
'[mkv-vp9] video:',
video.map((s) => s.codec_name),
'audio:',
audio.map((s) => s.codec_name)
)
expect(video.length, 'should have a video stream').toBeGreaterThan(0)
expect(audio.length, 'merge should pull in a separate audio stream').toBeGreaterThan(0)
expect(video[0].codec_name, 'the vcodec sort should steer to vp9').toBe('vp9')
}, 240_000)
it(
'embed auto-subtitles + chapters (mp4): mov_text subtitle stream and all 3 chapters',
() => {
const opts: StartDownloadOptions = {
id: 'subs-ch',
url: 'https://www.youtube.com/watch?v=jNQXAC9IVRw', // has en auto-captions + 3 chapters
kind: 'video',
quality: '360p'
}
const options = {
...DEFAULT_DOWNLOAD_OPTIONS,
videoContainer: 'mp4' as const,
embedSubtitles: true,
autoSubtitles: true,
subtitleLanguages: 'en',
embedChapters: true,
embedThumbnail: false
}
const argv = buildArgs(opts, join(outDir, '%(id)s.%(ext)s'), options, BIN_DIR, NO_ACCESS)
console.log('[subs-ch] argv:', JSON.stringify(argv))
const res = runYtdlp(argv)
if (res.status !== 0) console.error('[subs-ch] stderr:\n' + res.stderr)
expect(res.status, 'yt-dlp should exit 0').toBe(0)
expect(res.filePath, 'after_move path should be printed').toBeTruthy()
it('embed auto-subtitles + chapters (mp4): mov_text subtitle stream and all 3 chapters', () => {
const opts: StartDownloadOptions = {
id: 'subs-ch',
url: 'https://www.youtube.com/watch?v=jNQXAC9IVRw', // has en auto-captions + 3 chapters
kind: 'video',
quality: '360p'
}
const options = {
...DEFAULT_DOWNLOAD_OPTIONS,
videoContainer: 'mp4' as const,
embedSubtitles: true,
autoSubtitles: true,
subtitleLanguages: 'en',
embedChapters: true,
embedThumbnail: false
}
const argv = buildArgs(opts, join(outDir, '%(id)s.%(ext)s'), options, BIN_DIR, NO_ACCESS)
console.log('[subs-ch] argv:', JSON.stringify(argv))
const res = runYtdlp(argv)
if (res.status !== 0) console.error('[subs-ch] stderr:\n' + res.stderr)
expect(res.status, 'yt-dlp should exit 0').toBe(0)
expect(res.filePath, 'after_move path should be printed').toBeTruthy()
const info = ffprobeJson(res.filePath!)
const subs = (info.streams ?? []).filter((s) => s.codec_type === 'subtitle')
const chapters = info.chapters ?? []
console.log('[subs-ch] subtitles:', subs.map((s) => s.codec_name), 'chapters:', chapters.length)
expect(subs.length, 'an en auto-caption should be embedded').toBeGreaterThan(0)
expect(subs[0].codec_name, 'mp4 subtitles convert to mov_text').toBe('mov_text')
expect(chapters.length, 'all 3 source chapters should be embedded').toBe(3)
},
240_000
)
const info = ffprobeJson(res.filePath!)
const subs = (info.streams ?? []).filter((s) => s.codec_type === 'subtitle')
const chapters = info.chapters ?? []
console.log(
'[subs-ch] subtitles:',
subs.map((s) => s.codec_name),
'chapters:',
chapters.length
)
expect(subs.length, 'an en auto-caption should be embedded').toBeGreaterThan(0)
expect(subs[0].codec_name, 'mp4 subtitles convert to mov_text').toBe('mov_text')
expect(chapters.length, 'all 3 source chapters should be embedded').toBe(3)
}, 240_000)
it(
'restrictFilenames: output basename is sanitized to a portable ASCII subset',
() => {
const opts: StartDownloadOptions = {
id: 'restrict',
url: 'https://www.youtube.com/watch?v=jNQXAC9IVRw', // title "Me at the zoo"
kind: 'audio',
quality: 'Best'
}
const options = { ...DEFAULT_DOWNLOAD_OPTIONS, audioFormat: 'mp3' as const, embedThumbnail: false }
const access = { ...NO_ACCESS, restrictFilenames: true }
// %(title)s so the spaces in "Me at the zoo" actually exercise the sanitizer.
const argv = buildArgs(opts, join(outDir, '%(title)s.%(ext)s'), options, BIN_DIR, access)
const res = runYtdlp(argv)
if (res.status !== 0) console.error('[restrict] stderr:\n' + res.stderr)
expect(res.status, 'yt-dlp should exit 0').toBe(0)
expect(res.filePath, 'after_move path should be printed').toBeTruthy()
it('restrictFilenames: output basename is sanitized to a portable ASCII subset', () => {
const opts: StartDownloadOptions = {
id: 'restrict',
url: 'https://www.youtube.com/watch?v=jNQXAC9IVRw', // title "Me at the zoo"
kind: 'audio',
quality: 'Best'
}
const options = {
...DEFAULT_DOWNLOAD_OPTIONS,
audioFormat: 'mp3' as const,
embedThumbnail: false
}
const access = { ...NO_ACCESS, restrictFilenames: true }
// %(title)s so the spaces in "Me at the zoo" actually exercise the sanitizer.
const argv = buildArgs(opts, join(outDir, '%(title)s.%(ext)s'), options, BIN_DIR, access)
const res = runYtdlp(argv)
if (res.status !== 0) console.error('[restrict] stderr:\n' + res.stderr)
expect(res.status, 'yt-dlp should exit 0').toBe(0)
expect(res.filePath, 'after_move path should be printed').toBeTruthy()
const base = baseName(res.filePath!)
console.log('[restrict] basename:', base)
expect(base, 'a restricted filename has no spaces').not.toMatch(/\s/)
expect(base, 'restricted to [A-Za-z0-9._-]').toMatch(/^[A-Za-z0-9._-]+$/)
},
240_000
)
const base = baseName(res.filePath!)
console.log('[restrict] basename:', base)
expect(base, 'a restricted filename has no spaces').not.toMatch(/\s/)
expect(base, 'restricted to [A-Za-z0-9._-]').toMatch(/^[A-Za-z0-9._-]+$/)
}, 240_000)
it(
'downloadArchive: a second run of the same URL is skipped via the archive',
() => {
const archive = join(outDir, 'archive.txt')
const mk = (id: string): string[] =>
buildArgs(
{ id, url: 'https://www.youtube.com/watch?v=jNQXAC9IVRw', kind: 'audio', quality: 'Best' },
join(outDir, 'arch-%(id)s.%(ext)s'),
{ ...DEFAULT_DOWNLOAD_OPTIONS, audioFormat: 'mp3' as const, embedThumbnail: false },
BIN_DIR,
{ ...NO_ACCESS, downloadArchivePath: archive }
)
it('downloadArchive: a second run of the same URL is skipped via the archive', () => {
const archive = join(outDir, 'archive.txt')
const mk = (id: string): string[] =>
buildArgs(
{ id, url: 'https://www.youtube.com/watch?v=jNQXAC9IVRw', kind: 'audio', quality: 'Best' },
join(outDir, 'arch-%(id)s.%(ext)s'),
{ ...DEFAULT_DOWNLOAD_OPTIONS, audioFormat: 'mp3' as const, embedThumbnail: false },
BIN_DIR,
{ ...NO_ACCESS, downloadArchivePath: archive }
)
const first = runYtdlp(mk('a1'))
if (first.status !== 0) console.error('[archive] first stderr:\n' + first.stderr)
expect(first.status, 'first run should download').toBe(0)
expect(first.filePath, 'first run should produce a file').toBeTruthy()
expect(existsSync(archive), 'archive file should be written').toBe(true)
const first = runYtdlp(mk('a1'))
if (first.status !== 0) console.error('[archive] first stderr:\n' + first.stderr)
expect(first.status, 'first run should download').toBe(0)
expect(first.filePath, 'first run should produce a file').toBeTruthy()
expect(existsSync(archive), 'archive file should be written').toBe(true)
const second = runYtdlp(mk('a2'))
expect(second.status, 'second run should still exit 0').toBe(0)
// The id is in the archive, so the second run downloads nothing — and thus
// prints no after_move `path|` line. (yt-dlp's "has already been recorded"
// notice is itself silenced because buildArgs' --print implies --quiet, so
// we assert on the archive contents + the absence of a fresh download.)
expect(second.filePath, 'second run should be skipped (no new download)').toBeFalsy()
const archiveBody = readFileSync(archive, 'utf8')
console.log('[archive] archive.txt:', JSON.stringify(archiveBody.trim()))
expect(archiveBody, 'the archive should record the downloaded video id').toContain('jNQXAC9IVRw')
},
240_000
)
const second = runYtdlp(mk('a2'))
expect(second.status, 'second run should still exit 0').toBe(0)
// The id is in the archive, so the second run downloads nothing — and thus
// prints no after_move `path|` line. (yt-dlp's "has already been recorded"
// notice is itself silenced because buildArgs' --print implies --quiet, so
// we assert on the archive contents + the absence of a fresh download.)
expect(second.filePath, 'second run should be skipped (no new download)').toBeFalsy()
const archiveBody = readFileSync(archive, 'utf8')
console.log('[archive] archive.txt:', JSON.stringify(archiveBody.trim()))
expect(archiveBody, 'the archive should record the downloaded video id').toContain(
'jNQXAC9IVRw'
)
}, 240_000)
it(
'extraArgs (custom command): parseExtraArgs tokens reach yt-dlp (--write-info-json sidecar)',
() => {
const opts: StartDownloadOptions = {
id: 'extra',
url: 'https://www.youtube.com/watch?v=jNQXAC9IVRw',
kind: 'audio',
quality: 'Best'
}
const options = { ...DEFAULT_DOWNLOAD_OPTIONS, audioFormat: 'mp3' as const, embedThumbnail: false }
const extra = parseExtraArgs('--write-info-json --no-mtime')
expect(extra, 'parseExtraArgs splits into discrete tokens').toEqual([
'--write-info-json',
'--no-mtime'
])
const argv = buildArgs(opts, join(outDir, 'extra-%(id)s.%(ext)s'), options, BIN_DIR, NO_ACCESS, extra)
const res = runYtdlp(argv)
if (res.status !== 0) console.error('[extra] stderr:\n' + res.stderr)
expect(res.status, 'yt-dlp should exit 0').toBe(0)
expect(res.filePath, 'after_move path should be printed').toBeTruthy()
it('extraArgs (custom command): parseExtraArgs tokens reach yt-dlp (--write-info-json sidecar)', () => {
const opts: StartDownloadOptions = {
id: 'extra',
url: 'https://www.youtube.com/watch?v=jNQXAC9IVRw',
kind: 'audio',
quality: 'Best'
}
const options = {
...DEFAULT_DOWNLOAD_OPTIONS,
audioFormat: 'mp3' as const,
embedThumbnail: false
}
const extra = parseExtraArgs('--write-info-json --no-mtime')
expect(extra, 'parseExtraArgs splits into discrete tokens').toEqual([
'--write-info-json',
'--no-mtime'
])
const argv = buildArgs(
opts,
join(outDir, 'extra-%(id)s.%(ext)s'),
options,
BIN_DIR,
NO_ACCESS,
extra
)
const res = runYtdlp(argv)
if (res.status !== 0) console.error('[extra] stderr:\n' + res.stderr)
expect(res.status, 'yt-dlp should exit 0').toBe(0)
expect(res.filePath, 'after_move path should be printed').toBeTruthy()
const infoJson = join(outDir, 'extra-jNQXAC9IVRw.info.json')
console.log('[extra] info.json exists:', existsSync(infoJson))
expect(existsSync(infoJson), '--write-info-json should write a sidecar').toBe(true)
},
240_000
)
const infoJson = join(outDir, 'extra-jNQXAC9IVRw.info.json')
console.log('[extra] info.json exists:', existsSync(infoJson))
expect(existsSync(infoJson), '--write-info-json should write a sidecar').toBe(true)
}, 240_000)
})
+28 -4
View File
@@ -85,7 +85,9 @@ describe('isTrustedDownloadUrl (app-updater host pin)', () => {
it('rejects the right hostname on the wrong port', () => {
// host comparison includes the port, so :443 (default) is not the same origin
expect(isTrustedDownloadUrl('https://gitea.netbird.zimspace.uk/AeroFetch-Setup.exe')).toBe(false)
expect(isTrustedDownloadUrl('https://gitea.netbird.zimspace.uk/AeroFetch-Setup.exe')).toBe(
false
)
})
it('rejects plain http even on the update host', () => {
@@ -93,9 +95,9 @@ describe('isTrustedDownloadUrl (app-updater host pin)', () => {
})
it('is not fooled by the trusted host placed in the userinfo', () => {
expect(
isTrustedDownloadUrl('https://gitea.netbird.zimspace.uk:5938@evil.example/x.exe')
).toBe(false)
expect(isTrustedDownloadUrl('https://gitea.netbird.zimspace.uk:5938@evil.example/x.exe')).toBe(
false
)
})
it('rejects unparseable input', () => {
@@ -130,6 +132,19 @@ describe('extractSha256 (app-updater checksum parsing)', () => {
it('does not slice a 64-char window out of a longer hex run (e.g. sha512)', () => {
expect(extractSha256('a'.repeat(128))).toBeNull()
})
it('matches the hash on the line naming the asset in a combined file (B3)', () => {
const other = 'b'.repeat(64)
const combined = `${other} OtherApp.exe\n${hash} *AeroFetch-Setup-0.5.0.exe\n`
// Without the filename it would pick the FIRST hash (the wrong one); with the
// asset name it must select the line that actually names the installer.
expect(extractSha256(combined)).toBe(other)
expect(extractSha256(combined, 'AeroFetch-Setup-0.5.0.exe')).toBe(hash)
})
it('falls back to the only digest when a bare file omits the filename', () => {
expect(extractSha256(hash, 'AeroFetch-Setup-0.5.0.exe')).toBe(hash)
})
})
// --- app-updater: compareVersions — prerelease never outranks its release ----
@@ -149,6 +164,15 @@ describe('compareVersions (app-updater version ordering)', () => {
// 0.5.0-rc.1 must not be offered as an "upgrade" over a running 0.5.0
expect(compareVersions('0.5.0-rc.1', '0.5.0')).toBe(0)
})
it('treats missing trailing components as zero (L36)', () => {
// Differing component counts: the shorter version pads with 0s, so 1.2 == 1.2.0
// and 1.2 < 1.2.1, rather than mis-ordering on length.
expect(compareVersions('1.2', '1.2.0')).toBe(0)
expect(compareVersions('1.2', '1.2.1')).toBe(-1)
expect(compareVersions('1.2.1', '1.2')).toBe(1)
expect(compareVersions('1', '1.0.0')).toBe(0)
})
})
// --- F1: isYtdlpUpdateChannel — --update-to allowlist -----------------------
+52 -2
View File
@@ -5,9 +5,10 @@ import {
isValidHistoryEntry,
isValidErrorLogEntry,
isTemplateLike,
isValidSource
isValidSource,
isValidMediaItem
} from '../src/main/validation'
import type { HistoryEntry, ErrorLogEntry, Source } from '@shared/ipc'
import type { HistoryEntry, ErrorLogEntry, Source, MediaItem } from '@shared/ipc'
// --- S4: filename template path-traversal -----------------------------------
@@ -208,3 +209,52 @@ describe('isValidSource', () => {
expect(isValidSource('nope')).toBe(false)
})
})
// --- L35: isValidMediaItem — the persisted media-items.json row validator ----
describe('isValidMediaItem', () => {
const valid: MediaItem = {
id: 'src1:vid1',
sourceId: 'src1',
videoId: 'vid1',
title: 'A video',
url: 'https://youtube.com/watch?v=vid1',
playlistTitle: 'Uploads',
playlistIndex: 1,
downloaded: false
}
it('accepts a minimal valid item', () => {
expect(isValidMediaItem(valid)).toBe(true)
})
it('accepts the optional fields when well-typed', () => {
expect(
isValidMediaItem({
...valid,
durationLabel: '3:21',
downloaded: true,
downloadedAt: 1_700_000_000_000,
filePath: 'C:/Videos/a.mp4'
})
).toBe(true)
})
it('rejects a missing or wrong-typed required field', () => {
const { videoId: _v, ...noVideoId } = valid
expect(isValidMediaItem(noVideoId)).toBe(false)
expect(isValidMediaItem({ ...valid, playlistIndex: '1' })).toBe(false)
expect(isValidMediaItem({ ...valid, downloaded: 'no' })).toBe(false)
})
it('rejects wrong-typed optional fields', () => {
expect(isValidMediaItem({ ...valid, durationLabel: 42 })).toBe(false)
expect(isValidMediaItem({ ...valid, downloadedAt: 'soon' })).toBe(false)
expect(isValidMediaItem({ ...valid, filePath: 99 })).toBe(false)
})
it('rejects non-objects', () => {
expect(isValidMediaItem(null)).toBe(false)
expect(isValidMediaItem('nope')).toBe(false)
})
})