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 Hyperswarm from 'hyperswarm'
import { DeployServer, DeployClient } from '../index.js'
// NOTE: the standalone `lib/scanner` module (buildManifest / diffManifest /
// hashFile) was removed — scanning now lives inside DeployServer (walkDir +
// FileIndex). Its unit tests were dropped here; equivalent coverage lives in
// tree-publish / incremental-rescan / manifest-boot.
const tmpdir = os.tmpdir()
function mktemp (prefix) {
const dir = path.join(tmpdir, 'hyper-deploy-test-' + prefix + '-' + crypto.randomBytes(6).toString('hex'))
fs.mkdirSync(dir, { recursive: true })
return dir
}
// ── DeployServer ────────────────────────────────────────────
test('DeployServer lifecycle', async (t) => {
const dir = mktemp('srv')
fs.writeFileSync(path.join(dir, 'test.txt'), 'hello')
const server = new DeployServer(dir)
await server.ready()
t.ok(server.manifest, 'has manifest')
t.is(server.manifest.files.length, 1)
t.ok(server.topicHex, 'has topic')
t.is(server.connections, 0)
await server.close()
fs.rmSync(dir, { recursive: true })
})
test('DeployServer rescan', async (t) => {
const dir = mktemp('rescan')
fs.writeFileSync(path.join(dir, 'a.txt'), 'first')
const server = new DeployServer(dir)
await server.ready()
t.is(server.manifest.files.length, 1)
fs.writeFileSync(path.join(dir, 'b.txt'), 'second')
await server.rescan()
t.is(server.manifest.files.length, 2)
await server.close()
fs.rmSync(dir, { recursive: true })
})
test('DeployServer rejects missing directory', async (t) => {
const server = new DeployServer('/nonexistent/path/xyz')
try {
await server.ready()
t.fail('should have thrown')
} catch (err) {
t.ok(err.message.includes('not found'), err.message)
}
})
// ── DeployClient ────────────────────────────────────────────
test('DeployClient timeout on no connection', async (t) => {
const client = new DeployClient(crypto.randomBytes(32), mktemp('timeout'), {
timeout: 500
})
await client.ready()
try {
await client.pull()
t.fail('should have timed out')
} catch (err) {
t.ok(err.message.includes('timeout'), err.message)
}
await client.close()
})
// ── Integration ─────────────────────────────────────────────
// These drive a real two-swarm Hyperswarm DHT transfer. They need a local DHT
// bootstrap (@hyperswarm/testnet) to run deterministically in-process — which
// isn't a dependency here — so they're skipped rather than left to flake on the
// public DHT. Networked transfer is covered by the cross-machine e2e harness.
test.skip('DeployServer + DeployClient full transfer', async (t) => {
t.timeout(15000)
const srcDir = mktemp('int-src')
for (let i = 0; i < 5; i++) {
fs.writeFileSync(path.join(srcDir, 'file_' + i + '.txt'), 'content ' + i)
}
const dstDir = mktemp('int-dst')
// Separate swarms — same process can't self-connect on one swarm.
const topic = crypto.randomBytes(32)
const swarmA = new Hyperswarm()
const swarmB = new Hyperswarm()
const server = new DeployServer(srcDir, { topic, swarm: swarmA })
const client = new DeployClient(topic, dstDir, { swarm: swarmB, timeout: 10000 })
await server.ready()
await client.ready()
const result = await client.pull()
t.is(result.filesTransferred, 5, '5 files transferred')
t.is(result.filesSkipped, 0, 'none skipped')
t.ok(result.bytesReceived > 0, 'received bytes')
t.is(result.errors.length, 0, 'no errors')
for (let i = 0; i < 5; i++) {
const src = fs.readFileSync(path.join(srcDir, 'file_' + i + '.txt'), 'utf8')
const dst = fs.readFileSync(path.join(dstDir, 'file_' + i + '.txt'), 'utf8')
t.is(dst, src, 'file_' + i + '.txt matches')
}
await client.close()
await server.close()
await swarmA.destroy()
await swarmB.destroy()
fs.rmSync(srcDir, { recursive: true })
fs.rmSync(dstDir, { recursive: true })
})
test.skip('DeployClient file-level dedup', async (t) => {
t.timeout(15000)
const srcDir = mktemp('dup-src')
fs.writeFileSync(path.join(srcDir, 'a.txt'), 'unchanged')
fs.writeFileSync(path.join(srcDir, 'b.txt'), 'will change')
const dstDir = mktemp('dup-dst')
fs.writeFileSync(path.join(dstDir, 'a.txt'), 'unchanged')
const topic = crypto.randomBytes(32)
const swarmA = new Hyperswarm()
const swarmB = new Hyperswarm()
const server = new DeployServer(srcDir, { topic, swarm: swarmA })
const client = new DeployClient(topic, dstDir, { swarm: swarmB, timeout: 10000 })
await server.ready()
await client.ready()
const result = await client.pull()
t.is(result.filesTransferred, 1, '1 transferred')
t.is(result.filesSkipped, 1, '1 skipped')
await client.close()
await server.close()
await swarmA.destroy()
await swarmB.destroy()
fs.rmSync(srcDir, { recursive: true })
fs.rmSync(dstDir, { recursive: true })
})