import test from 'brittle'
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import { ServerSync } from '../../lib/p2p/federation/server-sync.js'
const tmpDir = path.join(os.tmpdir(), 'pear-git-test-server-sync-' + Date.now())
test('setup: create temp dir', async (t) => {
await fs.promises.mkdir(tmpDir, { recursive: true })
t.pass('temp dir created')
})
test('constructor: sets defaults', (t) => {
const mockRegistry = {}
const sync = new ServerSync({
reposDir: '/tmp/repos',
registry: mockRegistry
})
t.is(sync.reposDir, '/tmp/repos')
t.is(sync.registry, mockRegistry)
t.is(sync.interval, 30 * 60 * 1000, 'default interval is 30 minutes')
t.is(sync._timer, null)
t.is(sync._running, false)
})
test('constructor: accepts custom interval', (t) => {
const sync = new ServerSync({
reposDir: '/tmp/repos',
registry: {},
interval: 30000
})
t.is(sync.interval, 30000)
})
test('_listRepos: returns repo names from .git directories', async (t) => {
const reposDir = path.join(tmpDir, 'repos')
await fs.promises.mkdir(reposDir, { recursive: true })
// Create some .git dirs and a non-.git dir
await fs.promises.mkdir(path.join(reposDir, 'my-app.git'), { recursive: true })
await fs.promises.mkdir(path.join(reposDir, 'another-repo.git'), { recursive: true })
await fs.promises.mkdir(path.join(reposDir, 'not-a-repo'), { recursive: true })
const sync = new ServerSync({
reposDir,
registry: {}
})
const repos = await sync._listRepos()
t.is(repos.length, 2)
t.ok(repos.includes('my-app'))
t.ok(repos.includes('another-repo'))
t.absent(repos.includes('not-a-repo'))
})
test('_listRepos: returns empty array for missing directory', async (t) => {
const sync = new ServerSync({
reposDir: path.join(tmpDir, 'nonexistent'),
registry: {}
})
const repos = await sync._listRepos()
t.alike(repos, [])
})
test('_buildSyncList: returns active peers with httpUrl', async (t) => {
// Create a mock registry with createReadStream
const entries = [
{
key: 'federation:abc123',
value: { serverKey: 'abc12345', status: 'active', httpUrl: 'http://peer1:5005/git' }
},
{
key: 'federation:def456',
value: { serverKey: 'def45678', status: 'inactive', httpUrl: 'http://peer2:5005/git' }
},
{
key: 'federation:ghi789',
value: { serverKey: 'ghi78901', status: 'active', httpUrl: 'http://peer3:5005/git' }
},
{
key: 'federation:jkl012',
value: { serverKey: 'jkl01234', status: 'active' } // no httpUrl
}
]
const mockRegistry = {
createReadStream () {
return (async function * () {
for (const entry of entries) {
yield entry
}
})()
}
}
const sync = new ServerSync({
reposDir: tmpDir,
registry: mockRegistry
})
const peers = await sync._buildSyncList()
t.is(peers.length, 3, 'active peers with serverKey or httpUrl')
t.is(peers[0].serverKey, 'abc12345')
t.is(peers[1].serverKey, 'ghi78901')
t.is(peers[2].serverKey, 'jkl01234')
})
test('_buildSyncList: returns empty for no entries', async (t) => {
const mockRegistry = {
createReadStream () {
return (async function * () {})()
}
}
const sync = new ServerSync({
reposDir: tmpDir,
registry: mockRegistry
})
const peers = await sync._buildSyncList()
t.alike(peers, [])
})
test('_buildSyncList: handles string JSON values', async (t) => {
const mockRegistry = {
createReadStream () {
return (async function * () {
yield {
key: 'federation:str1',
value: JSON.stringify({ serverKey: 'str12345', status: 'active', httpUrl: 'http://str-peer:5005' })
}
})()
}
}
const sync = new ServerSync({
reposDir: tmpDir,
registry: mockRegistry
})
const peers = await sync._buildSyncList()
t.is(peers.length, 1)
t.is(peers[0].serverKey, 'str12345')
})
test('start/stop: manages timer lifecycle', (t) => {
const sync = new ServerSync({
reposDir: tmpDir,
registry: {},
interval: 60000
})
t.is(sync._timer, null)
sync.start()
t.not(sync._timer, null, 'timer set after start')
// Starting again should be a no-op
const timer1 = sync._timer
sync.start()
t.is(sync._timer, timer1, 'start is idempotent')
sync.stop()
t.is(sync._timer, null, 'timer cleared after stop')
})
test('cleanup: remove temp dir', async (t) => {
await fs.promises.rm(tmpDir, { recursive: true, force: true })
t.pass('cleaned up')
})