Files
Multi-Hoster-Upload/tests/source-cleanup-policy.test.js
T
Sucukdeluxe 67978cb81f Harden source cleanup durability barriers
Separate provisional upload success from persistently confirmed cleanup state, invalidate prior confirmations before retries, and promote completion only inside the final queue persistence handshake. Preserve source files when history or queue persistence fails and add regression coverage for restart, retry, rollback, and real Electron finalization paths.
2026-08-13 19:45:31 +02:00

329 lines
11 KiB
JavaScript

const { test } = require('node:test');
const assert = require('node:assert/strict');
const policy = require('../lib/source-cleanup-policy');
function queueFixture() {
return [
{
id: 'job-doodstream',
file: 'C:\\Uploads\\Movie.MKV',
hoster: 'doodstream.com',
status: 'done'
},
{
id: 'job-voe',
file: 'C:\\Uploads\\Movie.MKV',
hoster: 'voe.sx',
status: 'done'
},
{
id: 'job-vidmoly',
file: 'C:\\Uploads\\Movie.MKV',
hoster: 'vidmoly.me',
status: 'error'
},
{
id: 'job-byse',
file: 'C:\\Uploads\\Movie.MKV',
hoster: 'byse.sx',
status: 'preview'
}
];
}
test('prepareGroups creates one Windows manifest with a stable token and immutable hosters', () => {
const queueJobs = queueFixture();
let tokenCalls = 0;
const prepared = policy.prepareGroups(queueJobs, [queueJobs[2]], () => {
tokenCalls += 1;
return 'cleanup-1';
}, 'win32');
assert.equal(tokenCalls, 1);
assert.equal(prepared.groups.length, 1);
assert.equal(prepared.touchedJobs.length, 4);
assert.deepEqual(prepared.groups[0], {
token: 'cleanup-1',
file: 'C:\\Uploads\\Movie.MKV',
requiredHosters: ['doodstream.com', 'voe.sx', 'vidmoly.me', 'byse.sx'],
confirmedHosters: [],
fingerprint: null,
jobs: [
{ jobId: 'job-doodstream', hoster: 'doodstream.com', status: 'done', currentRound: false },
{ jobId: 'job-voe', hoster: 'voe.sx', status: 'done', currentRound: false },
{ jobId: 'job-vidmoly', hoster: 'vidmoly.me', status: 'error', currentRound: true },
{ jobId: 'job-byse', hoster: 'byse.sx', status: 'preview', currentRound: false }
]
});
assert.deepEqual(queueJobs.map((job) => job.sourceCleanupToken), [
'cleanup-1',
'cleanup-1',
'cleanup-1',
'cleanup-1'
]);
for (const job of queueJobs) {
assert.deepEqual(job.sourceCleanupRequiredHosters, [
'doodstream.com',
'voe.sx',
'vidmoly.me',
'byse.sx'
]);
}
assert.doesNotThrow(() => JSON.stringify(prepared.groups));
});
test('prepareGroups reuses confirmed metadata across a partial retry', () => {
const queueJobs = queueFixture();
policy.prepareGroups(queueJobs, queueJobs, () => 'cleanup-1', 'win32');
for (const job of queueJobs) {
job.sourceCleanupMetadataVersion = 2;
job.sourceCleanupConfirmedHosters = ['doodstream.com', 'voe.sx'];
}
queueJobs[0].status = 'preview';
queueJobs[1].status = 'preview';
const prepared = policy.prepareGroups(queueJobs, [queueJobs[2]], () => {
throw new Error('token must stay stable');
}, 'win32');
assert.equal(prepared.groups[0].token, 'cleanup-1');
assert.deepEqual(prepared.groups[0].requiredHosters, [
'doodstream.com',
'voe.sx',
'vidmoly.me',
'byse.sx'
]);
assert.deepEqual(prepared.groups[0].confirmedHosters, ['doodstream.com', 'voe.sx']);
assert.equal(prepared.groups[0].jobs.find((job) => job.hoster === 'vidmoly.me').status, 'error');
});
test('prepareGroups extends an active manifest when a new hoster is added', () => {
const queueJobs = queueFixture().slice(0, 2);
policy.prepareGroups(queueJobs, queueJobs, () => 'cleanup-1', 'win32');
queueJobs.push({ id: 'job-new', file: 'C:\\Uploads\\Movie.MKV', hoster: 'byse.sx', status: 'preview' });
const prepared = policy.prepareGroups(queueJobs, [queueJobs[2]], () => 'cleanup-2', 'win32');
assert.equal(prepared.groups[0].token, 'cleanup-1');
assert.deepEqual(prepared.groups[0].requiredHosters, ['doodstream.com', 'voe.sx', 'byse.sx']);
});
test('keeps case-distinct Windows paths in separate groups', () => {
const queueJobs = [
{ id: 'upper', file: 'C:\\CaseSensitive\\Movie.mkv', hoster: 'voe.sx', status: 'preview' },
{ id: 'lower', file: 'C:\\CaseSensitive\\movie.mkv', hoster: 'byse.sx', status: 'preview' }
];
const prepared = policy.prepareGroups(queueJobs, queueJobs, (file) => `token-${file}`, 'win32');
assert.equal(prepared.groups.length, 2);
assert.deepEqual(prepared.groups.map((group) => group.requiredHosters), [['voe.sx'], ['byse.sx']]);
});
test('removing a failed job never relaxes the stored requirements', () => {
const queueJobs = queueFixture();
policy.prepareGroups(queueJobs, queueJobs, () => 'cleanup-1', 'win32');
const failedJob = queueJobs[2];
const remainingJobs = queueJobs.filter((job) => job !== failedJob);
policy.removeRequirement(remainingJobs, failedJob, 'win32');
const prepared = policy.prepareGroups(remainingJobs, [remainingJobs[2]], () => {
throw new Error('token must stay stable');
}, 'win32');
assert.deepEqual(prepared.groups[0].requiredHosters, [
'doodstream.com',
'voe.sx',
'vidmoly.me',
'byse.sx'
]);
for (const job of remainingJobs) {
assert.deepEqual(job.sourceCleanupRequiredHosters, [
'doodstream.com',
'voe.sx',
'vidmoly.me',
'byse.sx'
]);
}
});
test('removeRequirement drops only an explicitly discarded unstarted hoster', () => {
const queueJobs = queueFixture();
policy.prepareGroups(queueJobs, queueJobs, () => 'cleanup-1', 'win32');
const touchedJobs = policy.removeRequirement(queueJobs, queueJobs[3], 'win32');
assert.equal(touchedJobs.length, 4);
for (const job of queueJobs) {
assert.deepEqual(job.sourceCleanupRequiredHosters, [
'doodstream.com',
'voe.sx',
'vidmoly.me'
]);
}
});
test('markCompleted keeps successful hosters provisional for the current round', () => {
const queueJobs = queueFixture();
policy.prepareGroups(queueJobs, queueJobs, () => 'cleanup-1', 'win32');
policy.markCompleted(queueJobs, queueJobs[2], 'win32');
policy.markCompleted(queueJobs, queueJobs[2], 'win32');
for (const job of queueJobs) {
assert.deepEqual(job.sourceCleanupConfirmedHosters, []);
assert.deepEqual(job.sourceCleanupProvisionalHosters, ['vidmoly.me']);
}
});
test('applyFingerprints attaches Main fingerprints by token and includes them in payloads', () => {
const queueJobs = queueFixture();
policy.prepareGroups(queueJobs, queueJobs, () => 'cleanup-1', 'win32');
const fingerprint = {
type: 'file',
size: 1048576,
mtimeMs: 1786467600000,
birthtimeMs: 1786467500000,
dev: 2114,
ino: 9001
};
const touchedJobs = policy.applyFingerprints(queueJobs, { 'cleanup-1': fingerprint });
const prepared = policy.prepareGroups(queueJobs, [queueJobs[3]], () => {
throw new Error('token must stay stable');
}, 'win32');
assert.equal(touchedJobs.length, 4);
assert.deepEqual(prepared.groups[0].fingerprint, fingerprint);
for (const job of queueJobs) {
assert.deepEqual(job.sourceCleanupFingerprint, fingerprint);
assert.notEqual(job.sourceCleanupFingerprint, fingerprint);
}
});
test('keeps round successes provisional until history and queue persistence succeed', async () => {
const queueJobs = [
{ id: 'job-voe', file: 'C:\\Uploads\\Round.bin', hoster: 'voe.sx', status: 'preview' },
{ id: 'job-byse', file: 'C:\\Uploads\\Round.bin', hoster: 'byse.sx', status: 'error' }
];
policy.prepareGroups(queueJobs, queueJobs, () => 'cleanup-round', 'win32');
queueJobs[0].status = 'done';
policy.markCompleted(queueJobs, queueJobs[0], 'win32');
for (const job of queueJobs) {
assert.deepEqual(job.sourceCleanupConfirmedHosters, []);
assert.deepEqual(job.sourceCleanupProvisionalHosters, ['voe.sx']);
}
let persistedHosters = null;
const queuePersisted = await policy.persistRoundCompletions(queueJobs, {
historyPersisted: true,
persist: async () => {
persistedHosters = queueJobs.map((job) => [...job.sourceCleanupConfirmedHosters]);
return true;
}
});
assert.equal(queuePersisted, true);
assert.deepEqual(persistedHosters, [['voe.sx'], ['voe.sx']]);
for (const job of queueJobs) {
assert.deepEqual(job.sourceCleanupConfirmedHosters, ['voe.sx']);
assert.deepEqual(job.sourceCleanupProvisionalHosters, []);
}
});
test('rolls back provisional promotion when the final queue save fails', async () => {
const queueJobs = [
{
id: 'job-voe',
file: 'C:\\Uploads\\Partial.bin',
hoster: 'voe.sx',
status: 'done',
sourceCleanupMetadataVersion: 2,
sourceCleanupToken: 'cleanup-partial',
sourceCleanupRequiredHosters: ['voe.sx', 'byse.sx'],
sourceCleanupConfirmedHosters: ['voe.sx']
},
{
id: 'job-byse',
file: 'C:\\Uploads\\Partial.bin',
hoster: 'byse.sx',
status: 'preview',
sourceCleanupMetadataVersion: 2,
sourceCleanupToken: 'cleanup-partial',
sourceCleanupRequiredHosters: ['voe.sx', 'byse.sx'],
sourceCleanupConfirmedHosters: ['voe.sx']
}
];
policy.prepareGroups(queueJobs, [queueJobs[1]], () => 'unused', 'win32');
queueJobs[1].status = 'done';
policy.markCompleted(queueJobs, queueJobs[1], 'win32');
const queuePersisted = await policy.persistRoundCompletions(queueJobs, {
historyPersisted: true,
persist: async () => false
});
assert.equal(queuePersisted, false);
for (const job of queueJobs) {
assert.deepEqual(job.sourceCleanupConfirmedHosters, ['voe.sx']);
assert.deepEqual(job.sourceCleanupProvisionalHosters, []);
}
});
test('ignores legacy v2.1.19 completed markers when preparing a retry', () => {
const queueJobs = [{
id: 'job-voe',
file: 'C:\\Uploads\\Legacy.bin',
hoster: 'voe.sx',
status: 'error',
sourceCleanupToken: 'cleanup-legacy',
sourceCleanupRequiredHosters: ['voe.sx'],
sourceCleanupCompletedHosters: ['voe.sx']
}];
const prepared = policy.prepareGroups(queueJobs, queueJobs, () => 'unused', 'win32');
assert.deepEqual(prepared.groups[0].confirmedHosters, []);
assert.equal(queueJobs[0].sourceCleanupMetadataVersion, 2);
assert.deepEqual(queueJobs[0].sourceCleanupConfirmedHosters, []);
assert.equal(Object.prototype.hasOwnProperty.call(queueJobs[0], 'sourceCleanupCompletedHosters'), false);
});
test('selecting a confirmed hoster for retry revokes its durable completion', () => {
const queueJobs = [
{
id: 'job-voe',
file: 'C:\\Uploads\\Retry.bin',
hoster: 'voe.sx',
status: 'preview',
sourceCleanupMetadataVersion: 2,
sourceCleanupToken: 'cleanup-retry',
sourceCleanupRequiredHosters: ['voe.sx', 'byse.sx'],
sourceCleanupConfirmedHosters: ['voe.sx', 'byse.sx']
},
{
id: 'job-byse',
file: 'C:\\Uploads\\Retry.bin',
hoster: 'byse.sx',
status: 'done',
sourceCleanupMetadataVersion: 2,
sourceCleanupToken: 'cleanup-retry',
sourceCleanupRequiredHosters: ['voe.sx', 'byse.sx'],
sourceCleanupConfirmedHosters: ['voe.sx', 'byse.sx']
}
];
const prepared = policy.prepareGroups(queueJobs, [queueJobs[0]], () => 'unused', 'win32');
assert.deepEqual(prepared.groups[0].confirmedHosters, ['byse.sx']);
assert.equal(prepared.groups[0].jobs[0].currentRound, true);
assert.equal(prepared.groups[0].jobs[1].currentRound, false);
for (const job of queueJobs) {
assert.deepEqual(job.sourceCleanupConfirmedHosters, ['byse.sx']);
}
});