/**
* Unit tests for PearDeployer and deploy-hook
*
* Tests deployer construction, state management, and deploy-hook logic.
* IPC calls are not tested here (require running Pear sidecar).
*/
import test from 'brittle'
import { PearDeployer } from '../../lib/ci/pear-deployer.js'
import { getDeployLog } from '../../lib/ci/deploy-hook.js'
// ========================================
// PearDeployer construction
// ========================================
test('PearDeployer - constructs with defaults', async (t) => {
const deployer = new PearDeployer()
t.ok(deployer, 'deployer created')
t.is(deployer._ready, false, 'not ready until .ready() called')
t.ok(deployer._seedStreams instanceof Map, 'seed streams map exists')
t.is(deployer._seedStreams.size, 0, 'no seed streams initially')
})
test('PearDeployer - constructs with custom socket path', async (t) => {
const deployer = new PearDeployer({ socketPath: '/tmp/test.sock' })
t.is(deployer._socketPath, '/tmp/test.sock', 'custom socket path set')
})
test('PearDeployer - default socket path is ~/.config/pear/pear.sock', async (t) => {
const deployer = new PearDeployer()
t.ok(deployer._socketPath.endsWith('/pear.sock'), 'socket path ends with pear.sock')
t.ok(deployer._socketPath.includes('.config/pear'), 'socket path includes .config/pear')
})
// ========================================
// PearDeployer state management
// ========================================
test('PearDeployer - isSeeding returns false for unknown channel', async (t) => {
const deployer = new PearDeployer()
t.is(deployer.isSeeding('nonexistent'), false)
})
test('PearDeployer - getSeededChannels returns empty array initially', async (t) => {
const deployer = new PearDeployer()
const channels = deployer.getSeededChannels()
t.ok(Array.isArray(channels), 'returns array')
t.is(channels.length, 0, 'empty initially')
})
test('PearDeployer - unseed is safe on unknown channel', async (t) => {
const deployer = new PearDeployer()
await deployer.unseed('nonexistent') // should not throw
t.pass('unseed on unknown channel is safe')
})
test('PearDeployer - close is safe when not ready', async (t) => {
const deployer = new PearDeployer()
await deployer.close() // should not throw
t.pass('close when not ready is safe')
t.is(deployer._ready, false, 'still not ready')
})
// ========================================
// PearDeployer guards
// ========================================
test('PearDeployer - stage throws when not ready', async (t) => {
const deployer = new PearDeployer()
try {
await deployer.stage('/tmp/test', 'test-channel')
t.fail('should have thrown')
} catch (err) {
t.is(err.message, 'PearDeployer not ready')
}
})
test('PearDeployer - release throws when not ready', async (t) => {
const deployer = new PearDeployer()
try {
await deployer.release('test-channel')
t.fail('should have thrown')
} catch (err) {
t.is(err.message, 'PearDeployer not ready')
}
})
test('PearDeployer - seed throws when not ready', async (t) => {
const deployer = new PearDeployer()
try {
await deployer.seed('test-channel')
t.fail('should have thrown')
} catch (err) {
t.is(err.message, 'PearDeployer not ready')
}
})
test('PearDeployer - info throws when not ready', async (t) => {
const deployer = new PearDeployer()
try {
await deployer.info('test-channel')
t.fail('should have thrown')
} catch (err) {
t.is(err.message, 'PearDeployer not ready')
}
})
// ========================================
// Deploy config schema
// ========================================
test('deploy config - has expected shape', async (t) => {
const config = {
enabled: true,
channel: 'test-app',
autoRelease: true,
autoSeed: true,
configuredAt: Date.now(),
lastDeploy: null,
lastVersion: null,
lastKey: null
}
t.is(typeof config.enabled, 'boolean')
t.is(typeof config.channel, 'string')
t.is(typeof config.autoRelease, 'boolean')
t.is(typeof config.autoSeed, 'boolean')
t.is(typeof config.configuredAt, 'number')
t.is(config.lastDeploy, null)
t.is(config.lastVersion, null)
t.is(config.lastKey, null)
})
// ========================================
// Deploy log with mock registry
// ========================================
test('getDeployLog - returns empty array for no entries', async (t) => {
const mockServer = {
registry: {
createReadStream () {
return {
async * [Symbol.asyncIterator] () {
// No entries
}
}
}
}
}
const entries = await getDeployLog(mockServer, 'test-repo')
t.ok(Array.isArray(entries), 'returns array')
t.is(entries.length, 0, 'empty when no entries')
})
test('getDeployLog - returns entries in reverse order', async (t) => {
const mockEntries = [
{ value: { repoName: 'test', version: 3, timestamp: 3000 } },
{ value: { repoName: 'test', version: 2, timestamp: 2000 } },
{ value: { repoName: 'test', version: 1, timestamp: 1000 } }
]
const mockServer = {
registry: {
createReadStream (opts) {
t.ok(opts.reverse, 'requests reverse order')
t.ok(opts.gte.startsWith('deploy-log:test-repo:'), 'correct key prefix')
return {
async * [Symbol.asyncIterator] () {
for (const entry of mockEntries) {
yield entry
}
}
}
}
}
}
const entries = await getDeployLog(mockServer, 'test-repo')
t.is(entries.length, 3, 'returns all entries')
t.is(entries[0].version, 3, 'first entry is newest')
t.is(entries[2].version, 1, 'last entry is oldest')
})
test('getDeployLog - respects limit option', async (t) => {
const mockServer = {
registry: {
createReadStream (opts) {
t.is(opts.limit, 5, 'passes limit to stream')
return {
async * [Symbol.asyncIterator] () {
// empty
}
}
}
}
}
await getDeployLog(mockServer, 'test-repo', { limit: 5 })
t.pass('limit passed correctly')
})