Files
wehub-resource-sync 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) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:24:41 +08:00

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
}
}