d18ada4ee7
Docker Publish / docker (web, apps/web/Dockerfile, web) (push) Failing after 0s
Docker Publish / docker (api, apps/api/Dockerfile, api) (push) Failing after 1s
Publish Extension / detect-version (push) Has been skipped
Publish Extension / submit (push) Has been cancelled
296 lines
9.9 KiB
TypeScript
296 lines
9.9 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { ParseError, parseArgv } from '../src/parser'
|
|
|
|
describe('parseArgv — VidBee flag handling (§4.1, §4.2)', () => {
|
|
it('consumes --vidbee-wait switch and removes it from yt-dlp argv', () => {
|
|
const r = parseArgv(['--vidbee-wait', 'https://x'])
|
|
if (r.kind !== 'ytdlp') throw new Error('expected ytdlp')
|
|
expect(r.flags.wait).toBe(true)
|
|
expect(r.ytArgs).toEqual(['https://x'])
|
|
})
|
|
|
|
it('accepts --vidbee-api with separate value', () => {
|
|
const r = parseArgv(['--vidbee-api', 'http://10.0.0.5:3100', 'https://x'])
|
|
if (r.kind !== 'ytdlp') throw new Error('expected ytdlp')
|
|
expect(r.flags.api).toBe('http://10.0.0.5:3100')
|
|
expect(r.ytArgs).toEqual(['https://x'])
|
|
})
|
|
|
|
it('accepts --vidbee-api=value form', () => {
|
|
const r = parseArgv(['--vidbee-api=http://10.0.0.5:3100', 'https://x'])
|
|
if (r.kind !== 'ytdlp') throw new Error('expected ytdlp')
|
|
expect(r.flags.api).toBe('http://10.0.0.5:3100')
|
|
})
|
|
|
|
it('rejects unknown --vidbee-* with exit 2 (§4.2 typo guard)', () => {
|
|
expect(() => parseArgv(['--vidbe-wait', 'https://x'])).toThrow(ParseError)
|
|
try {
|
|
parseArgv(['--vidbe-wait', 'https://x'])
|
|
} catch (e) {
|
|
expect((e as ParseError).exitCode).toBe(2)
|
|
expect((e as ParseError).code).toBe('UNKNOWN_VIDBEE_FLAG')
|
|
}
|
|
})
|
|
|
|
it('rejects --vidbee-wait=anything (switch with value)', () => {
|
|
expect(() => parseArgv(['--vidbee-wait=true', 'https://x'])).toThrow(
|
|
ParseError
|
|
)
|
|
})
|
|
|
|
it('rejects --vidbee-api with no value', () => {
|
|
expect(() => parseArgv(['--vidbee-api'])).toThrow(ParseError)
|
|
})
|
|
|
|
it('validates --vidbee-target enum', () => {
|
|
expect(() => parseArgv(['--vidbee-target', 'lol', 'https://x'])).toThrow(
|
|
ParseError
|
|
)
|
|
const r = parseArgv(['--vidbee-target', 'local', 'https://x'])
|
|
if (r.kind !== 'ytdlp') throw new Error('expected ytdlp')
|
|
expect(r.flags.target).toBe('local')
|
|
})
|
|
|
|
it('validates --vidbee-priority enum', () => {
|
|
expect(() => parseArgv(['--vidbee-priority', 'critical', 'https://x'])).toThrow(
|
|
ParseError
|
|
)
|
|
const r = parseArgv(['--vidbee-priority', 'subscription', 'https://x'])
|
|
if (r.kind !== 'ytdlp') throw new Error('expected ytdlp')
|
|
expect(r.flags.priority).toBe('subscription')
|
|
})
|
|
|
|
it('validates --vidbee-max-attempts is non-negative integer', () => {
|
|
expect(() => parseArgv(['--vidbee-max-attempts', '-1', 'https://x'])).toThrow(
|
|
ParseError
|
|
)
|
|
expect(() => parseArgv(['--vidbee-max-attempts', '1.5', 'https://x'])).toThrow(
|
|
ParseError
|
|
)
|
|
const r = parseArgv(['--vidbee-max-attempts', '0', 'https://x'])
|
|
if (r.kind !== 'ytdlp') throw new Error('expected ytdlp')
|
|
expect(r.flags.maxAttempts).toBe(0)
|
|
expect(r.flags.noRetry).toBe(true)
|
|
})
|
|
|
|
it('validates --vidbee-timeout is positive integer', () => {
|
|
expect(() => parseArgv(['--vidbee-timeout', '0', 'https://x'])).toThrow(ParseError)
|
|
const r = parseArgv(['--vidbee-timeout', '5000', 'https://x'])
|
|
if (r.kind !== 'ytdlp') throw new Error('expected ytdlp')
|
|
expect(r.flags.timeoutMs).toBe(5000)
|
|
})
|
|
|
|
it('--vidbee-no-retry is equivalent to --vidbee-max-attempts 0 (§6.3)', () => {
|
|
const r = parseArgv(['--vidbee-no-retry', 'https://x'])
|
|
if (r.kind !== 'ytdlp') throw new Error('expected ytdlp')
|
|
expect(r.flags.noRetry).toBe(true)
|
|
expect(r.flags.maxAttempts).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe('parseArgv — yt-dlp passthrough (§4.1)', () => {
|
|
it('preserves order of yt-dlp argv', () => {
|
|
const argv = [
|
|
'-f',
|
|
'bestvideo+bestaudio/best',
|
|
'-o',
|
|
'%(title)s.%(ext)s',
|
|
'https://x'
|
|
]
|
|
const r = parseArgv(argv)
|
|
if (r.kind !== 'ytdlp') throw new Error('expected ytdlp')
|
|
expect(r.ytArgs).toEqual(argv)
|
|
})
|
|
|
|
it('preserves order across interleaved --vidbee-* flags', () => {
|
|
const r = parseArgv([
|
|
'-f',
|
|
'best',
|
|
'--vidbee-wait',
|
|
'-o',
|
|
'./out',
|
|
'https://x',
|
|
'--vidbee-priority',
|
|
'background'
|
|
])
|
|
if (r.kind !== 'ytdlp') throw new Error('expected ytdlp')
|
|
expect(r.ytArgs).toEqual(['-f', 'best', '-o', './out', 'https://x'])
|
|
expect(r.flags.wait).toBe(true)
|
|
expect(r.flags.priority).toBe('background')
|
|
})
|
|
|
|
it('passes through after --', () => {
|
|
const r = parseArgv(['--', '--vidbee-wait', '--anything-goes'])
|
|
if (r.kind !== 'ytdlp') throw new Error('expected ytdlp')
|
|
expect(r.ytArgs).toEqual(['--', '--vidbee-wait', '--anything-goes'])
|
|
})
|
|
|
|
it('treats unknown yt-dlp flag as passthrough (§4.5)', () => {
|
|
const r = parseArgv(['--future-yt-dlp-flag', 'value', 'https://x'])
|
|
if (r.kind !== 'ytdlp') throw new Error('expected ytdlp')
|
|
expect(r.ytArgs).toEqual(['--future-yt-dlp-flag', 'value', 'https://x'])
|
|
})
|
|
})
|
|
|
|
describe('parseArgv — mode detection (§4.3, §4.5)', () => {
|
|
it('detects probe mode from any probe-class flag', () => {
|
|
const r = parseArgv(['-j', 'https://x'])
|
|
if (r.kind !== 'ytdlp') throw new Error('expected ytdlp')
|
|
expect(r.mode).toBe('probe')
|
|
expect(r.probeFlag).toBe('-j')
|
|
})
|
|
|
|
it('defaults to download mode when no probe flag is present', () => {
|
|
const r = parseArgv(['https://x'])
|
|
if (r.kind !== 'ytdlp') throw new Error('expected ytdlp')
|
|
expect(r.mode).toBe('download')
|
|
expect(r.probeFlag).toBe(null)
|
|
})
|
|
|
|
it('rejects -o - in download mode (§4.5)', () => {
|
|
expect(() => parseArgv(['-o', '-', 'https://x'])).toThrow(ParseError)
|
|
expect(() => parseArgv(['--output', '-', 'https://x'])).toThrow(ParseError)
|
|
expect(() => parseArgv(['-o-', 'https://x'])).toThrow(ParseError)
|
|
})
|
|
|
|
it('allows -o - in probe mode (§4.5)', () => {
|
|
const r = parseArgv(['-j', '-o', '-', 'https://x'])
|
|
if (r.kind !== 'ytdlp') throw new Error('expected ytdlp')
|
|
expect(r.mode).toBe('probe')
|
|
expect(r.ytArgs).toEqual(['-j', '-o', '-', 'https://x'])
|
|
})
|
|
})
|
|
|
|
describe('parseArgv — subcommands (§4.1)', () => {
|
|
it('routes :status as subcommand', () => {
|
|
const r = parseArgv([':status'])
|
|
if (r.kind !== 'subcommand') throw new Error('expected subcommand')
|
|
expect(r.subcommand).toBe('status')
|
|
expect(r.subArgs).toEqual([])
|
|
})
|
|
|
|
it('captures all tokens after :subcommand as its args', () => {
|
|
const r = parseArgv([':download', 'list', '--status', 'queued'])
|
|
if (r.kind !== 'subcommand') throw new Error('expected subcommand')
|
|
expect(r.subcommand).toBe('download')
|
|
expect(r.subArgs).toEqual(['list', '--status', 'queued'])
|
|
})
|
|
|
|
it('still consumes --vidbee-* before subcommand', () => {
|
|
const r = parseArgv(['--vidbee-pretty', ':download', 'list'])
|
|
if (r.kind !== 'subcommand') throw new Error('expected subcommand')
|
|
expect(r.flags.pretty).toBe(true)
|
|
expect(r.subcommand).toBe('download')
|
|
expect(r.subArgs).toEqual(['list'])
|
|
})
|
|
|
|
it('a bare ":" (length 1) is treated as yt-dlp argv passthrough', () => {
|
|
const r = parseArgv([':', 'https://x'])
|
|
expect(r.kind).toBe('ytdlp')
|
|
})
|
|
})
|
|
|
|
describe('parseArgv — fuzz / property tests (§4.1)', () => {
|
|
it('preserves yt-dlp argv order across 1000 random argvs', () => {
|
|
const rng = mulberry32(0xdead_beef)
|
|
const ytTokens = [
|
|
'https://example.com/v',
|
|
'-f',
|
|
'best',
|
|
'-o',
|
|
'%(title)s.%(ext)s',
|
|
'--continue',
|
|
'--no-overwrites',
|
|
'--newline',
|
|
'--cookies-from-browser',
|
|
'firefox',
|
|
'-N',
|
|
'4',
|
|
'--retries',
|
|
'10'
|
|
]
|
|
// Only single-token vidbee flags (switches and `--flag=value`) so a
|
|
// value flag can never be split from its value by another insertion.
|
|
const vidbeeTokens: string[][] = [
|
|
['--vidbee-wait'],
|
|
['--vidbee-detach'],
|
|
['--vidbee-priority=subscription'],
|
|
['--vidbee-priority=background'],
|
|
['--vidbee-max-attempts=3'],
|
|
['--vidbee-no-retry'],
|
|
['--vidbee-pretty'],
|
|
['--vidbee-group-key=host:example.com']
|
|
]
|
|
for (let i = 0; i < 1000; i++) {
|
|
const yt: string[] = []
|
|
const argv: string[] = []
|
|
const ytLen = Math.floor(rng() * ytTokens.length)
|
|
for (let j = 0; j < ytLen; j++) {
|
|
const idx = Math.floor(rng() * ytTokens.length)
|
|
const tok = ytTokens[idx] as string
|
|
yt.push(tok)
|
|
argv.push(tok)
|
|
}
|
|
// Sprinkle 0..3 vidbee flag groups at random positions.
|
|
const vidbeeCount = Math.floor(rng() * 4)
|
|
for (let j = 0; j < vidbeeCount; j++) {
|
|
const groupIdx = Math.floor(rng() * vidbeeTokens.length)
|
|
const group = vidbeeTokens[groupIdx] as string[]
|
|
const insertPos = Math.floor(rng() * (argv.length + 1))
|
|
argv.splice(insertPos, 0, ...group)
|
|
}
|
|
// -o - is only legal in probe; if argv contains "-o" "-" pair, ensure
|
|
// the test doesn't trip the §4.5 guard incorrectly.
|
|
if (containsStdoutOutput(argv) && !containsProbe(argv)) continue
|
|
const r = parseArgv(argv)
|
|
if (r.kind !== 'ytdlp') {
|
|
throw new Error(
|
|
`unexpected subcommand result for argv: ${JSON.stringify(argv)}`
|
|
)
|
|
}
|
|
expect(r.ytArgs).toEqual(yt)
|
|
}
|
|
})
|
|
})
|
|
|
|
function containsStdoutOutput(argv: readonly string[]): boolean {
|
|
for (let i = 0; i < argv.length; i++) {
|
|
if (argv[i] === '-o' || argv[i] === '--output') {
|
|
if (argv[i + 1] === '-') return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
function containsProbe(argv: readonly string[]): boolean {
|
|
return argv.some(
|
|
(t) =>
|
|
t === '-j' ||
|
|
t === '-J' ||
|
|
t === '-F' ||
|
|
t === '-s' ||
|
|
t.startsWith('--dump-') ||
|
|
t.startsWith('--list-') ||
|
|
t.startsWith('--get-') ||
|
|
t === '--simulate' ||
|
|
t === '--skip-download' ||
|
|
t === '--print' ||
|
|
t.startsWith('--print=') ||
|
|
t === '--update' ||
|
|
t === '--version'
|
|
)
|
|
}
|
|
|
|
// Deterministic PRNG so failures are reproducible.
|
|
function mulberry32(seed: number): () => number {
|
|
let s = seed >>> 0
|
|
return () => {
|
|
s = (s + 0x6d_2b_79_f5) >>> 0
|
|
let t = s
|
|
t = Math.imul(t ^ (t >>> 15), t | 1)
|
|
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
|
|
return ((t ^ (t >>> 14)) >>> 0) / 4_294_967_296
|
|
}
|
|
}
|