1import {
2 boundScan,
3 decodeHashResponse,
4 encodeHashEvent,
5 handleHashEvent,
6 isHashEvent,
7} from "@/Git/Hasher/index.ts";
8import {
9 encodeTypeSize,
10 hashObject,
11 makeSha1,
12} from "@/Git/Protocol/ObjectCodec.ts";
13import { packHeader } from "@/Git/Protocol/PackWriter.ts";
14import { scanPart } from "@/Git/Protocol/PartialScan.ts";
15import * as Zlib from "@/Git/Protocol/Zlib.ts";
16import { describe, expect, test } from "alchemy-test";
17import * as Effect from "effect/Effect";
18import { concat } from "../harness/pack.ts";
19
20const buildPack = (n: number) =>
21 Effect.gen(function* () {
22 const pieces: Array<Uint8Array> = [packHeader(n)];
23 for (let i = 0; i < n; i++) {
24 const c = new Uint8Array(300 + (i % 50));
25 crypto.getRandomValues(c);
26 yield* hashObject(3, c);
27 pieces.push(encodeTypeSize(3, c.length), yield* Zlib.deflate(c));
28 }
29 const body = concat(pieces);
30 const sha = makeSha1();
31 sha.update(body);
32 return concat([body, sha.digest()]);
33 });
34
35describe(
36 "Lambda hash event (DESIGN §22.11)",
37 { tags: ["unit", "local"] },
38 () => {
39 test("a chunk round-trips through the event and response encodings", async () => {
40 await Effect.runPromise(
41 Effect.gen(function* () {
42 const pack = yield* buildPack(40);
43 const event = encodeHashEvent(pack, {
44 base: 12,
45 skip: 12,
46 remaining: 40,
47 maxObjectSize: 1 << 20,
48 });
49 expect(isHashEvent(event)).toBe(true);
50 expect(isHashEvent({ payload: 1 })).toBe(false);
51 const response = yield* handleHashEvent(event);
52 const scan = yield* decodeHashResponse(response);
53 const direct = yield* scanPart(pack.subarray(12), {
54 base: 12,
55 remaining: 40,
56 maxObjectSize: 1 << 20,
57 });
58 expect(scan.count).toBe(direct.count);
59 expect(scan.entries.map((e) => e.oid)).toEqual(
60 direct.entries.map((e) => e.oid),
61 );
62 expect(scan.consumedTo).toBe(direct.consumedTo);
63 }),
64 );
65 });
66
67 test("boundScan demotes the largest resolved deltas until the budget fits and keeps their bases", () => {
68 const mk = (i: number, z: number) => ({
69 oid: `${i}`.padStart(40, "a") as never,
70 type: 3 as const,
71 offset: i * 100,
72 size: z * 2,
73 dataOffset: i * 100 + 2,
74 span: 10,
75 zdata: new Uint8Array(z),
76 baseOffset: i * 100 - 50,
77 });
78 const scan = {
79 firstOffset: 12,
80 entries: [mk(1, 5000), mk(2, 100), mk(3, 3000)],
81 unresolved: [],
82 consumedTo: 400,
83 count: 3,
84 };
85 const bounded = boundScan(scan, 3000 + 3 * 120);
86 // 5000 goes first (8100 → 3100, still over), then 3000; 100 fits.
87 expect(bounded.entries.map((e) => e.offset)).toEqual([200]);
88 expect(bounded.unresolved.map((u) => [u.offset, u.baseOffset])).toEqual([
89 [100, 50],
90 [300, 250],
91 ]);
92 // Under budget: content stripped, nothing demoted.
93 const roomy = boundScan(scan, 1 << 20);
94 expect(roomy.entries.length).toBe(3);
95 expect(roomy.unresolved.length).toBe(0);
96 });
97
98 test("an error on the Lambda side is a typed HashError for the client", async () => {
99 const result = await Effect.runPromise(
100 Effect.result(decodeHashResponse({ error: "PackFormatError: bad" })),
101 );
102 expect(result._tag).toBe("Failure");
103 });
104 },
105);