Skip to content
File

Blob: test/pack-first-read-regressions.worker.test.ts

typescript195 lines
1import { describe, expect, it } from "vitest";
2import { env, exports as workerExports } from "cloudflare:workers";
3import { encodeGitObject } from "@/worker/git/core";
4import {
5 buildPack,
6 callStubWithRetry,
7 deleteLooseObjectCopies,
8 seedLegacyPackedRepo,
9 seedPackedRepo,
10 toRequestBody,
11 uniqueRepoId,
12} from "./util/test-helpers";
13import { setupRepoForTests } from "./util/repoSeed";
14import { buildFetchBody, findBytes } from "./util/fetch-protocol";
15 
16describe("pack-first read-path regressions", () => {
17 it("serves UI routes from packs after all loose copies are deleted", async () => {
18 const owner = "o";
19 const repo = uniqueRepoId("pack-first-ui");
20 await setupRepoForTests(env, owner, repo);
21 const repoId = `${owner}/${repo}`;
22 const id = env.REPO_DO.idFromName(repoId);
23 const getStub = () => env.REPO_DO.get(id);
24 
25 const seeded = await seedPackedRepo(env, repoId, getStub, { mirrorLooseToR2: true });
26 await callStubWithRetry(getStub, (stub) => stub.getActivePackCatalog());
27 await deleteLooseObjectCopies(env, getStub, seeded.objectOids);
28 
29 const treeRes = await workerExports.default.fetch(
30 `https://example.com/${owner}/${repo}/tree?ref=main`
31 );
32 expect(treeRes.status).toBe(200);
33 expect(await treeRes.text()).toContain("hello.txt");
34 
35 const blobRes = await workerExports.default.fetch(
36 `https://example.com/${owner}/${repo}/blob?ref=main&path=${encodeURIComponent("hello.txt")}`
37 );
38 expect(blobRes.status).toBe(200);
39 expect(await blobRes.text()).toContain("hello from packed storage");
40 
41 const rawRes = await workerExports.default.fetch(
42 `https://example.com/${owner}/${repo}/raw?oid=${encodeURIComponent(seeded.blob.oid)}&name=hello.txt`
43 );
44 expect(rawRes.status).toBe(200);
45 expect(await rawRes.text()).toBe("hello from packed storage\n");
46 
47 const rawPathRes = await workerExports.default.fetch(
48 `https://example.com/${owner}/${repo}/rawpath?ref=main&path=${encodeURIComponent("hello.txt")}&name=hello.txt`,
49 {
50 headers: {
51 referer: `https://example.com/${owner}/${repo}/blob?ref=main&path=hello.txt`,
52 },
53 }
54 );
55 expect(rawPathRes.status).toBe(200);
56 expect(await rawPathRes.text()).toBe("hello from packed storage\n");
57 
58 const commitRes = await workerExports.default.fetch(
59 `https://example.com/${owner}/${repo}/commit/${encodeURIComponent(seeded.commit.oid)}`
60 );
61 expect(commitRes.status).toBe(200);
62 expect(await commitRes.text()).toContain("packed commit");
63 
64 const diffRes = await workerExports.default.fetch(
65 `https://example.com/${owner}/${repo}/commit/${encodeURIComponent(seeded.commit.oid)}/diff?path=${encodeURIComponent("hello.txt")}`
66 );
67 expect(diffRes.status).toBe(200);
68 const diff = (await diffRes.json()) as {
69 changeType: string;
70 patch?: string;
71 };
72 expect(diff.changeType).toBe("A");
73 expect(diff.patch).toContain("+++ b/hello.txt");
74 expect(diff.patch).toContain("+hello from packed storage");
75 });
76 
77 it("serves fetch from packs after all loose copies are deleted", async () => {
78 const owner = "o";
79 const repo = uniqueRepoId("pack-first-fetch");
80 await setupRepoForTests(env, owner, repo);
81 const repoId = `${owner}/${repo}`;
82 const id = env.REPO_DO.idFromName(repoId);
83 const getStub = () => env.REPO_DO.get(id);
84 
85 const seeded = await seedPackedRepo(env, repoId, getStub, { mirrorLooseToR2: true });
86 await callStubWithRetry(getStub, (stub) => stub.getActivePackCatalog());
87 await deleteLooseObjectCopies(env, getStub, seeded.objectOids);
88 
89 const res = await workerExports.default.fetch(
90 `https://example.com/${owner}/${repo}/git-upload-pack`,
91 {
92 method: "POST",
93 headers: {
94 "Content-Type": "application/x-git-upload-pack-request",
95 "Git-Protocol": "version=2",
96 },
97 body: toRequestBody(buildFetchBody({ wants: [seeded.commit.oid], done: true })),
98 }
99 );
100 
101 expect(res.status).toBe(200);
102 const bytes = new Uint8Array(await res.arrayBuffer());
103 const packOffset = findBytes(bytes, new TextEncoder().encode("PACK"));
104 expect(packOffset).toBeGreaterThan(-1);
105 expect(new TextDecoder().decode(bytes.subarray(packOffset, packOffset + 4))).toBe("PACK");
106 });
107 
108 it("serves commits and merge fragments from packs after all loose copies are deleted", async () => {
109 const owner = "o";
110 const repo = uniqueRepoId("pack-first-commits");
111 await setupRepoForTests(env, owner, repo);
112 const repoId = `${owner}/${repo}`;
113 const id = env.REPO_DO.idFromName(repoId);
114 const getStub = () => env.REPO_DO.get(id);
115 
116 const author = "You <you@example.com> 0 +0000";
117 const tree = await encodeGitObject("tree", new Uint8Array(0));
118 
119 const baseCommitPayload = new TextEncoder().encode(
120 `tree ${tree.oid}\n` + `author ${author}\n` + `committer ${author}\n\nbase\n`
121 );
122 const baseCommit = await encodeGitObject("commit", baseCommitPayload);
123 
124 const mainCommitPayload = new TextEncoder().encode(
125 `tree ${tree.oid}\n` +
126 `parent ${baseCommit.oid}\n` +
127 `author ${author}\n` +
128 `committer ${author}\n\nmainline\n`
129 );
130 const mainCommit = await encodeGitObject("commit", mainCommitPayload);
131 
132 const sideCommitPayload = new TextEncoder().encode(
133 `tree ${tree.oid}\n` +
134 `parent ${baseCommit.oid}\n` +
135 `author ${author}\n` +
136 `committer ${author}\n\nside branch\n`
137 );
138 const sideCommit = await encodeGitObject("commit", sideCommitPayload);
139 
140 const mergeCommitPayload = new TextEncoder().encode(
141 `tree ${tree.oid}\n` +
142 `parent ${mainCommit.oid}\n` +
143 `parent ${sideCommit.oid}\n` +
144 `author ${author}\n` +
145 `committer ${author}\n\nmerge commit\n`
146 );
147 const mergeCommit = await encodeGitObject("commit", mergeCommitPayload);
148 
149 const packBytes = await buildPack([
150 { type: "tree", payload: new Uint8Array(0) },
151 { type: "commit", payload: baseCommitPayload },
152 { type: "commit", payload: mainCommitPayload },
153 { type: "commit", payload: sideCommitPayload },
154 { type: "commit", payload: mergeCommitPayload },
155 ]);
156 
157 await seedLegacyPackedRepo({
158 env,
159 repoId,
160 getStub,
161 packs: [{ name: "pack-merge.pack", packBytes }],
162 refs: [{ name: "refs/heads/main", oid: mergeCommit.oid }],
163 head: { target: "refs/heads/main", oid: mergeCommit.oid },
164 looseObjects: [tree, baseCommit, mainCommit, sideCommit, mergeCommit],
165 mirrorLooseToR2: true,
166 });
167 
168 await callStubWithRetry(getStub, (stub) => stub.getActivePackCatalog());
169 await deleteLooseObjectCopies(env, getStub, [
170 tree.oid,
171 baseCommit.oid,
172 mainCommit.oid,
173 sideCommit.oid,
174 mergeCommit.oid,
175 ]);
176 
177 const commitsRes = await workerExports.default.fetch(
178 `https://example.com/${owner}/${repo}/commits?ref=main`
179 );
180 expect(commitsRes.status).toBe(200);
181 const commitsHtml = await commitsRes.text();
182 expect(commitsHtml).toContain("merge commit");
183 expect(commitsHtml).toContain("mainline");
184 
185 const fragmentsRes = await workerExports.default.fetch(
186 `https://example.com/${owner}/${repo}/commits/fragments/${encodeURIComponent(mergeCommit.oid)}?limit=10`
187 );
188 expect(fragmentsRes.status).toBe(200);
189 const payload = (await fragmentsRes.json()) as {
190 commits: Array<{ firstLine: string }>;
191 };
192 expect(payload.commits.some((commit) => commit.firstLine === "side branch")).toBe(true);
193 });
194});