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,169 @@
import { describe, expect, it } from 'vitest';
import {
SourceReattachmentError,
bestReattachmentCandidate,
confirmSourceReattachment,
rankReattachmentCandidates,
} from '../../../src/project/source-reattachment';
import type { SourceHash } from '../../../src/project/project.types';
import { UPDATED_AT, mediaAsset, populatedProject } from './project-fixture';
const HASH_A: SourceHash = {
algorithm: 'sha-256',
value: 'a'.repeat(64),
scope: 'full',
};
const HASH_B: SourceHash = {
algorithm: 'sha-256',
value: 'b'.repeat(64),
scope: 'full',
};
describe('source reattachment matching', () => {
it('ranks size, timestamp, filename, MIME type and optional hash evidence', () => {
const asset = mediaAsset({ sourceStatus: 'missing', hash: HASH_A });
const candidates = [
{
name: 'renamed.mp4',
size: asset.size,
lastModified: asset.lastModified,
mimeType: asset.mimeType,
},
{
name: asset.originalName,
size: asset.size,
lastModified: asset.lastModified,
mimeType: asset.mimeType,
hash: HASH_A,
},
{
name: asset.originalName,
size: asset.size,
lastModified: asset.lastModified,
mimeType: asset.mimeType,
hash: HASH_B,
},
];
const matches = rankReattachmentCandidates(asset, candidates);
expect(matches.map((match) => match.confidence)).toEqual([
'exact',
'likely',
'none',
]);
expect(matches[0]).toMatchObject({
score: 190,
requiresExplicitConfirmation: true,
});
expect(
matches[0]?.evidence.find((evidence) => evidence.field === 'hash')
).toMatchObject({ matches: true });
});
it('does not suggest ambiguous ties', () => {
const asset = mediaAsset({ sourceStatus: 'missing' });
const candidates = [
{
name: 'a.mp4',
size: asset.size,
lastModified: asset.lastModified,
mimeType: asset.mimeType,
},
{
name: 'b.mp4',
size: asset.size,
lastModified: asset.lastModified,
mimeType: asset.mimeType,
},
];
expect(bestReattachmentCandidate(asset, candidates)).toBeUndefined();
});
it('allows filesystem timestamp tolerance but treats size/hash mismatch as unsafe', () => {
const asset = mediaAsset({ sourceStatus: 'missing', hash: HASH_A });
expect(
rankReattachmentCandidates(asset, [
{
name: asset.originalName,
size: asset.size,
lastModified: asset.lastModified + 1_500,
mimeType: asset.mimeType,
},
])[0]?.confidence
).toBe('likely');
expect(
rankReattachmentCandidates(asset, [
{
name: asset.originalName,
size: asset.size + 1,
lastModified: asset.lastModified,
mimeType: asset.mimeType,
},
])[0]?.confidence
).toBe('none');
});
it('does not treat incomparable partial and full hashes as a mismatch', () => {
const asset = mediaAsset({ sourceStatus: 'missing', hash: HASH_A });
const partial: SourceHash = {
algorithm: 'sha-256',
value: 'b'.repeat(64),
scope: 'partial',
bytesHashed: 1_024,
};
expect(
rankReattachmentCandidates(asset, [
{
name: asset.originalName,
size: asset.size,
lastModified: asset.lastModified,
mimeType: asset.mimeType,
hash: partial,
},
])[0]?.confidence
).toBe('likely');
});
it('rejects malformed file descriptors before scoring', () => {
expect(() =>
rankReattachmentCandidates(mediaAsset(), [
{
name: '',
size: -1,
lastModified: Number.NaN,
},
])
).toThrow(SourceReattachmentError);
});
it('only marks a source attached after an explicit confirmation call', () => {
const project = {
...populatedProject(),
assets: [mediaAsset({ sourceStatus: 'missing' })],
};
const candidate = {
name: 'renamed.mp4',
size: 1_024,
lastModified: 1_700_000_000_000,
mimeType: 'video/mp4',
};
const result = confirmSourceReattachment(
project,
'asset-1',
candidate,
UPDATED_AT
);
expect(project.assets[0]?.sourceStatus).toBe('missing');
expect(result.assets[0]?.sourceStatus).toBe('attached');
expect(() =>
confirmSourceReattachment(
project,
'asset-1',
{ ...candidate, size: 1 },
UPDATED_AT
)
).toThrow(SourceReattachmentError);
});
});