const test = require('node:test'); const assert = require('node:assert'); const { summarizePerHoster, classifyErrorCategory, summarizeBatchErrors, isRetryableCategory } = require('../lib/stats'); function makeBatch(timestamp, results) { return { id: 'b-' + timestamp, timestamp: new Date(timestamp).toISOString(), files: [{ name: 'foo.mp4', size: 1, results }] }; } test('summarizePerHoster counts ok and fail per hoster across all batches', () => { const history = [ makeBatch(1, [ { hoster: 'voe.sx', status: 'done' }, { hoster: 'byse.sx', status: 'error', error: 'Not video file format' } ]), makeBatch(2, [ { hoster: 'voe.sx', status: 'done' }, { hoster: 'voe.sx', status: 'error', error: 'CSRF' }, { hoster: 'byse.sx', status: 'done' } ]) ]; const s = summarizePerHoster(history); assert.strictEqual(s['voe.sx'].ok, 2); assert.strictEqual(s['voe.sx'].fail, 1); assert.strictEqual(s['voe.sx'].total, 3); assert.strictEqual(Math.round(s['voe.sx'].rate * 100), 67); assert.strictEqual(s['byse.sx'].ok, 1); assert.strictEqual(s['byse.sx'].fail, 1); assert.strictEqual(s['byse.sx'].rate, 0.5); }); test('summarizePerHoster honors sinceMs cutoff', () => { const history = [ makeBatch(1000, [{ hoster: 'voe.sx', status: 'done' }]), makeBatch(5000, [{ hoster: 'voe.sx', status: 'error', error: 'x' }]) ]; const s = summarizePerHoster(history, { sinceMs: 3000 }); assert.strictEqual(s['voe.sx'].ok, 0); assert.strictEqual(s['voe.sx'].fail, 1); }); test('summarizePerHoster honors lastNBatches (newest first)', () => { const history = [ makeBatch(1000, [{ hoster: 'voe.sx', status: 'done' }]), makeBatch(2000, [{ hoster: 'voe.sx', status: 'done' }]), makeBatch(3000, [{ hoster: 'voe.sx', status: 'error', error: 'x' }]) ]; const s = summarizePerHoster(history, { lastNBatches: 1 }); assert.strictEqual(s['voe.sx'].ok, 0); assert.strictEqual(s['voe.sx'].fail, 1); }); test('summarizePerHoster handles empty / malformed input', () => { assert.deepStrictEqual(summarizePerHoster(null), {}); assert.deepStrictEqual(summarizePerHoster([]), {}); assert.deepStrictEqual(summarizePerHoster([{ id: 'x', files: null }]), {}); }); test('classifyErrorCategory: file-rejected phrases', () => { assert.strictEqual(classifyErrorCategory('Byse lehnte Datei ab: Not video file format'), 'file-rejected'); assert.strictEqual(classifyErrorCategory('Duplicate file already exists'), 'file-rejected'); assert.strictEqual(classifyErrorCategory('Datei zu groß (Max: 5 GB)'), 'file-rejected'); }); test('classifyErrorCategory: account-error phrases', () => { assert.strictEqual(classifyErrorCategory('Quota exceeded'), 'account-error'); assert.strictEqual(classifyErrorCategory('account banned'), 'account-error'); assert.strictEqual(classifyErrorCategory('not enough disk space'), 'account-error'); }); test('classifyErrorCategory: hoster-transient phrases', () => { assert.strictEqual(classifyErrorCategory('CSRF-Token nicht gefunden'), 'hoster-transient'); assert.strictEqual(classifyErrorCategory('Kein Upload-Server erhalten: server busy'), 'hoster-transient'); assert.strictEqual(classifyErrorCategory('Kein Filecode'), 'hoster-transient'); }); test('classifyErrorCategory: network phrases', () => { assert.strictEqual(classifyErrorCategory('socket hang up'), 'network'); assert.strictEqual(classifyErrorCategory('fetch failed'), 'network'); assert.strictEqual(classifyErrorCategory('Timeout while reading'), 'network'); }); test('classifyErrorCategory: aborted is its own bucket (not retryable)', () => { assert.strictEqual(classifyErrorCategory('Abgebrochen'), 'aborted'); assert.strictEqual(isRetryableCategory('aborted'), false); }); test('classifyErrorCategory: unknown for everything else', () => { assert.strictEqual(classifyErrorCategory(''), 'unknown'); assert.strictEqual(classifyErrorCategory(null), 'unknown'); assert.strictEqual(classifyErrorCategory('Some weird thing'), 'unknown'); }); test('summarizeBatchErrors buckets results by category', () => { const summary = { files: [ { name: 'a.mp4', results: [ { hoster: 'voe.sx', status: 'done' }, { hoster: 'byse.sx', status: 'error', error: 'Not video file format' } ] }, { name: 'b.mp4', results: [ { hoster: 'voe.sx', status: 'error', error: 'CSRF' }, { hoster: 'doodstream.com', status: 'error', error: 'socket hang up' } ] } ] }; const buckets = summarizeBatchErrors(summary); assert.strictEqual(buckets['file-rejected'].length, 1); assert.strictEqual(buckets['file-rejected'][0].hoster, 'byse.sx'); assert.strictEqual(buckets['hoster-transient'].length, 1); assert.strictEqual(buckets['hoster-transient'][0].hoster, 'voe.sx'); assert.strictEqual(buckets['network'].length, 1); assert.strictEqual(buckets['network'][0].hoster, 'doodstream.com'); assert.strictEqual(buckets['account-error'].length, 0); }); test('isRetryableCategory: only transient + network + unknown retry-worthy', () => { assert.strictEqual(isRetryableCategory('hoster-transient'), true); assert.strictEqual(isRetryableCategory('network'), true); assert.strictEqual(isRetryableCategory('unknown'), true); assert.strictEqual(isRetryableCategory('file-rejected'), false); assert.strictEqual(isRetryableCategory('account-error'), false); assert.strictEqual(isRetryableCategory('aborted'), false); });