Twitch-VOD-Manager/src/main/domain/integrity-check.test.ts
xRangerDE 987fb73a0e feat(integrity): ffprobe JSON parser + verdict assessor (12 tests)
Pillar 1 storage-layer piece. Spawn-free design — caller liefert das
ffprobe-Output als String, dieses Modul parsed + bewertet (no-video-stream,
duration-too-short, expected-duration mismatch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:52:48 +02:00

116 lines
4.0 KiB
TypeScript

import { test, expect, describe } from 'vitest';
import { parseFfprobeJson, assessIntegrity, verifyIntegrityFromJson } from './integrity-check';
const FIXTURE_GOOD = JSON.stringify({
streams: [
{ index: 0, codec_type: 'video', codec_name: 'h264', width: 1920, height: 1080, duration: '600.5' },
{ index: 1, codec_type: 'audio', codec_name: 'aac', duration: '600.5' },
],
format: { duration: '600.5', size: '50000000' },
});
const FIXTURE_NO_VIDEO = JSON.stringify({
streams: [
{ index: 0, codec_type: 'audio', codec_name: 'aac', duration: '10' },
],
format: { duration: '10', size: '500000' },
});
const FIXTURE_EMPTY = JSON.stringify({
streams: [],
format: { duration: '0.04', size: '1234' },
});
describe('parseFfprobeJson', () => {
test('parses streams + format', () => {
const r = parseFfprobeJson(FIXTURE_GOOD);
expect(r.streams).toHaveLength(2);
expect(r.streams[0].codecType).toBe('video');
expect(r.streams[0].codecName).toBe('h264');
expect(r.streams[0].width).toBe(1920);
expect(r.durationSeconds).toBe(600.5);
expect(r.sizeBytes).toBe(50000000);
});
test('handles missing format gracefully', () => {
const r = parseFfprobeJson(JSON.stringify({ streams: [] }));
expect(r.durationSeconds).toBe(0);
expect(r.sizeBytes).toBe(0);
});
test('throws on malformed JSON', () => {
expect(() => parseFfprobeJson('{not-valid')).toThrow(/parse failed/);
});
test('coerces numeric strings to numbers', () => {
const r = parseFfprobeJson(JSON.stringify({
streams: [{ codec_type: 'video', duration: '12.34' }],
format: { duration: '12.34', size: '987654' },
}));
expect(r.durationSeconds).toBe(12.34);
expect(r.streams[0].durationSeconds).toBe(12.34);
expect(r.sizeBytes).toBe(987654);
});
});
describe('assessIntegrity', () => {
test('valid file: ok=true, no reasons', () => {
const probe = parseFfprobeJson(FIXTURE_GOOD);
const v = assessIntegrity(probe);
expect(v.ok).toBe(true);
expect(v.reasons).toEqual([]);
expect(v.hasVideo).toBe(true);
expect(v.hasAudio).toBe(true);
expect(v.durationSeconds).toBe(600.5);
});
test('no-video stream rejected', () => {
const v = assessIntegrity(parseFfprobeJson(FIXTURE_NO_VIDEO));
expect(v.ok).toBe(false);
expect(v.reasons).toContain('no-video-stream');
expect(v.hasVideo).toBe(false);
});
test('zero-duration rejected as too-short', () => {
const v = assessIntegrity(parseFfprobeJson(FIXTURE_EMPTY));
expect(v.ok).toBe(false);
expect(v.reasons.some(r => r.startsWith('duration-too-short'))).toBe(true);
});
test('expected-duration mismatch outside tolerance flagged', () => {
const v = assessIntegrity(parseFfprobeJson(FIXTURE_GOOD), {
expectedDurationSeconds: 700,
durationToleranceSeconds: 5,
});
expect(v.ok).toBe(false);
expect(v.reasons.some(r => r.startsWith('duration-mismatch'))).toBe(true);
});
test('expected-duration within tolerance accepted', () => {
const v = assessIntegrity(parseFfprobeJson(FIXTURE_GOOD), {
expectedDurationSeconds: 598,
durationToleranceSeconds: 5,
});
expect(v.ok).toBe(true);
});
test('custom minDurationSeconds threshold', () => {
const v = assessIntegrity(parseFfprobeJson(FIXTURE_GOOD), {
minDurationSeconds: 700,
});
expect(v.ok).toBe(false);
expect(v.reasons.some(r => r.startsWith('duration-too-short'))).toBe(true);
});
});
describe('verifyIntegrityFromJson', () => {
test('one-shot parse + assess', () => {
const v = verifyIntegrityFromJson(FIXTURE_GOOD);
expect(v.ok).toBe(true);
});
test('propagates parse errors', () => {
expect(() => verifyIntegrityFromJson('{broken')).toThrow();
});
});