Skip to content
File

Blob: test/pack-first-fetch-and-ui.worker.test.ts

typescript193 lines
1import { describe, expect, it } from "vitest";
2import { env, exports as workerExports } from "cloudflare:workers";
3import type { RepoStateSchema } from "@/worker/do/repo/repoState";
4 
5import { buildPack, callStubWithRetry, runDOWithRetry, uniqueRepoId } from "./util/test-helpers";
6import { setupRepoForTests } from "./util/repoSeed";
7import { asTypedStorage, objKey } from "@/worker/do/repo/repoState";
8import { encodeGitObject } from "@/worker/git/core";
9import { doPrefix, r2LooseKey, r2PackKey } from "@/worker/keys";
10import { buildTreePayload } from "./util/packed-repo";
11import { indexTestPack } from "./util/test-indexer";
12import { getDb, upsertPackCatalogRow } from "@/worker/do/repo/db";
13import { buildFetchBody, findBytes } from "./util/fetch-protocol";
14 
15async function seedPackedOnlyRepo(repoId: string) {
16 const id = env.REPO_DO.idFromName(repoId);
17 const getStub = () => env.REPO_DO.get(id);
18 const encoder = new TextEncoder();
19 const author = "You <you@example.com> 0 +0000";
20 
21 const blob1Payload = encoder.encode("hello from v1\n");
22 const blob1 = await encodeGitObject("blob", blob1Payload);
23 const tree1Payload = buildTreePayload([{ mode: "100644", name: "hello.txt", oid: blob1.oid }]);
24 const tree1 = await encodeGitObject("tree", tree1Payload);
25 const commit1Payload = encoder.encode(
26 `tree ${tree1.oid}\n` + `author ${author}\n` + `committer ${author}\n\n` + `first commit\n`
27 );
28 const commit1 = await encodeGitObject("commit", commit1Payload);
29 
30 const blob2Payload = encoder.encode("hello from v2\n");
31 const blob2 = await encodeGitObject("blob", blob2Payload);
32 const symlinkPayload = encoder.encode("AGENTS.md");
33 const symlinkBlob = await encodeGitObject("blob", symlinkPayload);
34 const tree2Payload = buildTreePayload([
35 { mode: "100644", name: "hello.txt", oid: blob2.oid },
36 { mode: "120000", name: "CLAUDE.md", oid: symlinkBlob.oid },
37 ]);
38 const tree2 = await encodeGitObject("tree", tree2Payload);
39 const commit2Payload = encoder.encode(
40 `tree ${tree2.oid}\n` +
41 `parent ${commit1.oid}\n` +
42 `author ${author}\n` +
43 `committer ${author}\n\n` +
44 `second commit\n`
45 );
46 const commit2 = await encodeGitObject("commit", commit2Payload);
47 
48 const looseObjects = [blob1, tree1, commit1, blob2, symlinkBlob, tree2, commit2];
49 const packs = [
50 {
51 name: "pack-receive-0002.pack",
52 packBytes: await buildPack([
53 { type: "blob", payload: blob2Payload },
54 { type: "blob", payload: symlinkPayload },
55 { type: "tree", payload: tree2Payload },
56 { type: "commit", payload: commit2Payload },
57 ]),
58 },
59 {
60 name: "pack-receive-0001.pack",
61 packBytes: await buildPack([
62 { type: "blob", payload: blob1Payload },
63 { type: "tree", payload: tree1Payload },
64 { type: "commit", payload: commit1Payload },
65 ]),
66 },
67 ];
68 
69 await runDOWithRetry(getStub, async (_instance, state) => {
70 const prefix = doPrefix(state.id.toString());
71 const store = asTypedStorage<RepoStateSchema>(state.storage);
72 const db = getDb(state.storage);
73 
74 for (const obj of looseObjects) {
75 await store.put(objKey(obj.oid), obj.zdata);
76 await env.REPO_BUCKET.put(r2LooseKey(prefix, obj.oid), obj.zdata);
77 }
78 
79 let nextSeq = (await store.get("nextPackSeq")) || 1;
80 for (const pack of packs) {
81 const packKey = r2PackKey(prefix, pack.name);
82 await env.REPO_BUCKET.put(packKey, pack.packBytes);
83 
84 const resolveResult = await indexTestPack(env, packKey, pack.packBytes.byteLength);
85 const seq = nextSeq++;
86 await upsertPackCatalogRow(db, {
87 packKey,
88 kind: "receive",
89 state: "active",
90 tier: 0,
91 seqLo: seq,
92 seqHi: seq,
93 objectCount: resolveResult.objectCount,
94 packBytes: pack.packBytes.byteLength,
95 idxBytes: resolveResult.idxBytes,
96 createdAt: Date.now(),
97 supersededBy: null,
98 });
99 }
100 
101 const packsetVersion = ((await store.get("packsetVersion")) || 0) + 1;
102 await store.put("packsetVersion", packsetVersion);
103 await store.put("nextPackSeq", nextSeq);
104 await store.put("refs", [{ name: "refs/heads/main", oid: commit2.oid }]);
105 await store.put("head", { target: "refs/heads/main", oid: commit2.oid });
106 });
107 
108 await callStubWithRetry(getStub, (stub) => stub.getActivePackCatalog());
109 
110 await runDOWithRetry(getStub, async (_instance, state) => {
111 const prefix = doPrefix(state.id.toString());
112 for (const obj of looseObjects) {
113 await state.storage.delete(objKey(obj.oid));
114 await env.REPO_BUCKET.delete(r2LooseKey(prefix, obj.oid));
115 }
116 });
117 
118 return {
119 commit1,
120 commit2,
121 blob2,
122 getStub,
123 };
124}
125 
126describe("pack-first fetch and UI", () => {
127 it("serves UI routes from packs after loose copies are deleted", async () => {
128 const owner = "o";
129 const repo = uniqueRepoId("pack-first-ui");
130 await setupRepoForTests(env, owner, repo);
131 const repoId = `${owner}/${repo}`;
132 const { commit2, blob2 } = await seedPackedOnlyRepo(repoId);
133 
134 const rawRes = await workerExports.default.fetch(
135 `https://example.com/${owner}/${repo}/raw?oid=${encodeURIComponent(blob2.oid)}&name=hello.txt`
136 );
137 expect(rawRes.status).toBe(200);
138 expect(await rawRes.text()).toBe("hello from v2\n");
139 
140 const treeRes = await workerExports.default.fetch(
141 `https://example.com/${owner}/${repo}/tree?ref=main`
142 );
143 expect(treeRes.status).toBe(200);
144 const treeHtml = await treeRes.text();
145 expect(treeHtml).toContain("hello.txt");
146 expect(treeHtml).toContain("CLAUDE.md");
147 expect(treeHtml).toContain("lucide-file-symlink");
148 
149 const blobRes = await workerExports.default.fetch(
150 `https://example.com/${owner}/${repo}/blob?ref=main&path=${encodeURIComponent("hello.txt")}`
151 );
152 expect(blobRes.status).toBe(200);
153 expect(await blobRes.text()).toContain("hello from v2");
154 
155 const commitRes = await workerExports.default.fetch(
156 `https://example.com/${owner}/${repo}/commit/${commit2.oid}`
157 );
158 expect(commitRes.status).toBe(200);
159 expect(await commitRes.text()).toContain("second commit");
160 
161 const diffRes = await workerExports.default.fetch(
162 `https://example.com/${owner}/${repo}/commit/${commit2.oid}/diff?path=${encodeURIComponent("hello.txt")}`
163 );
164 expect(diffRes.status).toBe(200);
165 expect(await diffRes.text()).toContain("hello.txt");
166 });
167 
168 it("streams fetches from multiple active packs after loose copies are deleted", async () => {
169 const owner = "o";
170 const repo = uniqueRepoId("pack-first-fetch");
171 await setupRepoForTests(env, owner, repo);
172 const repoId = `${owner}/${repo}`;
173 const { commit2 } = await seedPackedOnlyRepo(repoId);
174 
175 const res = await workerExports.default.fetch(
176 `https://example.com/${owner}/${repo}/git-upload-pack`,
177 {
178 method: "POST",
179 headers: {
180 "Content-Type": "application/x-git-upload-pack-request",
181 "Git-Protocol": "version=2",
182 },
183 body: buildFetchBody({ wants: [commit2.oid], done: true, agent: false }),
184 } as RequestInit
185 );
186 
187 expect(res.status).toBe(200);
188 const bytes = new Uint8Array(await res.arrayBuffer());
189 const packStart = findBytes(bytes, new TextEncoder().encode("PACK"));
190 expect(packStart).toBeGreaterThan(-1);
191 });
192});