Files
av-tools/tests/unit/export-zip.test.ts

110 lines
3.4 KiB
TypeScript

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' },
]);
});
});