1import { sha256 } from "@/Util/sha256";
2import { zipCode, zipFiles } from "@/Util/zip";
3import { strFromU8, unzipSync } from "fflate";
4import * as Effect from "effect/Effect";
5import { expect, test } from "alchemy-test";
6
7test(
8 "zipCode is deterministic for identical inputs",
9 async () => {
10 const hash = () =>
11 Effect.runPromise(
12 zipCode("export default 1", [
13 {
14 path: "index.mjs.map",
15 content: JSON.stringify({
16 version: 3,
17 sources: ["index.ts"],
18 }),
19 },
20 ]).pipe(Effect.flatMap(sha256)),
21 );
22
23 const first = await hash();
24 await new Promise((resolve) => setTimeout(resolve, 1100));
25 expect(await hash()).toBe(first);
26 },
27 { tags: ["unit", "local"] },
28);
29
30// Nested paths must remain deterministic across builds.
31test(
32 "zipCode is deterministic for nested package paths",
33 async () => {
34 const build = () =>
35 Effect.runPromise(
36 zipCode("export default 1", [
37 {
38 path: "node_modules/uuid/package.json",
39 content: JSON.stringify({ name: "uuid" }),
40 },
41 ]),
42 );
43
44 const first = await build();
45 const entries = unzipSync(first);
46 expect(strFromU8(entries["node_modules/uuid/package.json"]!)).toBe(
47 JSON.stringify({ name: "uuid" }),
48 );
49
50 await new Promise((resolve) => setTimeout(resolve, 1100));
51 const second = await build();
52 expect(await Effect.runPromise(sha256(second))).toBe(
53 await Effect.runPromise(sha256(first)),
54 );
55 },
56 { tags: ["unit", "local"] },
57);
58
59test(
60 "zipFiles preserves binary data, Unicode, empty files, and input order independence",
61 async () => {
62 const files = [
63 { path: "nested/你好.txt", content: "Hello 🌍" },
64 { path: "binary", content: new Uint8Array([0, 255, 128, 1]) },
65 { path: "empty", content: "" },
66 { path: "large", content: "compress me".repeat(40000) },
67 ];
68 const first = await Effect.runPromise(zipFiles(files));
69 const second = await Effect.runPromise(zipFiles([...files].reverse()));
70 expect(Buffer.isBuffer(first)).toBe(true);
71 expect(first.equals(second)).toBe(true);
72 const entries = unzipSync(first);
73 for (const file of files) {
74 expect(entries[file.path]).toEqual(
75 typeof file.content === "string"
76 ? new TextEncoder().encode(file.content)
77 : file.content,
78 );
79 }
80 expect(
81 Object.keys(unzipSync(await Effect.runPromise(zipFiles([])))),
82 ).toEqual([]);
83 },
84 { tags: ["unit", "local"] },
85);
86
87test(
88 "zipFiles records fixed timestamps and Unix file types and permissions",
89 async () => {
90 const archive = await Effect.runPromise(
91 zipFiles([
92 { path: "bin/tool", content: "#!/bin/sh", mode: 0o100755 },
93 { path: "link", content: "bin/tool", mode: 0o120777 },
94 { path: "plain", content: "text" },
95 ]),
96 );
97 // Read the ZIP central directory independently of the compression library.
98 const end = archive.length - 22;
99 expect(archive.readUInt32LE(end)).toBe(0x06054b50);
100 let offset = archive.readUInt32LE(end + 16);
101 const modes: Record<string, number> = {};
102 for (let i = 0; i < archive.readUInt16LE(end + 10); i++) {
103 expect(archive.readUInt32LE(offset)).toBe(0x02014b50);
104 expect(archive[offset + 5]).toBe(3);
105 expect(archive.readUInt16LE(offset + 12)).toBe(0);
106 expect(archive.readUInt16LE(offset + 14)).toBe(33);
107 const nameLength = archive.readUInt16LE(offset + 28);
108 const name = archive.toString(
109 "utf8",
110 offset + 46,
111 offset + 46 + nameLength,
112 );
113 modes[name] = archive.readUInt32LE(offset + 38) >>> 16;
114 offset +=
115 46 +
116 nameLength +
117 archive.readUInt16LE(offset + 30) +
118 archive.readUInt16LE(offset + 32);
119 }
120 expect(modes).toEqual({
121 "bin/tool": 0o100755,
122 link: 0o120777,
123 plain: 0o100644,
124 });
125 expect(strFromU8(unzipSync(archive).link!)).toBe("bin/tool");
126 },
127 { tags: ["unit", "local"] },
128);