import test from 'brittle'
import { GitProcessQueue } from '../../lib/p2p/sync/git-process-queue.js'
test('constructor: sets defaults', (t) => {
const q = new GitProcessQueue()
t.is(q.maxConcurrent, 5)
t.is(q.timeout, 120000)
q.destroy()
})
test('constructor: accepts custom options', (t) => {
const q = new GitProcessQueue({ maxConcurrent: 3, timeout: 5000 })
t.is(q.maxConcurrent, 3)
t.is(q.timeout, 5000)
q.destroy()
})
test('spawn: resolves on exit code 0', async (t) => {
const q = new GitProcessQueue({ maxConcurrent: 2, timeout: 5000 })
const result = await q.spawn('echo', ['hello'])
t.is(result.code, 0)
t.ok(result.jobId)
q.destroy()
})
test('spawn: rejects on non-zero exit code', async (t) => {
const q = new GitProcessQueue({ maxConcurrent: 2, timeout: 5000 })
try {
await q.spawn('sh', ['-c', 'exit 1'])
t.fail('should have rejected')
} catch (err) {
t.ok(err.message.includes('exit code 1') || err.message.includes('exited with code 1'))
}
q.destroy()
})
test('spawn: respects maxConcurrent', async (t) => {
const q = new GitProcessQueue({ maxConcurrent: 2, timeout: 10000 })
let maxRunning = 0
let running = 0
const hooks = {
onStart () {
running++
if (running > maxRunning) maxRunning = running
},
onExit () {
running--
}
}
// Spawn 5 jobs that each take ~200ms
const promises = []
for (let i = 0; i < 5; i++) {
promises.push(q.spawn('sh', ['-c', 'sleep 0.2'], hooks))
}
await Promise.all(promises)
t.is(maxRunning, 2, 'never exceeded maxConcurrent of 2')
t.is(running, 0, 'all jobs finished')
q.destroy()
})
test('spawn: processes remaining jobs after prior complete', async (t) => {
const q = new GitProcessQueue({ maxConcurrent: 1, timeout: 5000 })
const order = []
const promises = []
for (let i = 0; i < 3; i++) {
const idx = i
promises.push(
q.spawn('echo', [String(idx)], {
onStart () { order.push('start-' + idx) },
onExit () { order.push('exit-' + idx) }
})
)
}
await Promise.all(promises)
// With maxConcurrent=1, jobs run sequentially: start-0, exit-0, start-1, exit-1, start-2, exit-2
t.is(order.length, 6)
t.is(order[0], 'start-0')
t.is(order[1], 'exit-0')
t.is(order[2], 'start-1')
t.is(order[3], 'exit-1')
t.is(order[4], 'start-2')
t.is(order[5], 'exit-2')
q.destroy()
})
test('spawn: timeout kills process and rejects promise', async (t) => {
const q = new GitProcessQueue({ maxConcurrent: 2, timeout: 500 })
try {
await q.spawn('sleep', ['10'])
t.fail('should have rejected')
} catch (err) {
t.ok(err.message.includes('timeout') || err.message.includes('Timeout'))
}
q.destroy()
})
test('cancel: kills active process and rejects promise', async (t) => {
const q = new GitProcessQueue({ maxConcurrent: 2, timeout: 10000 })
let jobId = null
const p = q.spawn('sleep', ['10'], {
onStart (child) {
jobId = q.activeJobs()[0]
}
})
// Wait for process to start
await new Promise(resolve => setTimeout(resolve, 100))
t.ok(jobId, 'got jobId')
const cancelled = q.cancel(jobId)
t.is(cancelled, true, 'cancel returned true')
try {
await p
t.fail('should have rejected')
} catch (err) {
t.ok(err.message.includes('cancel') || err.message.includes('Cancel'))
}
q.destroy()
})
test('cancel: returns false for unknown jobId', (t) => {
const q = new GitProcessQueue({ maxConcurrent: 2, timeout: 5000 })
t.is(q.cancel('nonexistent'), false)
q.destroy()
})
test('activeJobs: returns correct IDs', async (t) => {
const q = new GitProcessQueue({ maxConcurrent: 5, timeout: 10000 })
t.alike(q.activeJobs(), [], 'empty initially')
// Start some long-running jobs
const p1 = q.spawn('sleep', ['10'])
const p2 = q.spawn('sleep', ['10'])
// Wait for them to start
await new Promise(resolve => setTimeout(resolve, 100))
const active = q.activeJobs()
t.is(active.length, 2, 'two active jobs')
// Clean up
q.destroy()
await Promise.allSettled([p1, p2])
})
test('destroy: kills all active and rejects all queued', async (t) => {
const q = new GitProcessQueue({ maxConcurrent: 1, timeout: 10000 })
// One active (maxConcurrent=1) and two queued
const p1 = q.spawn('sleep', ['10'])
const p2 = q.spawn('sleep', ['10'])
const p3 = q.spawn('sleep', ['10'])
// Wait for first to start
await new Promise(resolve => setTimeout(resolve, 100))
q.destroy()
const results = await Promise.allSettled([p1, p2, p3])
for (const r of results) {
t.is(r.status, 'rejected', 'all promises rejected')
}
})
test('spawn: hooks are optional', async (t) => {
const q = new GitProcessQueue({ maxConcurrent: 2, timeout: 5000 })
// No hooks — should not throw
const result = await q.spawn('echo', ['no-hooks'])
t.is(result.code, 0)
q.destroy()
})
test('spawn: onStart receives child process', async (t) => {
const q = new GitProcessQueue({ maxConcurrent: 2, timeout: 5000 })
let receivedChild = null
await q.spawn('echo', ['hello'], {
onStart (child) {
receivedChild = child
}
})
t.ok(receivedChild, 'onStart received child process')
t.ok(typeof receivedChild.kill === 'function', 'child has kill method')
q.destroy()
})