Generate immutable post-cleanup summaries only after queue, history, and recovery finalization. Add bilingual accessible report UI, host and cleanup metrics, report-bound JSON and sanitized error CSV exports, renderer reload recovery, duplicate-filename-safe accounting, and public-source verification coverage.
151 lines
8.0 KiB
JavaScript
151 lines
8.0 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
test('catastrophic batch starts retain exact terminal outcomes for every job', () => {
|
|
const { buildFailedUploadSummary, buildTerminalJobSnapshots } = require('../lib/upload-recovery');
|
|
const summary = buildFailedUploadSummary([
|
|
{ jobId: 'job-a', file: 'C:\\one\\same.mkv', hoster: 'doodstream.com' },
|
|
{ jobId: 'job-b', file: 'D:\\two\\same.mkv', hoster: 'voe.sx' }
|
|
], 'Upload konnte nicht gestartet werden', Date.UTC(2026, 7, 13));
|
|
assert.equal(summary.total, 2);
|
|
assert.equal(summary.failed, 2);
|
|
assert.deepEqual(buildTerminalJobSnapshots(summary).map(entry => [entry.jobId, entry.status]), [
|
|
['job-a', 'error'],
|
|
['job-b', 'error']
|
|
]);
|
|
});
|
|
|
|
test('catastrophic batch summaries preserve safe file keys for later skipped-job merging', () => {
|
|
const { buildFailedUploadSummary } = require('../lib/upload-recovery');
|
|
const summary = buildFailedUploadSummary([
|
|
{ jobId: 'job-a', file: 'C:\\one\\same.mkv', fileKey: 'safe-file-key', hoster: 'doodstream.com' }
|
|
], 'Upload konnte nicht gestartet werden');
|
|
|
|
assert.equal(summary.files[0].fileKey, 'safe-file-key');
|
|
});
|
|
|
|
test('terminal recovery snapshots retain exact job outcomes and canonical links', () => {
|
|
const { buildTerminalJobSnapshots } = require('../lib/upload-recovery');
|
|
const snapshots = buildTerminalJobSnapshots({
|
|
files: [{
|
|
name: 'episode.mkv',
|
|
results: [
|
|
{ jobId: 'done-job', hoster: 'doodstream.com', status: 'done', download_url: 'https://doodstream.com/d/abc123', file_code: 'abc123' },
|
|
{ jobId: 'error-job', hoster: 'voe.sx', status: 'error', error: 'rejected', failureDetails: { kind: 'hoster' }, remoteCommitUncertain: true },
|
|
{ hoster: 'byse.sx', status: 'done', download_url: 'https://byse.sx/d/no-id' }
|
|
]
|
|
}]
|
|
});
|
|
|
|
assert.deepEqual(snapshots, [
|
|
{
|
|
jobId: 'done-job',
|
|
status: 'done',
|
|
error: null,
|
|
failureDetails: null,
|
|
result: { download_url: 'https://doodstream.com/d/abc123', embed_url: null, file_code: 'abc123' }
|
|
},
|
|
{
|
|
jobId: 'error-job',
|
|
status: 'error',
|
|
error: 'rejected',
|
|
failureDetails: { kind: 'hoster' },
|
|
remoteCommitUncertain: true,
|
|
result: null
|
|
}
|
|
]);
|
|
});
|
|
|
|
test('recovery markers affect only their exact job IDs and never restart terminal outcomes', () => {
|
|
const { getRecoveryOutcome } = require('../lib/upload-recovery');
|
|
const recovery = {
|
|
jobIds: ['done-job', 'active-job'],
|
|
historyPending: true,
|
|
terminalJobs: [{
|
|
jobId: 'done-job',
|
|
status: 'done',
|
|
error: null,
|
|
failureDetails: null,
|
|
result: { download_url: 'https://doodstream.com/d/abc123', embed_url: null, file_code: 'abc123' }
|
|
}, {
|
|
jobId: 'uncertain-job',
|
|
status: 'error',
|
|
error: 'remote result unknown',
|
|
failureDetails: null,
|
|
remoteCommitUncertain: true,
|
|
result: null
|
|
}]
|
|
};
|
|
|
|
assert.deepEqual(getRecoveryOutcome({ id: 'done-job', status: 'preview' }, recovery), {
|
|
status: 'done',
|
|
error: null,
|
|
failureDetails: null,
|
|
result: { download_url: 'https://doodstream.com/d/abc123', embed_url: null, file_code: 'abc123' },
|
|
historyPending: true,
|
|
interrupted: false
|
|
});
|
|
assert.deepEqual(getRecoveryOutcome({ id: 'active-job', status: 'queued' }, recovery), { status: 'queued', interrupted: true });
|
|
assert.deepEqual(getRecoveryOutcome({ id: 'uncertain-job', status: 'preview' }, recovery), {
|
|
status: 'error',
|
|
error: 'remote result unknown',
|
|
failureDetails: null,
|
|
remoteCommitUncertain: true,
|
|
result: null,
|
|
historyPending: true,
|
|
interrupted: false
|
|
});
|
|
assert.deepEqual(getRecoveryOutcome({ id: 'foreign-job', status: 'queued' }, recovery), { status: 'queued', interrupted: false });
|
|
assert.deepEqual(getRecoveryOutcome({ id: 'already-done', status: 'done' }, recovery), { status: 'done', interrupted: false });
|
|
});
|
|
|
|
test('remote commit uncertainty survives acknowledgements and failed retries until confirmed success', () => {
|
|
const { resolveRemoteCommitUncertainty } = require('../lib/upload-recovery');
|
|
assert.equal(resolveRemoteCommitUncertainty(true, { status: 'queued' }), true);
|
|
assert.equal(resolveRemoteCommitUncertainty(true, { status: 'getting-server' }), true);
|
|
assert.equal(resolveRemoteCommitUncertainty(true, { status: 'uploading' }), true);
|
|
assert.equal(resolveRemoteCommitUncertainty(true, { status: 'error' }), true);
|
|
assert.equal(resolveRemoteCommitUncertainty(true, { status: 'aborted' }), true);
|
|
assert.equal(resolveRemoteCommitUncertainty(true, { status: 'done' }), false);
|
|
assert.equal(resolveRemoteCommitUncertainty(false, { status: 'error', remoteCommitUncertain: true }), true);
|
|
assert.equal(resolveRemoteCommitUncertainty(false, { status: 'error' }), false);
|
|
});
|
|
|
|
test('main and renderer keep recovery evidence until final queue persistence succeeds', () => {
|
|
const root = path.join(__dirname, '..');
|
|
const mainSource = fs.readFileSync(path.join(root, 'main.js'), 'utf8');
|
|
const rendererSource = fs.readFileSync(path.join(root, 'renderer', 'app.js'), 'utf8');
|
|
const indexSource = fs.readFileSync(path.join(root, 'renderer', 'index.html'), 'utf8');
|
|
const batchDone = mainSource.slice(mainSource.indexOf("uploadManager.on('batch-done'"), mainSource.indexOf("ipcMain.handle('cancel-upload'"));
|
|
const barrier = mainSource.slice(mainSource.indexOf('function createUploadFinalizationBarrier'), mainSource.indexOf('function requestUploadFinalization'));
|
|
|
|
assert.match(mainSource, /buildTerminalSnapshots: buildTerminalJobSnapshots/);
|
|
assert.ok(barrier.indexOf('appendHistory(summary)') < barrier.indexOf('saveRecovery(terminalRecovery)'));
|
|
assert.ok(barrier.indexOf('saveRecovery(terminalRecovery)') < barrier.indexOf('requestFinalization(summary, historyPersisted)'));
|
|
assert.match(barrier, /if \(historyPersisted && queuePersisted && terminalRecoveryPersisted\)[\s\S]*saveRecovery\(null\)/);
|
|
assert.match(batchDone, /uploadFinalizationBarrier\.finalize\(summary, recovery\)/);
|
|
const startFailure = batchDone.slice(batchDone.indexOf('startBatch(tasks'));
|
|
assert.match(startFailure, /buildFailedUploadSummary\(tasks/);
|
|
assert.match(startFailure, /uploadFinalizationBarrier\.finalize\(errorSummary, recovery\)/);
|
|
const startHandler = mainSource.slice(mainSource.indexOf("ipcMain.handle('start-upload'"), mainSource.indexOf("ipcMain.handle('cancel-upload'"));
|
|
assert.match(startHandler, /await configStore\.saveUploadRecovery\(recovery\)/);
|
|
assert.match(startHandler, /uploadFinalizationBarrier\.finalize\(skippedSummary/);
|
|
assert.match(startHandler, /finalized: true/);
|
|
assert.ok(startHandler.indexOf('sourceCleanup.registerGroups(sourceCleanupGroups)') < startHandler.indexOf('saveUploadRecovery(recovery)'));
|
|
assert.match(startHandler, /catch \(error\)[\s\S]*return { error: 'Upload-Wiederherstellung konnte nicht gespeichert werden' }/);
|
|
const addHandler = mainSource.slice(mainSource.indexOf("ipcMain.handle('add-jobs-to-batch'"), mainSource.indexOf("ipcMain.handle('finish-after-active'"));
|
|
assert.match(addHandler, /await configStore\.saveUploadRecovery\(nextRecovery\)/);
|
|
assert.ok(addHandler.indexOf('saveUploadRecovery(nextRecovery)') < addHandler.indexOf('batchManager.addJobs(tasks)'));
|
|
assert.match(rendererSource, /window\.UploadRecovery\.getRecoveryOutcome/);
|
|
assert.match(rendererSource, /data\.historyPersisted !== true/);
|
|
assert.match(rendererSource, /deliveryId: data\.deliveryId/);
|
|
assert.match(rendererSource, /remoteCommitUncertain: Object\.hasOwn\(recoveryOutcome, 'remoteCommitUncertain'\)/);
|
|
assert.match(rendererSource, /remoteCommitUncertain: job\.remoteCommitUncertain === true/);
|
|
assert.match(rendererSource, /j\.remoteCommitUncertain !== true/);
|
|
assert.equal((rendererSource.match(/window\.UploadRecovery\.resolveRemoteCommitUncertainty/g) || []).length >= 2, true);
|
|
assert.doesNotMatch(rendererSource, /acknowledgeRemoteCommitRetry/);
|
|
assert.ok(indexSource.indexOf('../lib/upload-recovery.js') < indexSource.indexOf('app.js'));
|
|
});
|