OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-deploy / test / tree-publish.test.js

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