// 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')
})