OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / gip-fetch-cleanup.test.js

111 lines · javascript ref:
/**
 * 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 })
})