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>
116 lines
4.0 KiB
TypeScript
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();
|
|
});
|
|
});
|