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 FragmentMerger from '#services/fragment_merger' import { RangeIndexer } from '#lib/edge/index' import { decodeChunk } from '#lib/chunks/index' import { DEFAULT_RULES } from '#lib/rules/index' import type { Overview } from '#lib/nntp/index' const GROUP = 'alt.binaries.test' const rules = { ...DEFAULT_RULES, samplePercent: 0, droppedSignatures: [], priorities: { [GROUP]: 1 } } function parts(name: string, total: number, numbers: number[], at: number): Overview[] { return numbers.map((n, i) => [at + i, { 'subject': `Big.Upload.2020 [1/1] - "${name}" yEnc (${n}/${total})`, 'from': 'p
', 'date': 'Mon, 05 Oct 2026 10:00:00 +0000', 'message-id': `<${name}-${n}@test>`, ':bytes': '700000', 'xref': `news.example ${GROUP}:${at + i}`, }] as Overview) } /** Indexes one range's overviews and stores them as the range's only batch; the range ends done. */ async function indexRange(groupId: number, nodeId: number, start: number, end: number, overviews: Overview[]) { const [r] = await db.table('ranges').insert({ group_id: groupId, start_id: start, end_id: end, next_id: start, status: 'leased', node_id: nodeId }).returning('id') const indexer = new RangeIndexer({ group: GROUP, start, end, rules, edgeArticles: 100 }) indexer.add(overviews, end + 1) indexer.finish() await BatchStore.store(Number(r.id), nodeId, indexer.take()) await db.from('ranges').where('id', r.id).update({ status: 'done' }) return Number(r.id) } test.group('FragmentMerger', (group) => { group.each.setup(() => testUtils.db().withGlobalTransaction()) test('a file cut by a range edge is completed from both sides once both ranges are done', async ({ assert }) => { const [g] = await db.table('groups').insert({ name: GROUP, priority: 1, low: 1 }).returning('id') const [n] = await db.table('nodes').insert({ name: 'merge-node' }).returning('id') // Parts 1–2 at the end of the first range (still open there), 3–4 at the start of the next. await indexRange(g.id, n.id, 1, 1000, parts('big.part1.rar', 4, [1, 2], 990)) assert.equal((await db.from('fragments').count('* as c'))[0].c, '1') assert.include(await FragmentMerger.run(), { completed: 0, incomplete: 0, waiting: 1 }) await indexRange(g.id, n.id, 1001, 2000, parts('big.part1.rar', 4, [3, 4], 1001)) assert.include(await FragmentMerger.run(), { completed: 1, incomplete: 0 }) assert.equal((await db.from('fragments').count('* as c'))[0].c, '0') const release = await db.from('releases').first() assert.include(release, { files: 1, chunks: 1 }) const chunk = await db.from('release_chunks').where('release_id', release.id).first() assert.deepEqual(decodeChunk(chunk.payload)[0].segments.map((s) => s[0]), [1, 2, 3, 4]) assert.deepEqual((await db.from('probe_candidates')).map((c) => c.kind), ['archive']) }) test('a file still missing parts when all its neighbours are done is dropped', async ({ assert }) => { const [g] = await db.table('groups').insert({ name: GROUP, priority: 1, low: 1 }).returning('id') const [n] = await db.table('nodes').insert({ name: 'merge-node' }).returning('id') await indexRange(g.id, n.id, 1, 1000, parts('gap.part1.rar', 5, [1, 2], 990)) await indexRange(g.id, n.id, 1001, 2000, parts('gap.part1.rar', 5, [4, 5], 1001)) // Its later neighbour isn't there yet: the missing part could still come. assert.include(await FragmentMerger.run(), { completed: 0, incomplete: 0, waiting: 1 }) await indexRange(g.id, n.id, 2001, 3000, []) assert.include(await FragmentMerger.run(), { completed: 0, incomplete: 1 }) assert.equal((await db.from('fragments').count('* as c'))[0].c, '0') assert.lengthOf(await db.from('releases'), 0) }) })