import { test } from '@japa/runner' import db from '@adonisjs/lucid/services/db' import testUtils from '@adonisjs/core/services/test_utils' import BatchStore from '#services/batch_store' import { decidePar2Completeness, par2Complete } from '#services/par2_completeness' import { RangeIndexer } from '#lib/edge/index' import { DEFAULT_RULES } from '#lib/rules/index' import type { Overview } from '#lib/nntp/index' const GROUP = 'alt.binaries.test' /** A file posted without an [i/N] counter: the release gets no file_total. */ function file(name: string, parts: number, at: number): Overview[] { return Array.from({ length: parts }, (_, i) => [at + i, { 'subject': `"${name}" yEnc (${i + 1}/${parts})`, 'from': 'p
', 'date': 'Mon, 05 Oct 2026 10:00:00 +0000', 'message-id': `<${name}-${i + 1}@test>`, ':bytes': '700000', 'xref': `news.example ${GROUP}:${at + i}`, }] as Overview) } async function store(start: number, overviews: Overview[]) { const g = (await db.from('groups').where('name', GROUP).first()) ?? (await db.table('groups').insert({ name: GROUP, priority: 1 }).returning('*'))[0] const [n] = await db.table('nodes').insert({ name: `n${start}` }).returning('id') const [r] = await db.table('ranges').insert({ group_id: g.id, start_id: start, end_id: start + 999, next_id: start, status: 'leased', node_id: n.id }).returning('id') const indexer = new RangeIndexer({ group: GROUP, start, end: start + 999, rules: { ...DEFAULT_RULES, samplePercent: 0, droppedSignatures: [], priorities: { [GROUP]: 1 } } }) indexer.add(overviews, start + 1000) indexer.finish() await BatchStore.store(Number(r.id), Number(n.id), indexer.take()) } const setFile = (name: string, length: number, hash16k = name.padEnd(32, '0').slice(0, 32)) => ({ name, length, hash16k }) test.group('par2 completeness', (group) => { group.each.setup(() => testUtils.db().withGlobalTransaction()) test('every described file by name or by hash; a renamed upload by its files and bytes', ({ assert }) => { const set = [setFile('a.mkv', 1000), setFile('a.nfo', 10)] const files = [{ name: 'a.mkv', size: 1100 }, { name: 'a.nfo', size: 12 }, { name: 'a.par2', size: 50 }] assert.isTrue(par2Complete(set, files, [])) assert.isFalse(par2Complete(set, files.slice(0, 1), [])) // The nfo renamed, but its probe hashed it. assert.isTrue(par2Complete(set, [files[0], { name: 'x.nfo', size: 12 }], [{ hash16k: set[1].hash16k, size: 10 }])) // Renamed after the par2 was made (random names): two data files and enough bytes; one missing, or too small. assert.isTrue(par2Complete(set, [{ name: 'Qx81.bin', size: 1100 }, { name: 'Zr02.bin', size: 12 }, { name: 'Qx.par2', size: 50 }], [])) assert.isFalse(par2Complete(set, [{ name: 'Qx81.bin', size: 1100 }, { name: 'Qx.par2', size: 50 }], [])) assert.isFalse(par2Complete(set, [{ name: 'Qx81.bin', size: 500 }, { name: 'Zr02.bin', size: 12 }], [])) }) test('decided once settled with a par2 set, and kept when more files come', async ({ assert }) => { await store(1, [...file('Some.Upload.mkv', 3, 10), ...file('Some.Upload.par2', 1, 20)]) const release = await db.from('releases').firstOrFail() assert.isNull(release.file_total) await db.table('par2_files').multiInsert([setFile('Some.Upload.mkv', 2_000_000), setFile('Some.Upload.nfo', 100)].map((f, i) => ({ set_id: 's'.repeat(32), file_id: String(i).repeat(32), ...f }))) await db.table('par2_sets').insert({ set_id: 's'.repeat(32), release_id: release.id, files: 2 }) // Not settled yet: left alone. assert.equal(await decidePar2Completeness(db, [release.id]), 0) await db.from('releases').where('id', release.id).update({ settled_at: new Date() }) assert.equal(await decidePar2Completeness(db, [release.id]), 1) assert.isFalse((await db.from('releases').where('id', release.id).firstOrFail()).complete) // The nfo comes in a later range: complete, and the batch store's own update keeps it. await store(1001, file('Some.Upload.nfo', 1, 1010)) await decidePar2Completeness(db, [release.id]) await store(2001, file('Some.Upload.sfv', 1, 2010)) const after = await db.from('releases').where('id', release.id).firstOrFail() assert.equal(after.files, 4) assert.isTrue(after.complete) }) })