Files
usenet-indexer/tests/unit/delimited_reader.spec.ts
T
Daan MeijerandClaude Opus 5.5 be8a3d4580 Spotnet import, header backfill, staging and counter grouping, faster search
Spotnet (CONCEPT D29):
- Index free.pt fully and import spots with their descriptions, categories
  and NZBs; a spot renames its release, since Spotnet names are more
  trustworthy than names derived from subjects.
- Find passwords in spot descriptions too, and tell "needs a password we
  have" from "needs a password we don't have".

Content inspection:
- List the contents of 7z (.7z.001) archives, SFV sets, nicovideo uploads
  and other archive-like files; mark yEncBin .bin containers as encrypted.
- Take real names from yEnc headers for randomly named files, and merge
  uploads that were split across posts.

Storage (D31, D32):
- Store segment lists zstd-compressed behind the FileStore accessor.
- group:backfill indexes older headers at low priority and pauses when
  the NVMe or HDD part of the database gets too large.

Grouping (D35, D36):
- Lone header segments wait in staged_segments until a second segment of
  their file arrives, which drops the per-article junk binaries.
- Counter grouping joins random-subject uploads by poster, [i/M] counter,
  part count and day.

Search and maintenance (D33, D34):
- A trigram-indexed search_text column, smaller indexes, estimated stats.
- Passes that clean up nonsense names; weekly index maintenance; GC budgets
  per kind.
- Web UI: grey out stale results while a search runs and cancel superseded
  requests. Count only up to 1,000 matches for the page, which made filter
  clicks take seconds (Newznab keeps the 10,000 cap).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 19:59:23 +02:00

53 lines
2.4 KiB
TypeScript

import { test } from '@japa/runner'
import { delimitedReader } from '#lib/nntp/delimited_reader'
const CRLF = Buffer.from('\r\n')
const END = Buffer.from('\r\n.\r\n')
async function* chunks(...parts: string[]) {
for (const part of parts) yield Buffer.from(part, 'latin1')
}
test.group('nntp / delimitedReader', () => {
test('reads an empty multi-line reply (XOVER of a range without articles)', async ({ assert }) => {
const reader = delimitedReader(chunks('224 Overview follows\r\n.\r\n', '211 1 2 3 g\r\n'), CRLF)
assert.equal((await reader.next()).value!.toString(), '224 Overview follows')
assert.equal((await reader.next(END)).value!.toString(), '')
assert.equal((await reader.next(CRLF)).value!.toString(), '211 1 2 3 g')
})
test('reads multi-line data, also with the terminator split over chunks', async ({ assert }) => {
for (const split of [1, 2, 3, 4, 5, 6, 7]) {
const reply = '224 ok\r\nline 1\r\n..dotted\r\nline 3\r\n.\r\n200 next\r\n'
const at = reply.indexOf('.\r\n200') - 2 + split - 1
const reader = delimitedReader(chunks(reply.slice(0, at), reply.slice(at)), CRLF)
assert.equal((await reader.next()).value!.toString(), '224 ok')
assert.equal((await reader.next(END)).value!.toString(), 'line 1\r\n..dotted\r\nline 3', `split ${split}`)
assert.equal((await reader.next(CRLF)).value!.toString(), '200 next')
}
})
test('reads replies byte by byte', async ({ assert }) => {
const text = '200 welcome\r\n224 ok\r\na\r\n.\r\n224 ok\r\n.\r\n211 x\r\n'
const reader = delimitedReader(chunks(...text.split('')), CRLF)
assert.equal((await reader.next()).value!.toString(), '200 welcome')
assert.equal((await reader.next(CRLF)).value!.toString(), '224 ok')
assert.equal((await reader.next(END)).value!.toString(), 'a')
assert.equal((await reader.next(CRLF)).value!.toString(), '224 ok')
assert.equal((await reader.next(END)).value!.toString(), '')
assert.equal((await reader.next(CRLF)).value!.toString(), '211 x')
})
test('keeps binary data intact', async ({ assert }) => {
const body = Buffer.from([0, 13, 255, 10, 46, 13, 46, 10, 13])
const source = (async function* () {
yield Buffer.from('222 body\r\n')
yield body
yield Buffer.from('\r\n.\r\n')
})()
const reader = delimitedReader(source, CRLF)
await reader.next()
assert.deepEqual((await reader.next(END)).value, body)
})
})