/**
* A fetched bundle is a transfer artifact — once unbundled, drop it.
*
* Regression: the fetcher applied bundles and left every one of them in the
* overlay's destDir. On stortron that reached 17 GB, 736 files, back to June.
*
* The dangerous half is what must NOT be deleted: hyper-overlay short-circuits
* a hash it already holds and returns where those bytes really live, so an
* unconditional unlink would destroy this node's own producer spool.
*/
import test from 'brittle'
import fs from '#node:fs'
import fsp from '#node:fs/promises'
import path from '#node:path'
import os from '#node:os'
import { discardFetchedBundle, materializeObject } from '../lib/git-overlay-fetch.js'
async function tmpdir (name) {
return fsp.mkdtemp(path.join(os.tmpdir(), 'pgv3-gipclean-' + name + '-'))
}
test('a throwaway fetch is deleted', async (t) => {
const dir = await tmpdir('throwaway')
const destPath = path.join(dir, 'blob-abc123-1700000000000-xyz789')
fs.writeFileSync(destPath, 'bundle bytes')
t.is(discardFetchedBundle({ destPath }, null), true)
t.absent(fs.existsSync(destPath), 'file removed')
})
test('a local short-circuit is NEVER deleted', async (t) => {
const dir = await tmpdir('local')
// what the producer spool looks like: canonical bytes, natural name
const destPath = path.join(dir, 'deadbeef.bundle')
fs.writeFileSync(destPath, 'canonical bytes')
t.is(discardFetchedBundle({ destPath, local: true }, null), false)
t.ok(fs.existsSync(destPath), 'canonical file kept')
})
test('a mirror-spool (content-addressed) path is NEVER deleted', async (t) => {
const dir = await tmpdir('spool')
const destPath = path.join(dir, 'a'.repeat(64))
fs.writeFileSync(destPath, 'durably re-served bytes')
t.is(discardFetchedBundle({ destPath }, null), false)
t.ok(fs.existsSync(destPath), 'spool file kept')
})
test('cleanup failure never breaks a successful fetch', async (t) => {
const warnings = []
const res = { destPath: '/nonexistent/blob-abc-1-2' }
t.is(discardFetchedBundle(res, { warn: (m) => warnings.push(m) }), false)
t.is(warnings.length, 1, 'warned rather than threw')
})
test('materializeObject drops each bundle after applying it', async (t) => {
const dir = await tmpdir('materialize')
const repoPath = path.join(dir, 'repo.git')
fs.mkdirSync(repoPath)
// two-link chain: base then head
const fetched = []
function bundleFile (name) {
const p = path.join(dir, 'blob-' + name + '-1700000000000-aaaaaa')
fs.writeFileSync(p, '# v2 git bundle\n')
fetched.push(p)
return p
}
const applied = []
const present = new Set()
const tracker = {
async getBundle (oid) {
if (oid === 'head') return { contentHash: 'hash-head', baseOid: 'base' }
if (oid === 'base') return { contentHash: 'hash-base', baseOid: null }
return null
}
}
const overlay = {
async fetchFile (hash) {
return { destPath: bundleFile(hash) }
}
}
// stand in for git: cat-file -e answers from `present`, unbundle records
const git = (cmd, args) => {
if (args.includes('cat-file')) {
const oid = args[args.indexOf('-e') + 1].replace('^{object}', '')
if (!present.has(oid)) throw new Error('missing')
return Buffer.from('')
}
if (args.includes('unbundle')) {
applied.push(args[args.length - 1])
// applying the head bundle satisfies the target
if (applied.length === 2) present.add('head')
return Buffer.from('abc refs/heads/main\n')
}
return Buffer.from('')
}
await materializeObject({ oid: 'head', tracker, overlay, repoPath, git })
t.is(applied.length, 2, 'applied base then head')
t.is(fetched.length, 2, 'fetched two bundles')
for (const f of fetched) t.absent(fs.existsSync(f), 'fetched bundle removed: ' + path.basename(f))
await fsp.rm(dir, { recursive: true, force: true })
})