/**
* Tree publish pass tests — P.10 Phase 0.5b
*
* Verifies that DeployServer populates the hyper-overlay tree index
* alongside the file index during _scan, and that manifest.rootTreeHash
* is set.
*/
import test from 'brittle'
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import crypto from 'hypercore-crypto'
import { DeployServer } from '../index.js'
function mktemp (prefix) {
const dir = path.join(os.tmpdir(), 'tree-publish-' + prefix + '-' + Date.now() + '-' + crypto.randomBytes(3).toString('hex'))
fs.mkdirSync(dir, { recursive: true })
return dir
}
function makeAppDir () {
const dir = mktemp('app')
// Mimic a small app layout
fs.writeFileSync(path.join(dir, 'package.json'), '{"name":"test-app","version":"1.0.0"}')
fs.writeFileSync(path.join(dir, 'index.js'), 'console.log("hello")')
fs.mkdirSync(path.join(dir, 'lib'))
fs.writeFileSync(path.join(dir, 'lib', 'util.js'), 'module.exports = {}')
fs.writeFileSync(path.join(dir, 'lib', 'helper.js'), 'exports.x = 1')
fs.mkdirSync(path.join(dir, 'assets'))
fs.writeFileSync(path.join(dir, 'assets', 'icon.png'), Buffer.alloc(256, 0xaa))
return dir
}
// ── Tests ─────────────────────────────────────────────────────
test('DeployServer scan populates rootTreeHash', async (t) => {
const dir = makeAppDir()
const server = new DeployServer(dir)
await server.ready()
t.ok(server.manifest, 'manifest present')
t.ok(server.manifest.rootTreeHash, 'rootTreeHash set on manifest')
t.is(server.manifest.rootTreeHash.length, 64, 'hash is 32-byte hex (64 chars)')
await server.close()
fs.rmSync(dir, { recursive: true, force: true })
})
test('DeployServer stores tree entries in fileIndex', async (t) => {
const dir = makeAppDir()
const server = new DeployServer(dir)
await server.ready()
const rootHash = server.manifest.rootTreeHash
const root = await server._fileIndex.getTree(rootHash)
t.ok(root, 'root tree stored')
t.ok(Array.isArray(root.entries), 'root has entries list')
const names = root.entries.map(e => e.name).sort()
t.alike(names, ['assets', 'index.js', 'lib', 'package.json'], 'top-level entries present')
// lib/ subtree should also be stored
const libEntry = root.entries.find(e => e.name === 'lib')
t.ok(libEntry, 'lib entry in root')
t.is(libEntry.kind, 1, 'lib is directory (kind=1)')
const libTree = await server._fileIndex.getTree(libEntry.childHash)
t.ok(libTree, 'lib subtree stored by hash')
t.is(libTree.entries.length, 2, 'lib has 2 files')
await server.close()
fs.rmSync(dir, { recursive: true, force: true })
})
test('DeployServer treepath index lets you look up tree by path', async (t) => {
const dir = makeAppDir()
const server = new DeployServer(dir)
await server.ready()
const rootHashViaPath = await server._fileIndex.getTreePath('/')
t.is(rootHashViaPath, server.manifest.rootTreeHash, 'treepath / = rootTreeHash')
const libHashViaPath = await server._fileIndex.getTreePath('lib')
t.ok(libHashViaPath, 'lib path mapped to a hash')
const libTree = await server._fileIndex.getTree(libHashViaPath)
t.ok(libTree)
t.is(libTree.entries.length, 2)
await server.close()
fs.rmSync(dir, { recursive: true, force: true })
})
test('DeployServer — identical content produces identical rootTreeHash across rescans', async (t) => {
const dir = makeAppDir()
const server1 = new DeployServer(dir)
await server1.ready()
const h1 = server1.manifest.rootTreeHash
await server1.close()
// Touch all files to change mtimes — hash must NOT change
for (const f of ['index.js', 'package.json', 'lib/util.js', 'lib/helper.js', 'assets/icon.png']) {
const p = path.join(dir, f)
const past = new Date(Date.now() - 1000000)
fs.utimesSync(p, past, past)
}
const server2 = new DeployServer(dir)
await server2.ready()
const h2 = server2.manifest.rootTreeHash
await server2.close()
t.is(h1, h2, 'rootTreeHash is mtime-insensitive')
fs.rmSync(dir, { recursive: true, force: true })
})
test('DeployServer — changed content changes rootTreeHash', async (t) => {
const dir = makeAppDir()
const server1 = new DeployServer(dir)
await server1.ready()
const h1 = server1.manifest.rootTreeHash
await server1.close()
// Change one file's content
fs.writeFileSync(path.join(dir, 'index.js'), 'console.log("changed")')
const server2 = new DeployServer(dir)
await server2.ready()
const h2 = server2.manifest.rootTreeHash
await server2.close()
t.unlike(h1, h2, 'content change propagates to root hash')
fs.rmSync(dir, { recursive: true, force: true })
})
test('DeployServer — emits trees-indexed event with count', async (t) => {
const dir = makeAppDir()
const server = new DeployServer(dir)
let treesIndexed = null
server.on('trees-indexed', (count, rootHash) => {
treesIndexed = { count, rootHash }
})
await server.ready()
t.ok(treesIndexed, 'trees-indexed fired')
t.ok(treesIndexed.count > 0, 'non-zero tree count')
t.is(treesIndexed.rootHash, server.manifest.rootTreeHash)
await server.close()
fs.rmSync(dir, { recursive: true, force: true })
})
test('DeployServer — empty directory produces a valid rootTreeHash', async (t) => {
const dir = mktemp('empty')
const server = new DeployServer(dir)
await server.ready()
t.ok(server.manifest.rootTreeHash, 'empty dir has a tree hash')
t.is(server.manifest.files.length, 0)
const root = await server._fileIndex.getTree(server.manifest.rootTreeHash)
t.ok(root)
t.is(root.entries.length, 0)
await server.close()
fs.rmSync(dir, { recursive: true, force: true })
})