feat: publish av-tools 0.1.0

This commit is contained in:
2026-07-24 14:58:03 +02:00
commit fcc537b024
254 changed files with 63270 additions and 0 deletions

View File

@@ -0,0 +1,199 @@
import { describe, expect, it } from 'vitest';
import {
ProjectValidationError,
assertValidProject,
validateProject,
} from '../../../src/project/project.validation';
import { populatedProject } from './project-fixture';
describe('project validation', () => {
it('returns an isolated, typed copy for a valid version-1 project', () => {
const input = populatedProject();
const result = validateProject(input);
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.project).toEqual(input);
expect(result.project).not.toBe(input);
}
});
it('reports duplicate identities, missing references and bad ranges', () => {
const project = populatedProject();
const invalid = {
...project,
assets: [...project.assets, { ...project.assets[0] }],
timeline: [
{
...project.timeline[0],
assetId: 'missing',
sourceInSeconds: 5,
sourceOutSeconds: 3,
crop: { x: -1, y: 0, width: 0, height: 100 },
},
],
};
const result = validateProject(invalid);
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.issues.map((issue) => issue.code)).toEqual(
expect.arrayContaining([
'duplicate-id',
'missing-reference',
'invalid-range',
'invalid-value',
])
);
}
});
it('checks clip bounds, fade bounds, chapter bounds and output streams', () => {
const project = populatedProject();
const invalid = {
...project,
timeline: [
{
...project.timeline[0],
sourceOutSeconds: 11,
audio: {
enabled: true,
fadeInSeconds: 8,
fadeOutSeconds: 8,
},
},
],
chapters: [
{
id: 'chapter-1',
startSeconds: 0,
endSeconds: 20,
title: 'Too long',
},
],
output: {
...project.output,
videoEnabled: false,
audioEnabled: false,
},
};
const result = validateProject(invalid);
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'invalid-range',
path: 'timeline[0]',
}),
expect.objectContaining({
code: 'out-of-bounds',
path: 'timeline[0].audio',
}),
expect.objectContaining({
code: 'out-of-bounds',
path: 'chapters[0].endSeconds',
}),
expect.objectContaining({
code: 'invalid-value',
path: 'output',
}),
])
);
}
});
it('validates persisted fade curves and chapter time-base resolution', () => {
const project = populatedProject();
const invalid = {
...project,
timeline: [
{
...project.timeline[0],
audio: {
enabled: true,
fadeInSeconds: 0.2,
fadeCurve: 'log',
},
},
],
chapters: [
{
id: 'chapter-1',
startSeconds: 0,
endSeconds: 0.1,
title: 'Too short for this base',
timeBase: '1/1',
},
],
};
const result = validateProject(invalid);
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'invalid-value',
path: 'timeline[0].audio.fadeCurve',
}),
expect.objectContaining({
code: 'invalid-value',
path: 'chapters[0].timeBase',
}),
])
);
}
});
it('rejects unsupported schema versions with a useful typed exception', () => {
const project = { ...populatedProject(), schemaVersion: 99 };
expect(() => assertValidProject(project)).toThrow(ProjectValidationError);
const result = validateProject(project);
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.issues[0]).toMatchObject({
code: 'unsupported-version',
path: 'schemaVersion',
});
}
});
it('does not trust malformed normalized probe fields from an import', () => {
const project = populatedProject();
const invalid = structuredClone(project) as unknown as {
assets: Array<{
probe?: {
streams: Array<{
width?: unknown;
rotationDegrees?: unknown;
}>;
};
}>;
};
const stream = invalid.assets[0]?.probe?.streams[0];
if (stream) {
stream.width = '1920';
stream.rotationDegrees = '90';
}
const result = validateProject(invalid);
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.issues).toContainEqual(
expect.objectContaining({
code: 'invalid-type',
path: 'assets[0].probe.streams[0].width',
})
);
expect(result.issues).toContainEqual(
expect.objectContaining({
code: 'invalid-type',
path: 'assets[0].probe.streams[0].rotationDegrees',
})
);
}
});
});