Verify update artifacts with exact metadata and SHA-512, require host-confirmed upload completion before cleanup, harden credentials and backups, improve queue recovery and skipped-state reporting, expand Windows path coverage, and add CI packaging checks.
180 lines
7.1 KiB
JavaScript
180 lines
7.1 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert');
|
|
const {
|
|
summarizePerHoster,
|
|
classifyErrorCategory,
|
|
summarizeBatchErrors,
|
|
isRetryableCategory,
|
|
mergeSkippedIntoSummary
|
|
} = 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('summarizePerHoster reports skipped uploads without lowering the host success rate', () => {
|
|
const history = [makeBatch(1, [
|
|
{ hoster: 'voe.sx', status: 'done' },
|
|
{ hoster: 'voe.sx', status: 'error', error: 'x' },
|
|
{ hoster: 'voe.sx', status: 'skipped', error: 'Datei zu groß' }
|
|
])];
|
|
const summary = summarizePerHoster(history)['voe.sx'];
|
|
assert.deepStrictEqual({ ok: summary.ok, fail: summary.fail, skipped: summary.skipped, total: summary.total }, { ok: 1, fail: 1, skipped: 1, total: 3 });
|
|
assert.strictEqual(summary.rate, 0.5);
|
|
});
|
|
|
|
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('summarizeBatchErrors excludes skipped results from error and retry buckets', () => {
|
|
const buckets = summarizeBatchErrors({
|
|
files: [{ name: 'a.mp4', results: [{ hoster: 'voe.sx', status: 'skipped', error: 'Kein gültiger Account' }] }]
|
|
});
|
|
assert.strictEqual(Object.values(buckets).flat().length, 0);
|
|
});
|
|
|
|
test('mergeSkippedIntoSummary adds skipped jobs to totals and history files', () => {
|
|
const summary = {
|
|
id: 'batch-1',
|
|
timestamp: '2026-08-11T12:00:00.000Z',
|
|
total: 1,
|
|
succeeded: 1,
|
|
failed: 0,
|
|
skipped: 0,
|
|
files: [{ name: 'ok.mp4', size: 5, results: [{ jobId: 'ok', hoster: 'voe.sx', status: 'done' }] }]
|
|
};
|
|
const merged = mergeSkippedIntoSummary(summary, [{
|
|
jobId: 'skip',
|
|
file: 'C:\\incoming\\skip.mp4',
|
|
fileName: 'skip.mp4',
|
|
hoster: 'byse.sx',
|
|
reason: 'Kein gültiger Account'
|
|
}]);
|
|
assert.strictEqual(merged.total, 2);
|
|
assert.strictEqual(merged.succeeded, 1);
|
|
assert.strictEqual(merged.failed, 0);
|
|
assert.strictEqual(merged.skipped, 1);
|
|
assert.deepStrictEqual(merged.files[1], {
|
|
name: 'skip.mp4',
|
|
size: 0,
|
|
results: [{ jobId: 'skip', hoster: 'byse.sx', status: 'skipped', error: 'Kein gültiger Account' }]
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|