Files
usenet-indexer/tests/functional/par2_name_merge.spec.ts
T
Daan MeijerandClaude Fable 5.1 2580f20a43
Test and deploy / test (push) Successful in 51s
Test and deploy / edge (push) Successful in 1m48s
Test and deploy / deploy (push) Skipped
voyager 3: the batch store writes releases once, by id references and bytea keys
Each release chunk is a release inserted once: release_key the SHA-1 of
the identity, the owner group and the first article (deterministic; no
numbering under a group lock), identity_key the identity's SHA-1 bytes,
poster_id and group_set_id through catalog_refs (cached per process),
source as a code (weak-stem added), the name into release_texts. A
finished release's candidates are claimable at once; a provisional tail
release is finished by a later chunk of its key, a finished one sent again
is left alone. Gone: joins into earlier releases and across groups, key
numbering, the group lock, the fragment merger. Interim: chunks, counts
and samples keyed by ranges.block_id, a range without a block gets one.

Readers follow: search (posters and group_sets joined, words through
release_texts, posters by id, source by code), the par2 merger, the
sidecar folder, the judge's scraped rule, the spot inspector, the spotter,
the NZB service, releases:regroup, edge:bench. Tests insert fixtures
through tests/functional/helpers/releases.ts; the suites pass again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-25 21:04:03 +01:00

89 lines
5.8 KiB
TypeScript

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 { Par2Merger, fileKey } from '#services/par2_merger'
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'
/** One file without a counter, from `poster`, posted `minutes` after the first. */
function file(name: string, at: number, minutes: number, poster = 'up <up@load.er>'): Overview[] {
const date = new Date(Date.parse('2026-10-20T10:00:00Z') + minutes * 60_000).toUTCString().replace('GMT', '+0000')
return [[at, { 'subject': `"${name}" yEnc (1/1)`, 'from': poster, 'date': date, 'message-id': `<${at}@test>`, ':bytes': '700000', 'xref': `news.example ${GROUP}:${at}` }] as Overview]
}
let stores = 0
async function store(overviews: Overview[]) {
stores++
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${stores}` }).returning('id')
const [r] = await db.table('ranges').insert({ group_id: g.id, start_id: stores * 100_000, end_id: stores * 100_000 + 99_999, next_id: stores * 100_000, status: 'leased', node_id: n.id }).returning('id')
const indexer = new RangeIndexer({ group: GROUP, start: stores * 100_000, end: stores * 100_000 + 99_999, rules: { ...DEFAULT_RULES, samplePercent: 0, droppedSignatures: [], priorities: { [GROUP]: 1 } } })
indexer.add(overviews, stores * 100_000 + 100_000)
indexer.finish()
await BatchStore.store(Number(r.id), Number(n.id), indexer.take())
}
test.group('Par2Merger / by file names', (group) => {
group.each.setup(() => testUtils.db().withGlobalTransaction())
test('a name as compared: no case, spaces or punctuation, UTF-8 read as Latin-1 read back', ({ assert }) => {
assert.equal(fileKey('Some/Dir/Ein.dunkles.Vermächtnis - 1080p.MKV'), fileKey('Ein dunkles Vermächtnis 1080p.mkv'))
assert.notEqual(fileKey('Track One.flac'), fileKey('Track Two.flac'))
})
test("a set's poster's releases nearby whose files it all names join its release; others stay", async ({ assert }) => {
await store([
// The album's index files, its tracks apart (no counter: a release each), and the same poster's next album.
...file('Some.Album-WEB-2026.par2', 100_010, 0),
...file('Some.Album-WEB-2026.nfo', 100_011, 0),
...file('Track One.flac', 100_020, 1),
...file('Track Two.flac', 100_030, 2),
...file('Ein.dunkles.Vermächtnis.mkv', 100_040, 3),
...file('Another Song.flac', 100_050, 4),
// A track the set names, but posted by someone else, and one a day later.
...file('Track One.flac', 100_060, 5, 'other <o@x>'),
])
await store(file('Track Two.flac', 200_070, 24 * 60 + 10))
const ids = Object.fromEntries((await db.from('releases as r').leftJoin('posters as p', 'p.id', 'r.poster_id').select('r.id', 'r.name', 'p.poster', 'r.posted_at')).map((r) => [`${r.name}${r.poster.startsWith('other') ? ' (other)' : ''}${new Date(r.posted_at).getUTCDate() === 21 ? ' (later)' : ''}`, Number(r.id)]))
const index = ids['Some.Album-WEB-2026']
const setId = 'a'.repeat(32)
await db.table('par2_files').multiInsert(
['Track One.flac', 'Track Two.flac', 'Ein.dunkles.Vermächtnis.mkv'].map((name, i) => ({ set_id: setId, file_id: String(i).repeat(32), hash16k: 'f'.repeat(32), length: 600_000, name }))
)
await db.table('par2_sets').insert({ set_id: setId, release_id: index, files: 3 })
assert.equal(await new Par2Merger().mergeByName(setId), 3)
const left = (await db.from('releases as r').leftJoin('posters as p', 'p.id', 'r.poster_id').orderBy('r.name').select('r.name', 'r.files', 'p.poster')).map((r) => `${r.name} ${r.files}${r.poster.startsWith('other') ? ' (other)' : ''}`)
assert.includeMembers(left, ['Another Song 1', 'Track One 1 (other)', 'Track Two 1'])
assert.lengthOf(left, 4)
const merged = await db.from('release_merges').where('via', 'name').count('* as n')
assert.equal(Number(merged[0].n), 3)
// Nothing more to find.
assert.equal(await new Par2Merger().mergeByName(setId), 0)
})
test('not a holder years off (a repost of the par2); a member with an article the target has merges without it', async ({ assert }) => {
await store([...file('Some.Album-WEB-2026.par2', 100_010, 0), ...file('Track One.flac', 100_020, 1)])
// The same par2 posted again a year later, in a range of its own.
await store(file('Some.Album-WEB-2026.par2', 200_010, 365 * 24 * 60))
const releases = await db.from('releases').orderBy('posted_at').select('id', 'name', 'posted_at')
const [index, track, repost] = releases.map((r) => Number(r.id))
const setId = 'b'.repeat(32)
await db.table('par2_files').insert({ set_id: setId, file_id: '1'.repeat(32), hash16k: 'f'.repeat(32), length: 600_000, name: 'Track One.flac' })
await db.table('par2_sets').multiInsert([index, repost].map((id) => ({ set_id: setId, release_id: id, files: 1 })))
// The repost is the newest holder: nothing near it to join.
assert.equal(await new Par2Merger().mergeByName(setId), 0)
await db.from('par2_sets').where('release_id', repost).delete()
// Both releases hold a candidate of the same article.
await db.table('probe_candidates').multiInsert([index, track].map((id) => ({ release_id: id, kind: 'par2', message_id: '<same@x>', file: 'x.par2' })))
assert.equal(await new Par2Merger().mergeByName(setId), 1)
assert.lengthOf(await db.from('probe_candidates').where('message_id', '<same@x>'), 1)
assert.lengthOf(await db.from('releases').whereIn('id', [index, track]), 1)
assert.lengthOf(await db.from('releases').where('id', repost), 1)
})
})