Skip to content
File

Blob: test/pack-indexer.connectivity.commits.worker.test.ts

typescript360 lines
1import { describe, expect, it } from "vitest";
2import { env } from "cloudflare:workers";
3import { buildPack, makeCommit, makeTree } from "./util/git-pack";
4import { createTestCacheContext } from "./util/pack-first";
5import { uniqueRepoId } from "./util/test-helpers";
6import {
7 makeActiveCatalogRow,
8 makeLimiter,
9 makeTracingLimiter,
10 packIndexerLog as log,
11} from "./util/pack-indexer.helpers";
12 
13import {
14 runPackConnectivityCheck,
15 scanPack,
16 resolveDeltasAndWriteIdx,
17} from "@/worker/git/pack/indexer";
18import { computeOid } from "@/worker/git/core/objects";
19import { findObject } from "@/worker/git/object-store/store";
20 
21type ConnectivityStatus = { ref: string; ok: boolean; msg?: string };
22 
23async function indexPack(packKey: string, packBytes: Uint8Array, repoId: string) {
24 await env.REPO_BUCKET.put(packKey, packBytes);
25 const head = await env.REPO_BUCKET.head(packKey);
26 
27 const scanResult = await scanPack({
28 env,
29 packKey,
30 packSize: head!.size,
31 limiter: makeLimiter(),
32 countSubrequest: () => {},
33 log,
34 });
35 
36 const resolveResult = await resolveDeltasAndWriteIdx({
37 env,
38 packKey,
39 packSize: head!.size,
40 limiter: makeLimiter(),
41 countSubrequest: () => {},
42 log,
43 scanResult,
44 repoId,
45 });
46 
47 return {
48 head: head!,
49 scanResult,
50 resolveResult,
51 };
52}
53 
54describe("runPackConnectivityCheck commit reachability", () => {
55 it("accepts a valid push with commit pointing to an existing tree", async () => {
56 const tree = await makeTree();
57 const commit = await makeCommit(tree.oid, "valid commit");
58 
59 const packBytes = await buildPack([
60 { type: "tree", payload: tree.payload },
61 { type: "commit", payload: commit.payload },
62 ]);
63 
64 const packKey = "test/conn-valid.pack";
65 await env.REPO_BUCKET.put(packKey, packBytes);
66 const head = await env.REPO_BUCKET.head(packKey);
67 
68 const scanResult = await scanPack({
69 env,
70 packKey,
71 packSize: head!.size,
72 limiter: makeLimiter(),
73 countSubrequest: () => {},
74 log,
75 });
76 
77 const repoId = uniqueRepoId();
78 const resolveResult = await resolveDeltasAndWriteIdx({
79 env,
80 packKey,
81 packSize: head!.size,
82 limiter: makeLimiter(),
83 countSubrequest: () => {},
84 log,
85 scanResult,
86 repoId,
87 });
88 
89 const statuses: ConnectivityStatus[] = [{ ref: "refs/heads/main", ok: true }];
90 const cacheCtx = createTestCacheContext("http://localhost/test");
91 
92 await runPackConnectivityCheck({
93 env,
94 repoId,
95 newPackKey: packKey,
96 newIdxView: resolveResult.idxView,
97 newPackSize: head!.size,
98 activeCatalog: [],
99 commands: [{ oldOid: "0".repeat(40), newOid: commit.oid, ref: "refs/heads/main" }],
100 statuses,
101 log,
102 cacheCtx,
103 });
104 
105 expect(statuses[0].ok).toBe(true);
106 });
107 
108 it("rejects a push with commit pointing to a missing tree", async () => {
109 const fakeTreeOid = "a".repeat(40);
110 const commitPayload = new TextEncoder().encode(
111 `tree ${fakeTreeOid}\nauthor You <you@example.com> 0 +0000\ncommitter You <you@example.com> 0 +0000\n\nbad commit\n`
112 );
113 const commitOid = await computeOid("commit", commitPayload);
114 
115 const packBytes = await buildPack([{ type: "commit", payload: commitPayload }]);
116 
117 const packKey = "test/conn-missing-tree.pack";
118 await env.REPO_BUCKET.put(packKey, packBytes);
119 const head = await env.REPO_BUCKET.head(packKey);
120 
121 const scanResult = await scanPack({
122 env,
123 packKey,
124 packSize: head!.size,
125 limiter: makeLimiter(),
126 countSubrequest: () => {},
127 log,
128 });
129 
130 const repoId = uniqueRepoId();
131 const resolveResult = await resolveDeltasAndWriteIdx({
132 env,
133 packKey,
134 packSize: head!.size,
135 limiter: makeLimiter(),
136 countSubrequest: () => {},
137 log,
138 scanResult,
139 repoId,
140 });
141 
142 const statuses: ConnectivityStatus[] = [{ ref: "refs/heads/main", ok: true }];
143 const cacheCtx = createTestCacheContext("http://localhost/test");
144 
145 await runPackConnectivityCheck({
146 env,
147 repoId,
148 newPackKey: packKey,
149 newIdxView: resolveResult.idxView,
150 newPackSize: head!.size,
151 activeCatalog: [],
152 commands: [{ oldOid: "0".repeat(40), newOid: commitOid, ref: "refs/heads/main" }],
153 statuses,
154 log,
155 cacheCtx,
156 });
157 
158 expect(statuses[0].ok).toBe(false);
159 expect(statuses[0].msg).toBe("missing-objects");
160 });
161 
162 it("rejects a push when a commit parent is missing", async () => {
163 const tree = await makeTree();
164 const missingParentOid = "b".repeat(40);
165 const commitPayload = new TextEncoder().encode(
166 `tree ${tree.oid}\nparent ${missingParentOid}\nauthor You <you@example.com> 0 +0000\ncommitter You <you@example.com> 0 +0000\n\nmissing parent\n`
167 );
168 const commitOid = await computeOid("commit", commitPayload);
169 
170 const packBytes = await buildPack([
171 { type: "tree", payload: tree.payload },
172 { type: "commit", payload: commitPayload },
173 ]);
174 
175 const packKey = "test/conn-missing-parent.pack";
176 await env.REPO_BUCKET.put(packKey, packBytes);
177 const head = await env.REPO_BUCKET.head(packKey);
178 
179 const scanResult = await scanPack({
180 env,
181 packKey,
182 packSize: head!.size,
183 limiter: makeLimiter(),
184 countSubrequest: () => {},
185 log,
186 });
187 
188 const repoId = uniqueRepoId();
189 const resolveResult = await resolveDeltasAndWriteIdx({
190 env,
191 packKey,
192 packSize: head!.size,
193 limiter: makeLimiter(),
194 countSubrequest: () => {},
195 log,
196 scanResult,
197 repoId,
198 });
199 
200 const statuses: ConnectivityStatus[] = [{ ref: "refs/heads/main", ok: true }];
201 const cacheCtx = createTestCacheContext("http://localhost/test");
202 
203 await runPackConnectivityCheck({
204 env,
205 repoId,
206 newPackKey: packKey,
207 newIdxView: resolveResult.idxView,
208 newPackSize: head!.size,
209 activeCatalog: [],
210 commands: [{ oldOid: "0".repeat(40), newOid: commitOid, ref: "refs/heads/main" }],
211 statuses,
212 log,
213 cacheCtx,
214 });
215 
216 expect(statuses[0].ok).toBe(false);
217 expect(statuses[0].msg).toBe("missing-objects");
218 });
219 
220 it("uses the caller cache context for limiter and subrequest accounting", async () => {
221 const tree = await makeTree();
222 const commit = await makeCommit(tree.oid, "budgeted connectivity commit");
223 
224 const packBytes = await buildPack([
225 { type: "tree", payload: tree.payload },
226 { type: "commit", payload: commit.payload },
227 ]);
228 
229 const packKey = "test/conn-budget.pack";
230 await env.REPO_BUCKET.put(packKey, packBytes);
231 const head = await env.REPO_BUCKET.head(packKey);
232 
233 const scanResult = await scanPack({
234 env,
235 packKey,
236 packSize: head!.size,
237 limiter: makeLimiter(),
238 countSubrequest: () => {},
239 log,
240 });
241 
242 const repoId = uniqueRepoId();
243 const resolveResult = await resolveDeltasAndWriteIdx({
244 env,
245 packKey,
246 packSize: head!.size,
247 limiter: makeLimiter(),
248 countSubrequest: () => {},
249 log,
250 scanResult,
251 repoId,
252 });
253 
254 const statuses: ConnectivityStatus[] = [{ ref: "refs/heads/main", ok: true }];
255 const labels: string[] = [];
256 const cacheCtx = createTestCacheContext("http://localhost/test", 10);
257 cacheCtx.memo = {
258 ...(cacheCtx.memo || {}),
259 limiter: makeTracingLimiter(labels),
260 };
261 
262 await runPackConnectivityCheck({
263 env,
264 repoId,
265 newPackKey: packKey,
266 newIdxView: resolveResult.idxView,
267 newPackSize: head!.size,
268 activeCatalog: [],
269 commands: [{ oldOid: "0".repeat(40), newOid: commit.oid, ref: "refs/heads/main" }],
270 statuses,
271 log,
272 cacheCtx,
273 });
274 
275 expect(statuses[0].ok).toBe(true);
276 expect(labels).toContain("r2:get-range");
277 expect(cacheCtx.memo?.subreqBudget).toBeLessThan(10);
278 });
279 
280 it("accepts a new commit whose tree and parent exist only in the active catalog snapshot", async () => {
281 const repoId = uniqueRepoId();
282 
283 const baseTree = await makeTree();
284 const parentCommit = await makeCommit(baseTree.oid, "base commit");
285 const basePackKey = "test/conn-active-base.pack";
286 const basePack = await buildPack([
287 { type: "tree", payload: baseTree.payload },
288 { type: "commit", payload: parentCommit.payload },
289 ]);
290 const baseIndexed = await indexPack(basePackKey, basePack, repoId);
291 
292 const childCommitPayload = new TextEncoder().encode(
293 `tree ${baseTree.oid}\nparent ${parentCommit.oid}\nauthor You <you@example.com> 0 +0000\ncommitter You <you@example.com> 0 +0000\n\nchild commit\n`
294 );
295 const childCommitOid = await computeOid("commit", childCommitPayload);
296 const newPackKey = "test/conn-active-new.pack";
297 const newPack = await buildPack([{ type: "commit", payload: childCommitPayload }]);
298 const newIndexed = await indexPack(newPackKey, newPack, repoId);
299 
300 const statuses: ConnectivityStatus[] = [{ ref: "refs/heads/main", ok: true }];
301 const cacheCtx = createTestCacheContext("http://localhost/test");
302 
303 await runPackConnectivityCheck({
304 env,
305 repoId,
306 newPackKey,
307 newIdxView: newIndexed.resolveResult.idxView,
308 newPackSize: newIndexed.head.size,
309 activeCatalog: [
310 makeActiveCatalogRow({
311 packKey: basePackKey,
312 packBytes: baseIndexed.head.size,
313 objectCount: baseIndexed.scanResult.objectCount,
314 idxBytes: baseIndexed.resolveResult.idxBytes,
315 }),
316 ],
317 commands: [{ oldOid: "0".repeat(40), newOid: childCommitOid, ref: "refs/heads/main" }],
318 statuses,
319 log,
320 cacheCtx,
321 });
322 
323 expect(statuses[0].ok).toBe(true);
324 });
325 
326 it("does not leak the staged pack back into the caller cache memo", async () => {
327 const tree = await makeTree();
328 const commit = await makeCommit(tree.oid, "scoped commit");
329 const repoId = uniqueRepoId();
330 
331 const packKey = "test/conn-scoped-memo.pack";
332 const packBytes = await buildPack([
333 { type: "tree", payload: tree.payload },
334 { type: "commit", payload: commit.payload },
335 ]);
336 const indexed = await indexPack(packKey, packBytes, repoId);
337 
338 const statuses: ConnectivityStatus[] = [{ ref: "refs/heads/main", ok: true }];
339 const cacheCtx = createTestCacheContext("http://localhost/test", 20);
340 
341 await runPackConnectivityCheck({
342 env,
343 repoId,
344 newPackKey: packKey,
345 newIdxView: indexed.resolveResult.idxView,
346 newPackSize: indexed.head.size,
347 activeCatalog: [],
348 commands: [{ oldOid: "0".repeat(40), newOid: commit.oid, ref: "refs/heads/main" }],
349 statuses,
350 log,
351 cacheCtx,
352 });
353 
354 expect(statuses[0].ok).toBe(true);
355 expect(cacheCtx.memo?.packCatalog).toBeUndefined();
356 expect(cacheCtx.memo?.idxViews?.has(packKey)).toBeFalsy();
357 await expect(findObject(env, repoId, commit.oid, cacheCtx)).resolves.toBeUndefined();
358 });
359});