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

481 lines
15 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { run } from '../src/runtime'
import type { ContractClient } from '../src/subcommands'
import type { ConnectOptions, ConnectResult } from '../src/transport/connect'
import type { Task } from '@vidbee/task-queue'
import { EMPTY_PROGRESS } from '@vidbee/task-queue'
function fakeTask(id: string, status: Task['status'] = 'queued'): Task {
return {
id,
kind: 'video',
parentId: null,
input: { url: 'https://e.com', kind: 'video' },
priority: 0,
groupKey: 'e.com',
status,
prevStatus: null,
statusReason: null,
enteredStatusAt: 0,
attempt: 0,
maxAttempts: 5,
nextRetryAt: null,
progress: { ...EMPTY_PROGRESS },
output: null,
lastError: null,
pid: null,
pidStartedAt: null,
createdAt: 0,
updatedAt: 0
}
}
function fakeClient(seeded: Task[] = [fakeTask('t1', 'queued'), fakeTask('t2', 'completed')]): ContractClient {
return {
list: async () => ({ items: seeded, nextCursor: null }),
get: async (id) => seeded.find((t) => t.id === id) ?? fakeTask(id),
stats: async () => ({ pending: 0, running: 0, completed: 1 }),
removeFromHistory: async () => {}
}
}
function fakeConnect(client: ContractClient) {
return async (_opts: ConnectOptions): Promise<ConnectResult> => ({
kind: 'connected',
client
})
}
function makeIO(client = fakeClient()) {
const out: string[] = []
const err: string[] = []
return {
out,
err,
io: {
stdout: (l: string) => out.push(l),
stderr: (l: string) => err.push(l),
connect: fakeConnect(client)
}
}
}
describe('run() — argv error path', () => {
it('exits 2 with error envelope on unknown --vidbee-*', async () => {
const { io, out } = makeIO()
const r = await run(['--vidbe-wait', 'https://x'], io)
expect(r.exitCode).toBe(2)
const env = JSON.parse(out[0] as string)
expect(env.ok).toBe(false)
expect(env.code).toBe('UNKNOWN_VIDBEE_FLAG')
})
it('exits 2 with STDOUT_OUTPUT_DISALLOWED on -o - in download mode', async () => {
const { io, out } = makeIO()
const r = await run(['-o', '-', 'https://x'], io)
expect(r.exitCode).toBe(2)
const env = JSON.parse(out[0] as string)
expect(env.code).toBe('STDOUT_OUTPUT_DISALLOWED')
})
})
describe('run() — read-only subcommands via fake transport', () => {
it(':status returns stats from the contract', async () => {
const { io, out } = makeIO()
const r = await run(['--vidbee-local', ':status'], io)
expect(r.exitCode).toBe(0)
const env = JSON.parse(out[0] as string)
expect(env.ok).toBe(true)
expect(env.mode).toBe('subcommand')
expect(env.subcommand).toBe('status')
expect(env.result).toEqual({ pending: 0, running: 0, completed: 1 })
})
it(':download list passes filters through to the contract', async () => {
const { io, out } = makeIO()
const r = await run(
['--vidbee-local', ':download', 'list', '--status', 'queued', '--limit', '10'],
io
)
expect(r.exitCode).toBe(0)
const env = JSON.parse(out[0] as string)
expect(env.result.items).toHaveLength(2)
})
it(':download status <id> returns the task', async () => {
const { io, out } = makeIO()
const r = await run(['--vidbee-local', ':download', 'status', 't1'], io)
expect(r.exitCode).toBe(0)
const env = JSON.parse(out[0] as string)
expect(env.result.id).toBe('t1')
})
it(':download status without id reports parse error', async () => {
const { io, out } = makeIO()
const r = await run(['--vidbee-local', ':download', 'status'], io)
expect(r.exitCode).toBe(2)
const env = JSON.parse(out[0] as string)
expect(env.code).toBe('PARSE_ERROR')
})
it(':history list forwards to the contract', async () => {
const { io, out } = makeIO()
const r = await run(['--vidbee-local', ':history', 'list'], io)
expect(r.exitCode).toBe(0)
const env = JSON.parse(out[0] as string)
expect(env.ok).toBe(true)
})
it(':history remove <id...> calls the contract per id', async () => {
const removed: string[] = []
const client: ContractClient = {
list: async () => ({ items: [], nextCursor: null }),
get: async (id) => fakeTask(id),
stats: async () => ({}),
removeFromHistory: async (id) => {
removed.push(id)
}
}
const out: string[] = []
const r = await run(['--vidbee-local', ':history', 'remove', 'a', 'b'], {
stdout: (l) => out.push(l),
stderr: () => {},
connect: fakeConnect(client)
})
expect(r.exitCode).toBe(0)
expect(removed).toEqual(['a', 'b'])
})
it('unknown subcommand exits 2', async () => {
const { io, out } = makeIO()
const r = await run(['--vidbee-local', ':bogus'], io)
expect(r.exitCode).toBe(2)
const env = JSON.parse(out[0] as string)
expect(env.code).toBe('PARSE_ERROR')
})
})
describe('run() — :download write verbs', () => {
it(':download cancel calls client.cancel', async () => {
const cancelled: string[] = []
const client: ContractClient = {
...fakeClient(),
cancel: async (id) => {
cancelled.push(id)
}
}
const out: string[] = []
const r = await run(['--vidbee-local', ':download', 'cancel', 't1'], {
stdout: (l) => out.push(l),
stderr: () => {},
connect: fakeConnect(client)
})
expect(r.exitCode).toBe(0)
expect(cancelled).toEqual(['t1'])
const env = JSON.parse(out[0] as string)
expect(env.result.status).toBe('cancel-requested')
})
it(':download pause forwards --reason', async () => {
const paused: { id: string; reason?: string }[] = []
const client: ContractClient = {
...fakeClient(),
pause: async (id, reason) => {
paused.push({ id, reason })
}
}
const out: string[] = []
await run(['--vidbee-local', ':download', 'pause', 't1', '--reason', 'manual'], {
stdout: (l) => out.push(l),
stderr: () => {},
connect: fakeConnect(client)
})
expect(paused).toEqual([{ id: 't1', reason: 'manual' }])
})
it(':download retry calls client.retry', async () => {
const retried: string[] = []
const client: ContractClient = {
...fakeClient(),
retry: async (id) => {
retried.push(id)
}
}
const out: string[] = []
const r = await run(['--vidbee-local', ':download', 'retry', 't1'], {
stdout: (l) => out.push(l),
stderr: () => {},
connect: fakeConnect(client)
})
expect(r.exitCode).toBe(0)
expect(retried).toEqual(['t1'])
})
it(':download cancel surfaces CONTRACT_VERSION_MISMATCH when transport lacks support', async () => {
const out: string[] = []
const r = await run(['--vidbee-local', ':download', 'cancel', 't1'], {
stdout: (l) => out.push(l),
stderr: () => {},
connect: fakeConnect(fakeClient())
})
expect(r.exitCode).toBe(5)
const env = JSON.parse(out[0] as string)
expect(env.code).toBe('CONTRACT_VERSION_MISMATCH')
})
it(':download logs returns task + stderrTail', async () => {
const t = fakeTask('t1', 'failed')
;(t as { lastError: unknown }).lastError = { stderrTail: 'boom' }
const client: ContractClient = {
...fakeClient([t]),
get: async () => t
}
const out: string[] = []
await run(['--vidbee-local', ':download', 'logs', 't1'], {
stdout: (l) => out.push(l),
stderr: () => {},
connect: fakeConnect(client)
})
const env = JSON.parse(out[0] as string)
expect(env.result.logs.stderrTail).toBe('boom')
})
})
describe('run() — yt-dlp probe (Phase B)', () => {
it('emits §4.4 probe envelope with sanitized command', async () => {
const out: string[] = []
const r = await run(['--vidbee-local', '-j', '--password', 'shh', 'https://x'], {
stdout: (l) => out.push(l),
stderr: () => {},
connect: fakeConnect(fakeClient()),
probe: async () => ({ kind: 'success', stdout: '{"title":"x"}', stderr: '', exitCode: 0, binary: 'yt-dlp' })
})
expect(r.exitCode).toBe(0)
const env = JSON.parse(out[0] as string)
expect(env.ok).toBe(true)
expect(env.mode).toBe('probe')
expect(env.command).toContain('<redacted>')
expect(env.ytDlp.exitCode).toBe(0)
})
it('reports PROBE_OUTPUT_TOO_LARGE when stdout exceeds 32MB', async () => {
const out: string[] = []
const r = await run(['-j', 'https://x'], {
stdout: (l) => out.push(l),
stderr: () => {},
connect: fakeConnect(fakeClient()),
probe: async () => ({
kind: 'error',
envelope: {
ok: false,
code: 'PROBE_OUTPUT_TOO_LARGE',
message: 'too big'
}
})
})
expect(r.exitCode).toBe(1)
const env = JSON.parse(out[0] as string)
expect(env.code).toBe('PROBE_OUTPUT_TOO_LARGE')
})
})
describe('run() — yt-dlp download enqueue (Phase B)', () => {
it('detached mode returns queued task and exit 0', async () => {
const queued = fakeTask('q1', 'queued')
const client: ContractClient = {
...fakeClient(),
add: async () => ({ id: 'q1', task: queued }),
get: async () => queued
}
const out: string[] = []
const r = await run(['--vidbee-local', 'https://x'], {
stdout: (l) => out.push(l),
stderr: () => {},
connect: fakeConnect(client)
})
expect(r.exitCode).toBe(0)
const env = JSON.parse(out[0] as string)
expect(env.mode).toBe('download')
expect(env.task.id).toBe('q1')
expect(env.task.status).toBe('queued')
expect(env.task.command).toContain('yt-dlp')
})
it('--vidbee-wait blocks then exits 0 on completed', async () => {
const completed = fakeTask('q1', 'completed')
const client: ContractClient = {
...fakeClient(),
add: async () => ({ id: 'q1', task: completed }),
get: async () => completed
}
const out: string[] = []
const r = await run(['--vidbee-local', '--vidbee-wait', 'https://x'], {
stdout: (l) => out.push(l),
stderr: () => {},
connect: fakeConnect(client)
})
expect(r.exitCode).toBe(0)
const env = JSON.parse(out[0] as string)
expect(env.task.status).toBe('completed')
})
it('--vidbee-wait exits 1 on retry-scheduled', async () => {
const retry = fakeTask('q1', 'retry-scheduled')
const client: ContractClient = {
...fakeClient(),
add: async () => ({ id: 'q1', task: retry }),
get: async () => retry
}
const out: string[] = []
const r = await run(['--vidbee-local', '--vidbee-wait', 'https://x'], {
stdout: (l) => out.push(l),
stderr: () => {},
connect: fakeConnect(client)
})
expect(r.exitCode).toBe(1)
const env = JSON.parse(out[0] as string)
expect(env.ok).toBe(false)
})
})
describe('run() — output formatting', () => {
it('--vidbee-pretty produces pretty-printed JSON', async () => {
const { io, out } = makeIO()
await run(['--vidbee-local', '--vidbee-pretty', ':status'], io)
expect(out[0]).toContain('\n')
})
})
describe('run() — :version', () => {
it('emits CLI/contract version + changelog without contacting any host', async () => {
const out: string[] = []
let connectCalled = false
const r = await run([':version'], {
stdout: (l) => out.push(l),
stderr: () => {},
connect: async () => {
connectCalled = true
return { kind: 'connected', client: fakeClient() }
},
readVersion: () => ({
cli: '1.2.3',
contract: '1.2.3',
changelog: 'https://example.test/changelog'
})
})
expect(r.exitCode).toBe(0)
expect(connectCalled).toBe(false)
const env = JSON.parse(out[0] as string)
expect(env.ok).toBe(true)
expect(env.mode).toBe('subcommand')
expect(env.subcommand).toBe('version')
expect(env.result.cli).toBe('1.2.3')
expect(env.result.contract).toBe('1.2.3')
expect(env.result.changelog).toContain('changelog')
})
})
describe('run() — :upgrade', () => {
it('reports up-to-date when current >= latest', async () => {
const out: string[] = []
const r = await run([':upgrade'], {
stdout: (l) => out.push(l),
stderr: () => {},
readVersion: () => ({
cli: '0.2.0',
contract: '0.2.0',
changelog: 'https://example.test/changelog'
}),
checkUpgrade: async (input) => ({
current: input.current,
latest: '0.2.0',
upToDate: true,
cached: false,
cachedAt: '2026-01-01T00:00:00.000Z',
registryUrl: 'https://registry.npmjs.org/@vidbee/cli/latest',
installCommands: {
npm: 'npm install -g @vidbee/cli',
pnpm: 'pnpm add -g @vidbee/cli',
bun: 'bun install -g @vidbee/cli',
brew: 'brew upgrade vidbee/tap/vidbee'
}
})
})
expect(r.exitCode).toBe(0)
const env = JSON.parse(out[0] as string)
expect(env.subcommand).toBe('upgrade')
expect(env.result.upToDate).toBe(true)
})
it('passes --force through to the upgrade checker', async () => {
let receivedForce = false
await run([':upgrade', '--force'], {
stdout: () => {},
stderr: () => {},
readVersion: () => ({ cli: '0.1.0', contract: '0.1.0', changelog: '' }),
checkUpgrade: async (input) => {
receivedForce = input.force === true
return {
current: input.current,
latest: '0.2.0',
upToDate: false,
cached: false,
cachedAt: null,
registryUrl: 'https://registry.npmjs.org/@vidbee/cli/latest',
installCommands: { npm: '', pnpm: '', bun: '', brew: '' }
}
}
})
expect(receivedForce).toBe(true)
})
it('passes --cache <path> through to the upgrade checker', async () => {
let receivedCache: string | undefined
await run([':upgrade', '--cache', '/tmp/x'], {
stdout: () => {},
stderr: () => {},
readVersion: () => ({ cli: '0.1.0', contract: '0.1.0', changelog: '' }),
checkUpgrade: async (input) => {
receivedCache = input.cachePath
return {
current: input.current,
latest: '0.1.0',
upToDate: true,
cached: false,
cachedAt: null,
registryUrl: 'https://registry.npmjs.org/@vidbee/cli/latest',
installCommands: { npm: '', pnpm: '', bun: '', brew: '' }
}
}
})
expect(receivedCache).toBe('/tmp/x')
})
it('rejects unknown :upgrade flags with exit 2', async () => {
const out: string[] = []
const r = await run([':upgrade', '--bogus'], {
stdout: (l) => out.push(l),
stderr: () => {},
readVersion: () => ({ cli: '0.1.0', contract: '0.1.0', changelog: '' })
})
expect(r.exitCode).toBe(2)
const env = JSON.parse(out[0] as string)
expect(env.code).toBe('PARSE_ERROR')
})
it('reports HOST_UNREACHABLE when the registry fetch fails', async () => {
const out: string[] = []
const r = await run([':upgrade'], {
stdout: (l) => out.push(l),
stderr: () => {},
readVersion: () => ({ cli: '0.1.0', contract: '0.1.0', changelog: '' }),
checkUpgrade: async () => {
throw new Error('econnrefused')
}
})
expect(r.exitCode).toBe(3)
const env = JSON.parse(out[0] as string)
expect(env.code).toBe('API_UNREACHABLE')
})
})