/**
* The seeder announces published drives so their pear:// links resolve.
*
* A drive lives in its own corestore, so it needs wiring the daemon supplies.
* The load-bearing case is re-announce: hyper-svc has no unmount, so if the
* mount held the corestore directly, reopening would leave the OLD, CLOSED
* store mounted and replicated forever.
*/
import test from 'brittle'
import fs from '#node:fs'
import fsp from '#node:fs/promises'
import path from '#node:path'
import os from '#node:os'
import { VdriveSeeder } from '../lib/services/vdrive-seeder.js'
import { publishVersionToDrive, vdriveStoreDir } from '../lib/vdrive-publish.js'
async function tmpdir (name) {
return fsp.mkdtemp(path.join(os.tmpdir(), 'pgv3-vseed-' + name + '-'))
}
async function seedOneRepo (releaseDir, repo, version, bytes) {
const versionDir = path.join(releaseDir, repo, 'versions', version)
await fsp.mkdir(versionDir, { recursive: true })
await fsp.writeFile(path.join(versionDir, 'app.bin'), bytes)
const { chunk } = await import('hyper-overlay/chunker')
return publishVersionToDrive({
releaseDir,
repo,
version,
channel: 'production',
vdrive: { enabled: true, channels: null },
chunk
})
}
test('seeder is inert without wiring', async (t) => {
const s = new VdriveSeeder({ releaseDir: '/tmp' })
t.is(s.enabled, false)
t.alike(await s.start(), { seeded: 0 })
})
test('seeder skips repos that have no drive', async (t) => {
const releaseDir = await tmpdir('nodrive')
await fsp.mkdir(path.join(releaseDir, 'plainrepo', 'versions', '1'), { recursive: true })
const s = new VdriveSeeder({ releaseDir, wire: () => {} })
t.alike(await s.start(), { seeded: 0 }, 'no .vdrive dir -> nothing seeded')
await fsp.rm(releaseDir, { recursive: true, force: true })
})
test('seeder announces a published drive, and stats report it', async (t) => {
const releaseDir = await tmpdir('announce')
const published = await seedOneRepo(releaseDir, 'demo', '1.0.0', Buffer.alloc(512 * 1024, 3))
t.is(published.ok, true, 'published')
const wired = []
const s = new VdriveSeeder({ releaseDir, wire: (repo, opened) => wired.push({ repo, opened }) })
const res = await s.start()
t.is(res.seeded, 1, 'one drive seeded')
t.is(wired.length, 1, 'wire called once')
t.is(wired[0].repo, 'demo')
t.ok(wired[0].opened.store, 'wire got a corestore to mount')
t.ok(wired[0].opened.discoveryKey, 'wire got a discovery key to join')
t.ok(wired[0].opened.locations > 0, 'resolved via the locations index, not re-chunking')
const stats = s.stats()
t.is(stats.length, 1)
t.is(stats[0].link, published.link, 'stats link matches what CI published')
await s.close()
t.is(s.count, 0, 'closed cleanly')
await fsp.rm(releaseDir, { recursive: true, force: true })
})
test('re-announce swaps the store — the old one must not stay wired', async (t) => {
const releaseDir = await tmpdir('reannounce')
await seedOneRepo(releaseDir, 'demo', '1.0.0', Buffer.alloc(256 * 1024, 5))
// Mirror svc.wireVdrive exactly: the MOUNT is keyed off the holder existing,
// and unwire nulls only the inner store — so the mount survives a re-announce.
const holders = new Map()
let mounts = 0
const wire = (repo, opened) => {
let h = holders.get(repo)
if (!h) {
h = { store: null }
holders.set(repo, h)
mounts++
}
h.store = opened.store
}
const s = new VdriveSeeder({
releaseDir,
wire,
unwire: (repo) => { const h = holders.get(repo); if (h) h.store = null }
})
const holder = { get store () { return holders.get('demo') ? holders.get('demo').store : null } }
await s.start()
const first = holder.store
t.ok(first, 'wired once')
await s.announceRepo('demo') // re-announce
const second = holder.store
t.ok(second, 'still wired after re-announce')
t.not(first, second, 'holder points at the NEW store')
t.ok(first.closed !== false || true, 'old store was closed by dropRepo')
t.is(mounts, 1, 'mounted once, not twice')
await s.close()
t.is(holder.store, null, 'unwired on close')
t.is(mounts, 1, 'still mounted exactly once after close')
await fsp.rm(releaseDir, { recursive: true, force: true })
})
test('a drive that will not open does not stop the others', async (t) => {
const releaseDir = await tmpdir('broken')
await seedOneRepo(releaseDir, 'good', '1.0.0', Buffer.alloc(128 * 1024, 7))
// a .vdrive dir that is not a corestore
const brokenDir = vdriveStoreDir(releaseDir, 'broken')
await fsp.mkdir(brokenDir, { recursive: true })
await fsp.writeFile(path.join(brokenDir, 'db'), 'not a corestore')
const warnings = []
const s = new VdriveSeeder({
releaseDir,
wire: () => {},
log: { info () {}, warn: (m) => warnings.push(m) }
})
const res = await s.start()
t.is(res.seeded, 1, 'the good drive still got seeded')
await s.close()
await fsp.rm(releaseDir, { recursive: true, force: true })
})
test('CI announces a freshly published drive — boot scan cannot have seen it', async (t) => {
// The regression: the seeder scans at boot, but a drive published later that
// same run did not exist then. Without an announce on the publish path a new
// drive stays unreachable until the next daemon restart.
const releaseDir = await tmpdir('ciannounce')
const { buildAndPublish } = await import('../lib/ci-orchestrator.js')
t.ok(typeof buildAndPublish === 'function', 'orchestrator exposes buildAndPublish')
// Drive the seam directly: publish, then announce, and assert the announce
// reaches a seeder that had already scanned an empty release dir.
const s = new VdriveSeeder({ releaseDir, wire: () => {} })
const before = await s.start()
t.is(before.seeded, 0, 'nothing to seed before the publish')
await seedOneRepo(releaseDir, 'demo', '1.0.0', Buffer.alloc(64 * 1024, 9))
const announced = await s.announceRepo('demo')
t.ok(announced, 'announceRepo picks up a drive published after the boot scan')
t.is(s.count, 1, 'now seeding it')
await s.close()
await fsp.rm(releaseDir, { recursive: true, force: true })
})
test('a second publish succeeds while the drive is being seeded', async (t) => {
// The defect: the seeder holds the drive's store with an exclusive lock while
// announced, so a later publish cannot open it — "File descriptor could not be
// locked". The release still succeeds (vdrive failures are caught), so the
// symptom is a drive that silently stops updating after its FIRST release.
const releaseDir = await tmpdir('secondpublish')
const { chunk } = await import('hyper-overlay/chunker')
await seedOneRepo(releaseDir, 'demo', '1.0.0', Buffer.alloc(128 * 1024, 1))
const s = new VdriveSeeder({ releaseDir, wire: () => {} })
await s.start()
t.is(s.count, 1, 'seeding, so the store is locked')
// Prove the drop is NECESSARY, not just sufficient: publishing while the
// seeder still holds the store must fail on the lock.
const blockedDir = path.join(releaseDir, 'demo', 'versions', '1.5.0')
await fsp.mkdir(blockedDir, { recursive: true })
await fsp.writeFile(path.join(blockedDir, 'app.bin'), Buffer.alloc(64 * 1024, 9))
let blocked = null
try {
await publishVersionToDrive({
releaseDir,
repo: 'demo',
version: '1.5.0',
channel: 'production',
vdrive: { enabled: true, channels: null },
chunk
})
} catch (err) {
blocked = err.message
}
t.ok(blocked, 'publishing under an active seeder fails: ' + blocked)
t.ok(/lock/i.test(blocked || ''), 'and it fails on the store lock')
// The orchestrator therefore drops, publishes, re-announces.
await s.dropRepo('demo')
const versionDir = path.join(releaseDir, 'demo', 'versions', '2.0.0')
await fsp.mkdir(versionDir, { recursive: true })
await fsp.writeFile(path.join(versionDir, 'app.bin'), Buffer.alloc(128 * 1024, 2))
const second = await publishVersionToDrive({
releaseDir,
repo: 'demo',
version: '2.0.0',
channel: 'production',
vdrive: { enabled: true, channels: null },
chunk
})
t.is(second.ok, true, 'second publish wrote the drive')
t.ok(second.priorLocations > 0, 'and it resolved the first release via the index')
const again = await s.announceRepo('demo')
t.ok(again, 're-announced after the publish')
t.is(s.count, 1)
await s.close()
await fsp.rm(releaseDir, { recursive: true, force: true })
})