File
Blob: test/pack-ref-index.test.ts
| 1 | import { assert, test } from "vitest"; |
| 2 | |
| 3 | import { hexToBytes } from "@/worker/common"; |
| 4 | import { objTypeCode, type GitObjectType } from "@/worker/git/core"; |
| 5 | import type { IdxView } from "@/worker/git/object-store/types"; |
| 6 | import { |
| 7 | getPackRefObjectType, |
| 8 | getPackRefRefsAt, |
| 9 | PackRefsBuilder, |
| 10 | parsePackRefView, |
| 11 | parseTreeClosureRefs, |
| 12 | type PackRefSnapshotEntry, |
| 13 | } from "@/worker/git/pack/refIndex"; |
| 14 | import { allocateEntryTable, type PackEntryTable } from "@/worker/git/pack/indexer"; |
| 15 | import { computeNeededFromPackRefs } from "@/worker/git/operations/fetch/refClosure"; |
| 16 | |
| 17 | const HEADER_BYTES = 60; |
| 18 | |
| 19 | function oid(prefix: string): string { |
| 20 | return prefix.padEnd(40, "0"); |
| 21 | } |
| 22 | |
| 23 | function oidFromNumber(value: number): string { |
| 24 | return value.toString(16).padStart(40, "0"); |
| 25 | } |
| 26 | |
| 27 | function makeIdxView(args: { |
| 28 | packKey: string; |
| 29 | count: number; |
| 30 | packSize: number; |
| 31 | packChecksum: Uint8Array; |
| 32 | idxChecksum: Uint8Array; |
| 33 | oids?: string[]; |
| 34 | }): IdxView { |
| 35 | const rawNames = new Uint8Array(args.count * 20); |
| 36 | const fanout = new Uint32Array(256); |
| 37 | const sortedOids = args.oids || Array.from({ length: args.count }, () => oid("00")); |
| 38 | if (sortedOids.length !== args.count) { |
| 39 | throw new Error("test idx view oid count mismatch"); |
| 40 | } |
| 41 | |
| 42 | const firstByteCounts = new Uint32Array(256); |
| 43 | for (let index = 0; index < sortedOids.length; index++) { |
| 44 | const oidBytes = hexToBytes(sortedOids[index]!); |
| 45 | rawNames.set(oidBytes, index * 20); |
| 46 | firstByteCounts[oidBytes[0] || 0]++; |
| 47 | } |
| 48 | |
| 49 | let cumulative = 0; |
| 50 | for (let index = 0; index < fanout.length; index++) { |
| 51 | cumulative += firstByteCounts[index] || 0; |
| 52 | fanout[index] = cumulative; |
| 53 | } |
| 54 | |
| 55 | return { |
| 56 | packKey: args.packKey, |
| 57 | count: args.count, |
| 58 | fanout, |
| 59 | rawNames, |
| 60 | offsets: new Float64Array(args.count), |
| 61 | nextOffsetByIndex: new Float64Array(args.count), |
| 62 | sortedOffsets: new Float64Array(args.count), |
| 63 | sortedOffsetIndices: new Uint32Array(args.count), |
| 64 | packSize: args.packSize, |
| 65 | packChecksum: args.packChecksum, |
| 66 | idxChecksum: args.idxChecksum, |
| 67 | }; |
| 68 | } |
| 69 | |
| 70 | function recordObject(args: { |
| 71 | table: PackEntryTable; |
| 72 | builder: PackRefsBuilder; |
| 73 | index: number; |
| 74 | oid: string; |
| 75 | type: GitObjectType; |
| 76 | payload: Uint8Array; |
| 77 | }): void { |
| 78 | args.table.oids.set(hexToBytes(args.oid), args.index * 20); |
| 79 | args.table.objectTypes[args.index] = objTypeCode(args.type); |
| 80 | args.builder.recordObject(args.index, args.type, args.payload); |
| 81 | } |
| 82 | |
| 83 | function buildPackRefSnapshotEntry(args: { |
| 84 | packKey: string; |
| 85 | objects: Array<{ |
| 86 | oid: string; |
| 87 | type: GitObjectType; |
| 88 | payload: Uint8Array; |
| 89 | }>; |
| 90 | }): PackRefSnapshotEntry { |
| 91 | const packChecksum = hexToBytes(oid("aa")); |
| 92 | const idxChecksum = hexToBytes(oid("bb")); |
| 93 | const packSize = Math.max(256, args.objects.length * 64); |
| 94 | const table = allocateEntryTable(args.objects.length); |
| 95 | const builder = new PackRefsBuilder(args.objects.length); |
| 96 | |
| 97 | for (let index = 0; index < args.objects.length; index++) { |
| 98 | const object = args.objects[index]!; |
| 99 | recordObject({ |
| 100 | table, |
| 101 | builder, |
| 102 | index, |
| 103 | oid: object.oid, |
| 104 | type: object.type, |
| 105 | payload: object.payload, |
| 106 | }); |
| 107 | } |
| 108 | |
| 109 | const sortedOids = args.objects.map((object) => object.oid).sort(); |
| 110 | const idx = makeIdxView({ |
| 111 | packKey: args.packKey, |
| 112 | count: args.objects.length, |
| 113 | packSize, |
| 114 | packChecksum, |
| 115 | idxChecksum, |
| 116 | oids: sortedOids, |
| 117 | }); |
| 118 | const built = builder.build({ |
| 119 | table, |
| 120 | objectCount: args.objects.length, |
| 121 | packBytes: packSize, |
| 122 | packChecksum, |
| 123 | idxChecksum, |
| 124 | }); |
| 125 | const parsed = parsePackRefView(args.packKey, built.bytes, idx); |
| 126 | if (parsed.type !== "Ready") { |
| 127 | throw new Error(`test sidecar failed to parse: ${parsed.type}`); |
| 128 | } |
| 129 | |
| 130 | return { |
| 131 | packKey: args.packKey, |
| 132 | packBytes: packSize, |
| 133 | idx, |
| 134 | refs: parsed.view, |
| 135 | }; |
| 136 | } |
| 137 | |
| 138 | function treePayload(entries: Array<{ mode: string; name: string; oid: string }>): Uint8Array { |
| 139 | const encoder = new TextEncoder(); |
| 140 | const parts: Uint8Array[] = []; |
| 141 | let total = 0; |
| 142 | for (const entry of entries) { |
| 143 | const header = encoder.encode(`${entry.mode} ${entry.name}`); |
| 144 | const oidBytes = hexToBytes(entry.oid); |
| 145 | parts.push(header, Uint8Array.from([0]), oidBytes); |
| 146 | total += header.byteLength + 1 + oidBytes.byteLength; |
| 147 | } |
| 148 | const out = new Uint8Array(total); |
| 149 | let offset = 0; |
| 150 | for (const part of parts) { |
| 151 | out.set(part, offset); |
| 152 | offset += part.byteLength; |
| 153 | } |
| 154 | return out; |
| 155 | } |
| 156 | |
| 157 | function buildSampleRefIndex() { |
| 158 | const packKey = "do/test/objects/pack/sample.pack"; |
| 159 | const packChecksum = hexToBytes(oid("aa")); |
| 160 | const idxChecksum = hexToBytes(oid("bb")); |
| 161 | const packSize = 1234; |
| 162 | const table = allocateEntryTable(4); |
| 163 | const builder = new PackRefsBuilder(4); |
| 164 | const encoder = new TextEncoder(); |
| 165 | |
| 166 | const blobOid = oid("40"); |
| 167 | const treeOid = oid("20"); |
| 168 | const parentOid = oid("50"); |
| 169 | const tagTargetOid = oid("60"); |
| 170 | const fileOid = oid("70"); |
| 171 | const gitlinkOid = oid("80"); |
| 172 | |
| 173 | recordObject({ |
| 174 | table, |
| 175 | builder, |
| 176 | index: 0, |
| 177 | oid: oid("10"), |
| 178 | type: "commit", |
| 179 | payload: encoder.encode(`tree ${treeOid}\nparent ${parentOid}\n\nmessage\n`), |
| 180 | }); |
| 181 | recordObject({ |
| 182 | table, |
| 183 | builder, |
| 184 | index: 1, |
| 185 | oid: treeOid, |
| 186 | type: "tree", |
| 187 | payload: treePayload([ |
| 188 | { mode: "100644", name: "file.txt", oid: fileOid }, |
| 189 | { mode: "160000", name: "submodule", oid: gitlinkOid }, |
| 190 | ]), |
| 191 | }); |
| 192 | recordObject({ |
| 193 | table, |
| 194 | builder, |
| 195 | index: 2, |
| 196 | oid: oid("30"), |
| 197 | type: "tag", |
| 198 | payload: encoder.encode(`object ${tagTargetOid}\ntype commit\ntag v1\n\nmessage\n`), |
| 199 | }); |
| 200 | recordObject({ |
| 201 | table, |
| 202 | builder, |
| 203 | index: 3, |
| 204 | oid: blobOid, |
| 205 | type: "blob", |
| 206 | payload: encoder.encode("blob\n"), |
| 207 | }); |
| 208 | |
| 209 | const idx = makeIdxView({ |
| 210 | packKey, |
| 211 | count: 4, |
| 212 | packSize, |
| 213 | packChecksum, |
| 214 | idxChecksum, |
| 215 | }); |
| 216 | const built = builder.build({ |
| 217 | table, |
| 218 | objectCount: 4, |
| 219 | packBytes: packSize, |
| 220 | packChecksum, |
| 221 | idxChecksum, |
| 222 | }); |
| 223 | |
| 224 | return { built, idx, refs: { treeOid, parentOid, fileOid, tagTargetOid } }; |
| 225 | } |
| 226 | |
| 227 | test("pack ref sidecar encodes and parses logical object refs", () => { |
| 228 | const { built, idx, refs } = buildSampleRefIndex(); |
| 229 | const parsed = parsePackRefView(idx.packKey, built.bytes, idx); |
| 230 | |
| 231 | assert.strictEqual(parsed.type, "Ready"); |
| 232 | if (parsed.type !== "Ready") return; |
| 233 | |
| 234 | assert.strictEqual(parsed.view.objectCount, 4); |
| 235 | assert.strictEqual(parsed.view.refStartsBytes.byteLength, 5 * 4); |
| 236 | assert.strictEqual(getPackRefObjectType(parsed.view, 0), "commit"); |
| 237 | assert.deepEqual(getPackRefRefsAt(parsed.view, 0), [refs.treeOid, refs.parentOid]); |
| 238 | assert.strictEqual(getPackRefObjectType(parsed.view, 1), "tree"); |
| 239 | assert.deepEqual(getPackRefRefsAt(parsed.view, 1), [refs.fileOid]); |
| 240 | assert.strictEqual(getPackRefObjectType(parsed.view, 2), "tag"); |
| 241 | assert.deepEqual(getPackRefRefsAt(parsed.view, 2), [refs.tagTargetOid]); |
| 242 | assert.strictEqual(getPackRefObjectType(parsed.view, 3), "blob"); |
| 243 | assert.deepEqual(getPackRefRefsAt(parsed.view, 3), []); |
| 244 | }); |
| 245 | |
| 246 | test("tree closure parser excludes gitlinks", () => { |
| 247 | const fileOid = oid("11"); |
| 248 | const gitlinkOid = oid("22"); |
| 249 | const refs = parseTreeClosureRefs( |
| 250 | treePayload([ |
| 251 | { mode: "100644", name: "file.txt", oid: fileOid }, |
| 252 | { mode: "160000", name: "submodule", oid: gitlinkOid }, |
| 253 | { mode: "40000", name: "dir", oid: oid("33") }, |
| 254 | ]) |
| 255 | ); |
| 256 | |
| 257 | assert.deepEqual(refs, [fileOid, oid("33")]); |
| 258 | }); |
| 259 | |
| 260 | test("pack ref sidecar keeps duplicate OID ordering deterministic", () => { |
| 261 | const packKey = "do/test/objects/pack/dupe.pack"; |
| 262 | const packChecksum = hexToBytes(oid("aa")); |
| 263 | const idxChecksum = hexToBytes(oid("bb")); |
| 264 | const table = allocateEntryTable(2); |
| 265 | const builder = new PackRefsBuilder(2); |
| 266 | const encoder = new TextEncoder(); |
| 267 | const duplicateOid = oid("10"); |
| 268 | const treeA = oid("20"); |
| 269 | const treeB = oid("30"); |
| 270 | |
| 271 | recordObject({ |
| 272 | table, |
| 273 | builder, |
| 274 | index: 0, |
| 275 | oid: duplicateOid, |
| 276 | type: "commit", |
| 277 | payload: encoder.encode(`tree ${treeA}\n\nfirst\n`), |
| 278 | }); |
| 279 | recordObject({ |
| 280 | table, |
| 281 | builder, |
| 282 | index: 1, |
| 283 | oid: duplicateOid, |
| 284 | type: "commit", |
| 285 | payload: encoder.encode(`tree ${treeB}\n\nsecond\n`), |
| 286 | }); |
| 287 | |
| 288 | const idx = makeIdxView({ |
| 289 | packKey, |
| 290 | count: 2, |
| 291 | packSize: 200, |
| 292 | packChecksum, |
| 293 | idxChecksum, |
| 294 | }); |
| 295 | const built = builder.build({ |
| 296 | table, |
| 297 | objectCount: 2, |
| 298 | packBytes: 200, |
| 299 | packChecksum, |
| 300 | idxChecksum, |
| 301 | }); |
| 302 | const parsed = parsePackRefView(packKey, built.bytes, idx); |
| 303 | |
| 304 | assert.strictEqual(parsed.type, "Ready"); |
| 305 | if (parsed.type !== "Ready") return; |
| 306 | assert.deepEqual(getPackRefRefsAt(parsed.view, 0), [treeA]); |
| 307 | assert.deepEqual(getPackRefRefsAt(parsed.view, 1), [treeB]); |
| 308 | }); |
| 309 | |
| 310 | test("sidecar closure walks a wide graph with a cursor queue", async () => { |
| 311 | const packKey = "do/test/objects/pack/wide.pack"; |
| 312 | const packChecksum = hexToBytes(oid("aa")); |
| 313 | const idxChecksum = hexToBytes(oid("bb")); |
| 314 | const blobCount = 4096; |
| 315 | const objectCount = blobCount + 2; |
| 316 | const packSize = 200_000; |
| 317 | const table = allocateEntryTable(objectCount); |
| 318 | const builder = new PackRefsBuilder(objectCount); |
| 319 | const encoder = new TextEncoder(); |
| 320 | |
| 321 | const commitOid = oidFromNumber(1); |
| 322 | const treeOid = oidFromNumber(2); |
| 323 | const blobOids = Array.from({ length: blobCount }, (_value, index) => oidFromNumber(index + 3)); |
| 324 | |
| 325 | recordObject({ |
| 326 | table, |
| 327 | builder, |
| 328 | index: 0, |
| 329 | oid: commitOid, |
| 330 | type: "commit", |
| 331 | payload: encoder.encode(`tree ${treeOid}\n\nwide\n`), |
| 332 | }); |
| 333 | recordObject({ |
| 334 | table, |
| 335 | builder, |
| 336 | index: 1, |
| 337 | oid: treeOid, |
| 338 | type: "tree", |
| 339 | payload: treePayload( |
| 340 | blobOids.map((entryOid, index) => ({ |
| 341 | mode: "100644", |
| 342 | name: `file-${index}.txt`, |
| 343 | oid: entryOid, |
| 344 | })) |
| 345 | ), |
| 346 | }); |
| 347 | |
| 348 | const emptyBlob = new Uint8Array(0); |
| 349 | for (let index = 0; index < blobOids.length; index++) { |
| 350 | recordObject({ |
| 351 | table, |
| 352 | builder, |
| 353 | index: index + 2, |
| 354 | oid: blobOids[index]!, |
| 355 | type: "blob", |
| 356 | payload: emptyBlob, |
| 357 | }); |
| 358 | } |
| 359 | |
| 360 | const sortedOids = [commitOid, treeOid, ...blobOids].sort(); |
| 361 | const idx = makeIdxView({ |
| 362 | packKey, |
| 363 | count: objectCount, |
| 364 | packSize, |
| 365 | packChecksum, |
| 366 | idxChecksum, |
| 367 | oids: sortedOids, |
| 368 | }); |
| 369 | const built = builder.build({ |
| 370 | table, |
| 371 | objectCount, |
| 372 | packBytes: packSize, |
| 373 | packChecksum, |
| 374 | idxChecksum, |
| 375 | }); |
| 376 | const parsed = parsePackRefView(packKey, built.bytes, idx); |
| 377 | assert.strictEqual(parsed.type, "Ready"); |
| 378 | if (parsed.type !== "Ready") return; |
| 379 | |
| 380 | const closure = await computeNeededFromPackRefs({ |
| 381 | repoId: "test/wide", |
| 382 | packs: [{ packKey, packBytes: packSize, idx, refs: parsed.view }], |
| 383 | wants: [commitOid, commitOid], |
| 384 | haves: [], |
| 385 | }); |
| 386 | |
| 387 | assert.strictEqual(closure.type, "Ready"); |
| 388 | if (closure.type !== "Ready") return; |
| 389 | assert.strictEqual(closure.neededOids.length, objectCount); |
| 390 | assert.strictEqual(new Set(closure.neededOids).size, objectCount); |
| 391 | assert.isTrue(closure.neededOids.includes(commitOid)); |
| 392 | assert.isTrue(closure.neededOids.includes(treeOid)); |
| 393 | assert.isTrue(closure.neededOids.includes(blobOids[blobOids.length - 1]!)); |
| 394 | }); |
| 395 | |
| 396 | test("sidecar closure queues duplicate wants once", async () => { |
| 397 | const blobOid = oid("11"); |
| 398 | const pack = buildPackRefSnapshotEntry({ |
| 399 | packKey: "do/test/objects/pack/duplicate-wants.pack", |
| 400 | objects: [ |
| 401 | { |
| 402 | oid: blobOid, |
| 403 | type: "blob", |
| 404 | payload: new TextEncoder().encode("content\n"), |
| 405 | }, |
| 406 | ], |
| 407 | }); |
| 408 | |
| 409 | const closure = await computeNeededFromPackRefs({ |
| 410 | repoId: "test/duplicate-wants", |
| 411 | packs: [pack], |
| 412 | wants: [blobOid, blobOid], |
| 413 | haves: [], |
| 414 | }); |
| 415 | |
| 416 | assert.strictEqual(closure.type, "Ready"); |
| 417 | if (closure.type !== "Ready") return; |
| 418 | assert.deepEqual(closure.neededOids, [blobOid]); |
| 419 | assert.strictEqual(closure.stats.indexedObjects, 1); |
| 420 | assert.strictEqual(closure.stats.queued, 1); |
| 421 | assert.strictEqual(closure.stats.seen, 1); |
| 422 | assert.strictEqual(closure.stats.needed, 1); |
| 423 | assert.strictEqual(closure.stats.duplicateQueueSkips, 1); |
| 424 | }); |
| 425 | |
| 426 | test("sidecar closure does not grow the queue for duplicate tree edges", async () => { |
| 427 | const encoder = new TextEncoder(); |
| 428 | const commitOid = oid("10"); |
| 429 | const treeOid = oid("20"); |
| 430 | const blobOid = oid("30"); |
| 431 | const duplicateEntries = 4096; |
| 432 | const pack = buildPackRefSnapshotEntry({ |
| 433 | packKey: "do/test/objects/pack/duplicate-tree-edges.pack", |
| 434 | objects: [ |
| 435 | { |
| 436 | oid: commitOid, |
| 437 | type: "commit", |
| 438 | payload: encoder.encode(`tree ${treeOid}\n\nmessage\n`), |
| 439 | }, |
| 440 | { |
| 441 | oid: treeOid, |
| 442 | type: "tree", |
| 443 | payload: treePayload( |
| 444 | Array.from({ length: duplicateEntries }, (_value, index) => ({ |
| 445 | mode: "100644", |
| 446 | name: `file-${index}.txt`, |
| 447 | oid: blobOid, |
| 448 | })) |
| 449 | ), |
| 450 | }, |
| 451 | { |
| 452 | oid: blobOid, |
| 453 | type: "blob", |
| 454 | payload: encoder.encode("same blob\n"), |
| 455 | }, |
| 456 | ], |
| 457 | }); |
| 458 | |
| 459 | const closure = await computeNeededFromPackRefs({ |
| 460 | repoId: "test/duplicate-tree-edges", |
| 461 | packs: [pack], |
| 462 | wants: [commitOid], |
| 463 | haves: [], |
| 464 | }); |
| 465 | |
| 466 | assert.strictEqual(closure.type, "Ready"); |
| 467 | if (closure.type !== "Ready") return; |
| 468 | assert.strictEqual(closure.neededOids.length, 3); |
| 469 | assert.strictEqual(closure.stats.queued, 3); |
| 470 | assert.strictEqual(closure.stats.seen, 3); |
| 471 | assert.strictEqual(closure.stats.edgeVisits, duplicateEntries + 1); |
| 472 | assert.strictEqual(closure.stats.duplicateQueueSkips, duplicateEntries - 1); |
| 473 | }); |
| 474 | |
| 475 | test("sidecar closure canonicalizes cross-pack duplicate OIDs by snapshot order", async () => { |
| 476 | const encoder = new TextEncoder(); |
| 477 | const duplicateCommitOid = oid("10"); |
| 478 | const newerTreeOid = oid("20"); |
| 479 | const olderTreeOid = oid("30"); |
| 480 | const newerPack = buildPackRefSnapshotEntry({ |
| 481 | packKey: "do/test/objects/pack/newer.pack", |
| 482 | objects: [ |
| 483 | { |
| 484 | oid: duplicateCommitOid, |
| 485 | type: "commit", |
| 486 | payload: encoder.encode(`tree ${newerTreeOid}\n\nnew\n`), |
| 487 | }, |
| 488 | { |
| 489 | oid: newerTreeOid, |
| 490 | type: "tree", |
| 491 | payload: treePayload([]), |
| 492 | }, |
| 493 | ], |
| 494 | }); |
| 495 | const olderPack = buildPackRefSnapshotEntry({ |
| 496 | packKey: "do/test/objects/pack/older.pack", |
| 497 | objects: [ |
| 498 | { |
| 499 | oid: duplicateCommitOid, |
| 500 | type: "commit", |
| 501 | payload: encoder.encode(`tree ${olderTreeOid}\n\nold\n`), |
| 502 | }, |
| 503 | { |
| 504 | oid: olderTreeOid, |
| 505 | type: "tree", |
| 506 | payload: treePayload([]), |
| 507 | }, |
| 508 | ], |
| 509 | }); |
| 510 | |
| 511 | const closure = await computeNeededFromPackRefs({ |
| 512 | repoId: "test/cross-pack-duplicate", |
| 513 | packs: [newerPack, olderPack], |
| 514 | wants: [duplicateCommitOid], |
| 515 | haves: [], |
| 516 | }); |
| 517 | |
| 518 | assert.strictEqual(closure.type, "Ready"); |
| 519 | if (closure.type !== "Ready") return; |
| 520 | assert.isTrue(closure.neededOids.includes(duplicateCommitOid)); |
| 521 | assert.isTrue(closure.neededOids.includes(newerTreeOid)); |
| 522 | assert.isFalse(closure.neededOids.includes(olderTreeOid)); |
| 523 | assert.strictEqual(closure.stats.queued, 2); |
| 524 | assert.strictEqual(closure.stats.needed, 2); |
| 525 | }); |
| 526 | |
| 527 | test("sidecar closure keeps force-push overlap bounded by canonical active objects", async () => { |
| 528 | const encoder = new TextEncoder(); |
| 529 | const commitOid = oid("10"); |
| 530 | const treeOid = oid("20"); |
| 531 | const blobOid = oid("30"); |
| 532 | const newerPack = buildPackRefSnapshotEntry({ |
| 533 | packKey: "do/test/objects/pack/force-newer.pack", |
| 534 | objects: [ |
| 535 | { |
| 536 | oid: commitOid, |
| 537 | type: "commit", |
| 538 | payload: encoder.encode(`tree ${treeOid}\n\nforce push\n`), |
| 539 | }, |
| 540 | { |
| 541 | oid: treeOid, |
| 542 | type: "tree", |
| 543 | payload: treePayload([{ mode: "100644", name: "file.txt", oid: blobOid }]), |
| 544 | }, |
| 545 | { |
| 546 | oid: blobOid, |
| 547 | type: "blob", |
| 548 | payload: encoder.encode("overlap\n"), |
| 549 | }, |
| 550 | ], |
| 551 | }); |
| 552 | const olderPack = buildPackRefSnapshotEntry({ |
| 553 | packKey: "do/test/objects/pack/force-older.pack", |
| 554 | objects: [ |
| 555 | { |
| 556 | oid: blobOid, |
| 557 | type: "blob", |
| 558 | payload: encoder.encode("overlap\n"), |
| 559 | }, |
| 560 | ], |
| 561 | }); |
| 562 | |
| 563 | const closure = await computeNeededFromPackRefs({ |
| 564 | repoId: "test/force-overlap", |
| 565 | packs: [newerPack, olderPack], |
| 566 | wants: [commitOid], |
| 567 | haves: [oid("99")], |
| 568 | }); |
| 569 | |
| 570 | assert.strictEqual(closure.type, "Ready"); |
| 571 | if (closure.type !== "Ready") return; |
| 572 | assert.deepEqual(new Set(closure.neededOids), new Set([commitOid, treeOid, blobOid])); |
| 573 | assert.strictEqual(closure.stats.indexedObjects, 4); |
| 574 | assert.strictEqual(closure.stats.queued, 3); |
| 575 | assert.strictEqual(closure.stats.seen, 3); |
| 576 | assert.strictEqual(closure.stats.needed, 3); |
| 577 | }); |
| 578 | |
| 579 | test("sidecar closure returns a retryable budget result at the missing ref cap", async () => { |
| 580 | const treeOid = oid("20"); |
| 581 | const missingEntries = 1025; |
| 582 | const pack = buildPackRefSnapshotEntry({ |
| 583 | packKey: "do/test/objects/pack/missing-ref-budget.pack", |
| 584 | objects: [ |
| 585 | { |
| 586 | oid: treeOid, |
| 587 | type: "tree", |
| 588 | payload: treePayload( |
| 589 | Array.from({ length: missingEntries }, (_value, index) => ({ |
| 590 | mode: "100644", |
| 591 | name: `missing-${index}.txt`, |
| 592 | oid: oidFromNumber(index + 1), |
| 593 | })) |
| 594 | ), |
| 595 | }, |
| 596 | ], |
| 597 | }); |
| 598 | |
| 599 | const closure = await computeNeededFromPackRefs({ |
| 600 | repoId: "test/missing-ref-budget", |
| 601 | packs: [pack], |
| 602 | wants: [treeOid], |
| 603 | haves: [], |
| 604 | }); |
| 605 | |
| 606 | assert.strictEqual(closure.type, "BudgetExceeded"); |
| 607 | if (closure.type !== "BudgetExceeded") return; |
| 608 | assert.strictEqual(closure.reason, "missing-ref-budget"); |
| 609 | assert.strictEqual(closure.stats.queued, 1); |
| 610 | assert.strictEqual(closure.stats.seen, 1); |
| 611 | assert.strictEqual(closure.stats.missing, 1024); |
| 612 | }); |
| 613 | |
| 614 | test("pack ref sidecar parser rejects invalid artifacts", () => { |
| 615 | const { built, idx } = buildSampleRefIndex(); |
| 616 | const cases: Array<{ name: string; mutate: (bytes: Uint8Array) => Uint8Array; reason: string }> = |
| 617 | [ |
| 618 | { |
| 619 | name: "bad magic", |
| 620 | mutate(bytes) { |
| 621 | bytes[0] = 0; |
| 622 | return bytes; |
| 623 | }, |
| 624 | reason: "bad-magic", |
| 625 | }, |
| 626 | { |
| 627 | name: "bad version", |
| 628 | mutate(bytes) { |
| 629 | new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).setUint32(4, 2, false); |
| 630 | return bytes; |
| 631 | }, |
| 632 | reason: "bad-version", |
| 633 | }, |
| 634 | { |
| 635 | name: "count mismatch", |
| 636 | mutate(bytes) { |
| 637 | new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).setUint32(8, 99, false); |
| 638 | return bytes; |
| 639 | }, |
| 640 | reason: "object-count-mismatch", |
| 641 | }, |
| 642 | { |
| 643 | name: "pack bytes mismatch", |
| 644 | mutate(bytes) { |
| 645 | new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).setUint32(16, 999, false); |
| 646 | return bytes; |
| 647 | }, |
| 648 | reason: "pack-bytes-mismatch", |
| 649 | }, |
| 650 | { |
| 651 | name: "pack checksum mismatch", |
| 652 | mutate(bytes) { |
| 653 | bytes[20] ^= 0xff; |
| 654 | return bytes; |
| 655 | }, |
| 656 | reason: "pack-checksum-mismatch", |
| 657 | }, |
| 658 | { |
| 659 | name: "idx checksum mismatch", |
| 660 | mutate(bytes) { |
| 661 | bytes[40] ^= 0xff; |
| 662 | return bytes; |
| 663 | }, |
| 664 | reason: "idx-checksum-mismatch", |
| 665 | }, |
| 666 | { |
| 667 | name: "invalid type code", |
| 668 | mutate(bytes) { |
| 669 | bytes[HEADER_BYTES] = 9; |
| 670 | return bytes; |
| 671 | }, |
| 672 | reason: "invalid-type-code", |
| 673 | }, |
| 674 | { |
| 675 | name: "truncated type codes", |
| 676 | mutate(bytes) { |
| 677 | return bytes.slice(0, HEADER_BYTES + 1); |
| 678 | }, |
| 679 | reason: "truncated-type-codes", |
| 680 | }, |
| 681 | { |
| 682 | name: "non-monotonic starts", |
| 683 | mutate(bytes) { |
| 684 | const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); |
| 685 | const startsOffset = HEADER_BYTES + idx.count; |
| 686 | dv.setUint32(startsOffset, 2, false); |
| 687 | dv.setUint32(startsOffset + 4, 1, false); |
| 688 | return bytes; |
| 689 | }, |
| 690 | reason: "non-monotonic-ref-starts", |
| 691 | }, |
| 692 | { |
| 693 | name: "bad final offset", |
| 694 | mutate(bytes) { |
| 695 | const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); |
| 696 | const finalStartOffset = HEADER_BYTES + idx.count + idx.count * 4; |
| 697 | dv.setUint32(finalStartOffset, 999, false); |
| 698 | return bytes; |
| 699 | }, |
| 700 | reason: "invalid-final-ref-offset", |
| 701 | }, |
| 702 | ]; |
| 703 | |
| 704 | for (const entry of cases) { |
| 705 | const mutated = entry.mutate(new Uint8Array(built.bytes)); |
| 706 | const parsed = parsePackRefView(idx.packKey, mutated, idx); |
| 707 | assert.strictEqual(parsed.type, "Invalid", entry.name); |
| 708 | if (parsed.type === "Invalid") { |
| 709 | assert.strictEqual(parsed.reason, entry.reason, entry.name); |
| 710 | } |
| 711 | } |
| 712 | }); |