OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-deploy / test / manifest-boot.test.js

136 lines · javascript ref:
// Fast manifest-diff boot path: reconstruct the manifest from the persistent
// fileIndex + reconcile only the hash-dir delta, instead of re-walking every
// file. Verifies: first boot full-scans + stamps schema; warm boot fast-paths
// with an identical manifest (incl. symlinks served from the durable blob);
// a NEW hash-dir is picked up; a removed (GC'd) hash-dir is pruned.

import test from 'brittle'
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import Corestore from 'corestore'
import { DeployServer } from '../index.js'

function tmp (label) {
  return fs.mkdtempSync(path.join(os.tmpdir(), 'hd-mboot-' + label + '-'))
}

function writeDir (depDir, hashName, files, symlinks = {}) {
  const d = path.join(depDir, hashName)
  fs.mkdirSync(d, { recursive: true })
  for (const [rel, content] of Object.entries(files)) {
    const full = path.join(d, rel)
    fs.mkdirSync(path.dirname(full), { recursive: true })
    fs.writeFileSync(full, content)
  }
  for (const [rel, target] of Object.entries(symlinks)) {
    fs.symlinkSync(target, path.join(d, rel))
  }
}

function stubSwarm () {
  return {
    on () {},
    join () { return { flushed: async () => {}, destroy: async () => {} } },
    async destroy () {}
  }
}

// One "boot": fresh Corestore on the SAME dir (simulates a restart), stub swarm.
async function boot (depDir, storeDir, symlinkDir) {
  const store = new Corestore(storeDir)
  const events = []
  const server = new DeployServer(depDir, {
    corestore: store,
    swarm: stubSwarm(),
    enableTrees: true,
    symlinkBlobsDir: symlinkDir
  })
  server.on('scan-cache-stats', (s) => events.push(s))
  await server.ready()
  const manifest = {
    files: server.manifest.files.map((f) => ({ path: f.path, diskPath: f.diskPath, hash: f.hash })),
    count: server.manifest.files.length
  }
  await server.close()
  await store.close()
  return { manifest, stats: events[events.length - 1] || {} }
}

function byPath (manifest) {
  const m = new Map()
  for (const f of manifest.files) m.set(f.path, f)
  return m
}

test('first boot full-scans, warm boot fast-paths with identical manifest (+ symlink)', async (t) => {
  const depDir = tmp('dep')
  const storeDir = tmp('store')
  const symlinkDir = tmp('sym')
  t.teardown(() => { for (const d of [depDir, storeDir, symlinkDir]) try { fs.rmSync(d, { recursive: true, force: true }) } catch {} })

  writeDir(depDir, 'aaaa1111', { 'index.js': 'module.exports = 1\n', 'pkg/x.txt': 'hello' }, { 'lnk': './index.js' })
  writeDir(depDir, 'bbbb2222', { 'main.js': 'console.log(2)\n' })

  const b1 = await boot(depDir, storeDir, symlinkDir)
  t.absent(b1.stats.fastPath, 'first boot is a full scan (no schema yet)')
  t.ok(b1.manifest.count >= 4, 'full scan indexed all files (incl. symlink)')

  const b2 = await boot(depDir, storeDir, symlinkDir)
  t.ok(b2.stats.fastPath, 'warm boot took the fast manifest-diff path')
  t.is(b2.stats.newDirs, 0, 'no new dirs')
  t.is(b2.stats.prunedFiles, 0, 'nothing pruned')
  t.is(b2.manifest.count, b1.manifest.count, 'fast-path manifest has the same file count')

  // Same set of paths, and same content-hash per path.
  const m1 = byPath(b1.manifest)
  const m2 = byPath(b2.manifest)
  t.is(m2.size, m1.size, 'same path set')
  let hashesMatch = true
  for (const [p, f1] of m1) { if (!m2.has(p) || m2.get(p).hash !== f1.hash) hashesMatch = false }
  t.ok(hashesMatch, 'every path resolves to the same content hash on the fast path')

  // Symlink served from the DURABLE blob dir, with correct target bytes.
  const link = m2.get('aaaa1111/lnk')
  t.ok(link, 'symlink entry present in fast-path manifest')
  t.ok(link.diskPath.startsWith(symlinkDir), 'symlink diskPath points at the durable blob dir')
  t.is(fs.readFileSync(link.diskPath, 'utf8'), './index.js', 'symlink blob serves the correct target string')

  // Regular file diskPath still points into the tree and serves real content.
  const reg = m2.get('aaaa1111/index.js')
  t.is(fs.readFileSync(reg.diskPath, 'utf8'), 'module.exports = 1\n', 'regular file served correctly on fast path')
})

test('fast path picks up a NEW hash-dir', async (t) => {
  const depDir = tmp('dep2'); const storeDir = tmp('store2'); const symlinkDir = tmp('sym2')
  t.teardown(() => { for (const d of [depDir, storeDir, symlinkDir]) try { fs.rmSync(d, { recursive: true, force: true }) } catch {} })

  writeDir(depDir, 'aaaa1111', { 'a.js': 'a' })
  await boot(depDir, storeDir, symlinkDir) // full scan + stamp

  writeDir(depDir, 'cccc3333', { 'c.js': 'c', 'sub/d.js': 'd' }) // NEW dir
  const b = await boot(depDir, storeDir, symlinkDir)
  t.ok(b.stats.fastPath, 'fast path')
  t.is(b.stats.newDirs, 1, 'one new dir indexed incrementally')
  const m = byPath(b.manifest)
  t.ok(m.has('cccc3333/c.js') && m.has('cccc3333/sub/d.js'), 'new dir files present in manifest')
  t.ok(m.has('aaaa1111/a.js'), 'old dir still present')
})

test('fast path prunes a removed (GC\'d) hash-dir', async (t) => {
  const depDir = tmp('dep3'); const storeDir = tmp('store3'); const symlinkDir = tmp('sym3')
  t.teardown(() => { for (const d of [depDir, storeDir, symlinkDir]) try { fs.rmSync(d, { recursive: true, force: true }) } catch {} })

  writeDir(depDir, 'aaaa1111', { 'a.js': 'a' })
  writeDir(depDir, 'bbbb2222', { 'b.js': 'b', 'b2.js': 'bb' })
  await boot(depDir, storeDir, symlinkDir) // full scan + stamp

  fs.rmSync(path.join(depDir, 'bbbb2222'), { recursive: true, force: true }) // simulate GC
  const b = await boot(depDir, storeDir, symlinkDir)
  t.ok(b.stats.fastPath, 'fast path')
  t.is(b.stats.prunedFiles, 2, 'the removed dir\'s two files were pruned from the index')
  const m = byPath(b.manifest)
  t.absent(m.has('bbbb2222/b.js'), 'removed dir no longer in manifest')
  t.ok(m.has('aaaa1111/a.js'), 'surviving dir still served')
})