Skip to content
File

Blob: test/find-common-haves-batch.worker.test.ts

typescript107 lines
1import { it, expect } from "vitest";
2import { env, exports as workerExports } from "cloudflare:workers";
3import { decodePktLines } from "@/worker/git";
4import { uniqueRepoId, runDOWithRetry } from "./util/test-helpers";
5import { setupRepoForTests } from "./util/repoSeed";
6 
7function buildFetchBody({
8 wants,
9 haves,
10 done,
11}: {
12 wants: string[];
13 haves?: string[];
14 done?: boolean;
15}) {
16 // Build protocol v2 fetch request body using pkt-line framing
17 const chunks: Uint8Array[] = [];
18 const enc = new TextEncoder();
19 function pkt(s: string) {
20 const bytes = enc.encode(s);
21 const len = (bytes.length + 4).toString(16).padStart(4, "0");
22 const out = new Uint8Array(4 + bytes.length);
23 out.set(enc.encode(len), 0);
24 out.set(bytes, 4);
25 return out;
26 }
27 function flush() {
28 return enc.encode("0000");
29 }
30 chunks.push(pkt("command=fetch\n"));
31 chunks.push(pkt("agent=test\n"));
32 chunks.push(enc.encode("0001")); // delim
33 for (const w of wants) chunks.push(pkt(`want ${w}\n`));
34 for (const h of haves || []) chunks.push(pkt(`have ${h}\n`));
35 if (done) chunks.push(pkt("done\n"));
36 chunks.push(flush());
37 // concat
38 let total = 0;
39 for (const c of chunks) total += c.length;
40 const out = new Uint8Array(total);
41 let off = 0;
42 for (const c of chunks) {
43 out.set(c, off);
44 off += c.length;
45 }
46 return out;
47}
48 
49function parseAckLines(respBytes: Uint8Array): string[] {
50 const items = decodePktLines(respBytes);
51 const acks: string[] = [];
52 for (const it of items) {
53 if (it.type === "line" && (it as any).text.startsWith("ACK ")) {
54 acks.push((it as any).text.trim());
55 }
56 }
57 return acks;
58}
59 
60function randomOid(seed: string) {
61 return seed.repeat(40).slice(0, 40).toLowerCase();
62}
63 
64it("findCommonHaves batches and ACKs present haves preserving order and de-dup", async () => {
65 const owner = "o";
66 const repo = uniqueRepoId("r-find-haves");
67 await setupRepoForTests(env, owner, repo);
68 const repoId = `${owner}/${repo}`;
69 const id = env.REPO_DO.idFromName(repoId);
70 const { commitOid, treeOid } = await runDOWithRetry(
71 () => env.REPO_DO.get(id),
72 async (instance) => instance.seedMinimalRepo()
73 );
74 
75 // Build haves list with duplicates and missing entries interleaved
76 const missing1 = randomOid("a");
77 const missing2 = randomOid("b");
78 const haves = [commitOid, missing1, treeOid, commitOid, missing2];
79 
80 // Wants commitOid so server will assemble a minimal pack
81 const body = buildFetchBody({ wants: [commitOid], haves, done: false });
82 const url = `https://example.com/${owner}/${repo}/git-upload-pack`;
83 const res = await workerExports.default.fetch(url, {
84 method: "POST",
85 headers: {
86 "Content-Type": "application/x-git-upload-pack-request",
87 "Git-Protocol": "version=2",
88 },
89 body,
90 } as any);
91 expect(res.status).toBe(200);
92 
93 const bytes = new Uint8Array(await res.arrayBuffer());
94 const ackLines = parseAckLines(bytes);
95 // Expect two ACKs for commitOid and treeOid, de-duplicated, preserving first-order appearance
96 expect(ackLines.length).toBeGreaterThanOrEqual(1);
97 expect(ackLines[0]).toBe(`ACK ${commitOid} common`);
98 // Last ACK should end with ready when there is at least one ACK
99 const last = ackLines[ackLines.length - 1];
100 expect(last.startsWith("ACK ")).toBe(true);
101 expect(last.endsWith("ready")).toBe(true);
102 // Ensure treeOid is acknowledged somewhere (order after commitOid, before final ready suffix check)
103 expect(ackLines.some((l) => l === `ACK ${treeOid} common` || l === `ACK ${treeOid} ready`)).toBe(
104 true
105 );
106});