teqo-backup/tests/unit/webSpeeches.unit.spec.ts

147 lines
5 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
formatWebSpeechReport,
isCompleteWebSpeechState,
planWebSpeechBatch,
summarizePlannedActions,
summarizeWebSpeechResults,
webSpeechPlannedAction,
} from '../../scripts/lib/webSpeeches.mjs'
type WebSpeechIngestResult =
import('../../src/utilities/speech/webSpeechIngest.ts').WebSpeechIngestResult
// C215 — the CLI planning/reporting rules: one invalid finding fails only
// itself, duplicates are dropped by natural key and the report never hides a
// failure.
const youtube = (overrides = {}) => ({
platform: 'youtube',
url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
publishedAt: '2026-09-20',
...overrides,
})
describe('planWebSpeechBatch', () => {
it('keeps the valid entries and reports invalid/duplicates separately', () => {
const plan = planWebSpeechBatch([
youtube(),
youtube({ url: 'https://youtu.be/dQw4w9WgXcQ' }),
youtube({ platform: 'radio' }),
youtube({ externalId: 'outro', url: 'https://www.youtube.com/watch?v=zzz' }),
{ platform: 'youtube', url: 'nao-e-url', publishedAt: '2026-09-20' },
])
expect(plan.entries.map((entry) => entry.sourceKey)).toEqual([
'web:youtube:https://www.youtube.com/watch?v=dQw4w9WgXcQ',
'web:youtube:outro',
])
expect(plan.duplicates).toEqual([
{ index: 1, sourceKey: 'web:youtube:https://www.youtube.com/watch?v=dQw4w9WgXcQ' },
])
expect(plan.invalid.map((entry) => entry.index)).toEqual([2, 4])
})
})
describe('webSpeechPlannedAction', () => {
it('reads the stored state: new, incomplete, complete', () => {
expect(webSpeechPlannedAction(null)).toBe('novo')
expect(webSpeechPlannedAction({ segmentCount: 0, mirroredMedia: null })).toBe('atualizado')
expect(webSpeechPlannedAction({ segmentCount: 3, mirroredMedia: null })).toBe('atualizado')
expect(webSpeechPlannedAction({ segmentCount: 3, mirroredMedia: 9 })).toBe('ignorado')
expect(webSpeechPlannedAction({ segmentCount: 3, mirroredMedia: 9 }, true)).toBe('reprocessado')
})
it('isCompleteWebSpeechState requires transcript and mirrored file', () => {
expect(isCompleteWebSpeechState(null)).toBe(false)
expect(isCompleteWebSpeechState({ segmentCount: 0, mirroredMedia: 1 })).toBe(false)
expect(isCompleteWebSpeechState({ segmentCount: 1, mirroredMedia: null })).toBe(false)
expect(isCompleteWebSpeechState({ segmentCount: 1, mirroredMedia: 1 })).toBe(true)
})
})
describe('summarizeWebSpeechResults', () => {
it('counts statuses, sums costs and lists every failure', () => {
const result = (overrides: Partial<WebSpeechIngestResult>): WebSpeechIngestResult => ({
sourceKey: 'web:youtube:x',
status: 'skipped',
stage: null,
error: null,
asrSeconds: 0,
asrCostUsd: 0,
llmCostUsd: null,
...overrides,
})
const summary = summarizeWebSpeechResults([
result({ status: 'created', asrSeconds: 60, asrCostUsd: 0.00045, llmCostUsd: 0.0001 }),
result({ status: 'updated', asrSeconds: 120, asrCostUsd: 0.0009 }),
result({}),
result({
status: 'failed',
sourceKey: 'web:audio:https://radio.example/a.mp3',
stage: 'acquisition',
error: 'HTTP 404',
}),
])
expect(summary).toMatchObject({ created: 1, updated: 1, skipped: 1, failed: 1 })
expect(summary.asrSeconds).toBe(180)
expect(summary.asrCostUsd).toBeCloseTo(0.00135, 6)
expect(summary.llmCostUsd).toBeCloseTo(0.0001, 6)
expect(summary.failures).toEqual([
{
sourceKey: 'web:audio:https://radio.example/a.mp3',
stage: 'acquisition',
error: 'HTTP 404',
},
])
})
})
describe('formatWebSpeechReport', () => {
const base = {
runAt: '2026-09-24T10:00:00.000Z',
mode: 'import',
findings: 4,
planned: 3,
invalid: [{ index: 3, error: 'url: inválida' }],
duplicates: [{ index: 1, sourceKey: 'web:youtube:x' }],
durationMs: 12_500,
created: 1,
updated: 1,
skipped: 1,
failed: 1,
asrSeconds: 90,
asrCostUsd: 0.000675,
llmCostUsd: 0.0001,
failures: [
{
sourceKey: 'web:audio:https://radio.example/a.mp3',
stage: 'acquisition' as const,
error: 'HTTP 404',
},
],
}
it('prints counts, costs, the invalid entries and every failure', () => {
const lines = formatWebSpeechReport(base).join('\n')
expect(lines).toContain('novos: 1 · atualizados: 1 · ignorados: 1 · falhas: 1')
expect(lines).toContain('ASR: 1min30s (~US$ 0.0007)')
expect(lines).toContain('inválido #3')
expect(lines).toContain('web:audio:https://radio.example/a.mp3 (acquisition): HTTP 404')
})
it('prints the dry-run plan instead of the execution counts', () => {
const lines = formatWebSpeechReport({
...base,
mode: 'dry-run',
plannedActions: summarizePlannedActions([
{ action: 'novo' },
{ action: 'ignorado' },
{ action: 'reprocessado' },
]),
}).join('\n')
expect(lines).toContain('plano (dry-run): novos: 1 · atualizados: 0 · ignorados: 1')
})
})