import { unzipSync } from "fflate"; import { describe, expect, it } from "vitest"; import { buildManifest, collectSelection, compareManifests, deterministicZip, normalizePath, parseManifest, serializeManifest, } from "../../src/core/repro"; function files() { const first = new File(["alpha"], "a.txt"); const second = new File(["beta"], "b.txt"); return [ { file: second, path: "b.txt" }, { file: first, path: "a.txt" }, ]; } describe("reproducible manifests", () => { it("hashes in deterministic path order", async () => { const manifest = await buildManifest(files(), ["SHA-512", "SHA-256"]); expect(manifest.files.map((entry) => entry.path)).toEqual([ "a.txt", "b.txt", ]); expect(manifest.operation.algorithms).toEqual(["SHA-256", "SHA-512"]); expect(manifest.operation.recordedAt).toBeNull(); expect(parseManifest(serializeManifest(manifest))).toEqual(manifest); }); it("reports changed, missing and unexpected files", async () => { const reference = await buildManifest(files(), ["SHA-256"]); const actual = await buildManifest( [ { file: new File(["changed"], "a.txt"), path: "a.txt" }, { file: new File(["new"], "c.txt"), path: "c.txt" }, ], ["SHA-256"], ); expect( compareManifests(reference, actual).map((item) => item.status), ).toEqual(["changed", "missing", "unexpected"]); }); it("creates byte-identical sorted ZIP files", async () => { const manifest = await buildManifest(files(), ["SHA-256"]); const first = await deterministicZip(files(), manifest); const second = await deterministicZip(files(), manifest); expect(first).toEqual(second); expect(Object.keys(unzipSync(first))).toEqual([ "REPRODUCIBILITY.json", "a.txt", "b.txt", ]); }); it("rejects unsafe and duplicate paths", () => { expect(() => normalizePath("../secret")).toThrow(/Unsafe/u); expect(() => collectSelection( [new File(["a"], "same"), new File(["b"], "same")], false, ), ).toThrow(/Duplicate/u); }); it("rejects ZIP and untrusted-manifest path collisions", async () => { await expect( deterministicZip([ { file: new File(["a"], "same"), path: "same" }, { file: new File(["b"], "same"), path: "same" }, ]), ).rejects.toThrow(/Duplicate/u); const manifest = await buildManifest(files(), ["SHA-256"]); await expect( deterministicZip( [ { file: new File(["user data"], "REPRODUCIBILITY.json"), path: "REPRODUCIBILITY.json", }, ], manifest, ), ).rejects.toThrow(/reserved/u); const duplicateManifest = { ...manifest, files: [manifest.files[0]!, manifest.files[0]!], }; expect(() => parseManifest(JSON.stringify(duplicateManifest))).toThrow( /duplicate path/u, ); }); it("validates manifest metadata, totals and digests", async () => { const manifest = await buildManifest(files(), ["SHA-256"]); expect(() => parseManifest(JSON.stringify({ ...manifest, totalBytes: 999 })), ).toThrow(/totalBytes/u); expect(() => parseManifest( JSON.stringify({ ...manifest, files: [ { ...manifest.files[0], digests: { "sha-256": "not-a-hash" } }, manifest.files[1], ], }), ), ).toThrow(/invalid digests/u); expect(() => normalizePath("a".repeat(4_097))).toThrow(/4096/u); }); });