feat: publish av-tools 0.1.0
This commit is contained in:
109
tests/unit/export-zip.test.ts
Normal file
109
tests/unit/export-zip.test.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { unzipSync } from 'fflate';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
createResultZip,
|
||||
saveResultsZip,
|
||||
type BlobSaver,
|
||||
type ExportResult,
|
||||
} from '../../src/export';
|
||||
|
||||
function result(id: string, fileName: string, contents: string): ExportResult {
|
||||
const blob = new Blob([contents], { type: 'text/plain' });
|
||||
return {
|
||||
id,
|
||||
planId: 'plan',
|
||||
outputId: id,
|
||||
fileName,
|
||||
blob,
|
||||
mimeType: 'text/plain',
|
||||
role: 'analysis',
|
||||
size: blob.size,
|
||||
createdAt: '2026-07-24T10:00:00.000Z',
|
||||
};
|
||||
}
|
||||
|
||||
describe('deterministic result ZIP creation', () => {
|
||||
it('sorts and deduplicates safe paths with fixed archive metadata', async () => {
|
||||
const entries = [
|
||||
{ fileName: '../../Z.txt', blob: new Blob(['z']) },
|
||||
{ fileName: 'a.txt', blob: new Blob(['first']) },
|
||||
{ fileName: 'A.TXT', blob: new Blob(['second']) },
|
||||
];
|
||||
const first = await createResultZip(entries, {
|
||||
fileName: '../My outputs.any',
|
||||
});
|
||||
const second = await createResultZip(entries, {
|
||||
fileName: '../My outputs.any',
|
||||
});
|
||||
|
||||
expect(first.fileName).toBe('My-outputs.zip');
|
||||
expect(first.entryNames).toEqual(['A-2.txt', 'Z.txt', 'a.txt']);
|
||||
expect(first.totalInputBytes).toBe(12);
|
||||
expect(new Uint8Array(await first.blob.arrayBuffer())).toEqual(
|
||||
new Uint8Array(await second.blob.arrayBuffer())
|
||||
);
|
||||
|
||||
const extracted = unzipSync(new Uint8Array(await first.blob.arrayBuffer()));
|
||||
expect(Object.keys(extracted)).toEqual(['A-2.txt', 'Z.txt', 'a.txt']);
|
||||
expect(new TextDecoder().decode(extracted['a.txt'])).toBe('first');
|
||||
expect(new TextDecoder().decode(extracted['A-2.txt'])).toBe('second');
|
||||
});
|
||||
|
||||
it('rejects empty, oversized, and pre-cancelled batches', async () => {
|
||||
await expect(createResultZip([])).rejects.toThrow(/at least one/iu);
|
||||
await expect(
|
||||
createResultZip([{ fileName: 'large.bin', blob: new Blob(['12345']) }], {
|
||||
limits: {
|
||||
maxEntries: 2,
|
||||
maxEntryBytes: 4,
|
||||
maxTotalInputBytes: 4,
|
||||
maxArchiveBytes: 100,
|
||||
},
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
expect.objectContaining({
|
||||
limit: 'entry-bytes',
|
||||
})
|
||||
);
|
||||
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
await expect(
|
||||
createResultZip([{ fileName: 'one.txt', blob: new Blob(['one']) }], {
|
||||
signal: controller.signal,
|
||||
})
|
||||
).rejects.toMatchObject({ name: 'AbortError' });
|
||||
});
|
||||
|
||||
it('packages an optional report and passes the ZIP to the save adapter', async () => {
|
||||
const saverCalls: { fileName: string; type: string }[] = [];
|
||||
const saver: BlobSaver = async (blob, fileName) => {
|
||||
saverCalls.push({ fileName, type: blob.type });
|
||||
return 'saved';
|
||||
};
|
||||
const saved = await saveResultsZip([result('one', 'one.txt', 'contents')], {
|
||||
fileName: 'batch.zip',
|
||||
saver,
|
||||
report: {
|
||||
schemaVersion: 1,
|
||||
generatedAt: '2026-07-24T10:00:00.000Z',
|
||||
plan: { id: 'plan', operation: 'convert' },
|
||||
outputs: [],
|
||||
diagnostics: [],
|
||||
privacy: {
|
||||
includesSourceMedia: false,
|
||||
includesCommandArguments: false,
|
||||
privatePathsRedacted: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(saved.outcome).toBe('saved');
|
||||
expect(saved.archive.entryNames).toEqual([
|
||||
'convert-export-report.json',
|
||||
'one.txt',
|
||||
]);
|
||||
expect(saverCalls).toEqual([
|
||||
{ fileName: 'batch.zip', type: 'application/zip' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user