File
Blob: test/find-common-haves-batch.worker.test.ts
| 1 | import { it, expect } from "vitest"; |
| 2 | import { env, exports as workerExports } from "cloudflare:workers"; |
| 3 | import { decodePktLines } from "@/worker/git"; |
| 4 | import { uniqueRepoId, runDOWithRetry } from "./util/test-helpers"; |
| 5 | import { setupRepoForTests } from "./util/repoSeed"; |
| 6 | |
| 7 | function 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 | |
| 49 | function 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 | |
| 60 | function randomOid(seed: string) { |
| 61 | return seed.repeat(40).slice(0, 40).toLowerCase(); |
| 62 | } |
| 63 | |
| 64 | it("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 | }); |