94 lines
5.5 KiB
TypeScript
94 lines
5.5 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 RangeBlocks from '#services/range_blocks'
|
|
import SearchService from '#services/search_service'
|
|
import { RangeIndexer, postedSpan } from '#lib/edge/index'
|
|
import { DEFAULT_RULES } from '#lib/rules/index'
|
|
import type { Overview } from '#lib/nntp/index'
|
|
|
|
const GROUP = 'alt.binaries.final'
|
|
const at = (iso: string) => new Date(iso)
|
|
|
|
test.group('Final completeness', (group) => {
|
|
group.each.setup(() => testUtils.db().withGlobalTransaction())
|
|
|
|
test("a batch's posting times, without the earliest and latest 1%, become the range's span", async ({ assert }) => {
|
|
assert.deepEqual(postedSpan([]), [null, null])
|
|
const dates = Array.from({ length: 200 }, (_, i) => 1_000_000 + i)
|
|
dates.push(5, 9_999_999_999)
|
|
// 202 dates: 2 trimmed at each end, so the two wild ones go.
|
|
assert.deepEqual(postedSpan(dates), [1_000_001, 1_000_198])
|
|
|
|
const [g] = await db.table('groups').insert({ name: GROUP, priority: 1 }).returning('id')
|
|
const [node] = await db.table('nodes').insert({ name: 'final-node' }).returning('id')
|
|
const [range] = await db
|
|
.table('ranges')
|
|
.insert({ group_id: g.id, start_id: 1, end_id: 1000, next_id: 1, status: 'leased', node_id: node.id, rule_version: 1 })
|
|
.returning('id')
|
|
const overviews = Array.from({ length: 10 }, (_, i) => {
|
|
const date = new Date(Date.parse('2026-01-01T10:00:00Z') + i * 60_000).toUTCString().replace('GMT', '+0000')
|
|
return [1 + i, { 'subject': `[1/1] - "Some.Thing.2026.rar" yEnc (${i + 1}/10)`, 'from': 'p <p@x>', 'date': date, 'message-id': `<f${i}@x>`, ':bytes': '1000', 'xref': `x ${GROUP}:${1 + i}` }] as Overview
|
|
})
|
|
const ix = new RangeIndexer({ group: GROUP, start: 1, end: 1000, rules: { ...DEFAULT_RULES, samplePercent: 0, droppedSignatures: [], priorities: { [GROUP]: 1 } } })
|
|
ix.add(overviews, 11)
|
|
await BatchStore.store(Number(range.id), Number(node.id), ix.take())
|
|
const stored = await db.from('ranges').where('id', range.id).firstOrFail()
|
|
assert.equal(new Date(stored.posted_low).toISOString(), '2026-01-01T10:00:00.000Z')
|
|
assert.equal(new Date(stored.posted_high).toISOString(), '2026-01-01T10:09:00.000Z')
|
|
})
|
|
|
|
test('a release is final once one block of done ranges covers its posting times and 6 hours either side', async ({ assert }) => {
|
|
const [g] = await db.table('groups').insert({ name: GROUP, priority: 1 }).returning('id')
|
|
const range = (start: number, status: string, low: string | null, high: string | null) =>
|
|
db.table('ranges').insert({ group_id: g.id, start_id: start, end_id: start + 99_999, next_id: start, status, posted_low: low && at(low), posted_high: high && at(high) })
|
|
await range(1, 'done', '2026-01-01T00:00:00Z', '2026-01-01T12:00:00Z')
|
|
await range(100_001, 'done', '2026-01-01T12:00:00Z', '2026-01-01T23:00:00Z')
|
|
await range(200_001, 'planned', null, null)
|
|
await range(300_001, 'done', '2026-01-02T10:00:00Z', '2026-01-02T20:00:00Z')
|
|
assert.equal(await RangeBlocks.rebuild(), 2)
|
|
|
|
let n = 0
|
|
const release = async (first: number, posted: string, extra: Record<string, unknown> = {}) => {
|
|
n++
|
|
const key = `fc${n}`.padEnd(40, '0')
|
|
const [row] = await db
|
|
.table('releases')
|
|
.insert({ release_key: key, identity_key: key, name: `Final.Test.${n}`, source: 'stem', groups: '[]', group_id: g.id, files: 3, file_total: 5, complete: false, first_article: first, last_article: first + 10, posted_at: at(posted), last_posted_at: at(posted), readable: true, settled_at: new Date(), ...extra })
|
|
.returning('id')
|
|
return Number(row.id)
|
|
}
|
|
const final = async (id: number) => {
|
|
const r = await db.from('releases').where('id', id).firstOrFail()
|
|
const { rows } = await db.rawQuery(`SELECT release_final(?, ?, ?, ?, ?) AS final`, [r.group_id, r.first_article, r.last_article, r.posted_at, r.last_posted_at])
|
|
return rows[0].final as boolean
|
|
}
|
|
|
|
// Mid-block: final either way.
|
|
const middle = await release(90_000, '2026-01-01T11:00:00Z')
|
|
// In the first range: nothing older to wait for, although 6 hours before it isn't covered.
|
|
const first = await release(1_000, '2026-01-01T02:00:00Z')
|
|
// Near the block's end, the next range unread: its missing files may be there.
|
|
const edge = await release(195_000, '2026-01-01T22:00:00Z')
|
|
// In the island after the gap: the gap is before it.
|
|
const island = await release(310_000, '2026-01-02T12:00:00Z')
|
|
assert.isTrue(await final(middle))
|
|
assert.isTrue(await final(first))
|
|
assert.isFalse(await final(edge))
|
|
assert.isFalse(await final(island))
|
|
|
|
// Search: an incomplete release that isn't final isn't left out as incomplete; the API says which is final.
|
|
const shown = await SearchService.search({ q: 'Final.Test', notIncomplete: true })
|
|
assert.sameMembers(shown.releases.map((r) => r.id), [edge, island])
|
|
assert.isTrue(shown.releases.every((r) => r.final === false))
|
|
|
|
// The gap read: one block, the edge release final (the island's 12:00 + 6 h is inside 20:00).
|
|
await db.from('ranges').where('start_id', 200_001).update({ status: 'done', posted_low: at('2026-01-01T23:00:00Z'), posted_high: at('2026-01-02T10:00:00Z') })
|
|
assert.equal(await RangeBlocks.rebuild(), 1)
|
|
assert.isTrue(await final(edge))
|
|
assert.isTrue(await final(island))
|
|
assert.equal((await SearchService.search({ q: 'Final.Test', notIncomplete: true })).total, 0)
|
|
})
|
|
})
|