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.
This commit is contained in:
@@ -48,13 +48,13 @@ test('prepareGroups creates one Windows manifest with a stable token and immutab
|
||||
token: 'cleanup-1',
|
||||
file: 'C:\\Uploads\\Movie.MKV',
|
||||
requiredHosters: ['doodstream.com', 'voe.sx', 'vidmoly.me', 'byse.sx'],
|
||||
completedHosters: ['doodstream.com', 'voe.sx'],
|
||||
confirmedHosters: [],
|
||||
fingerprint: null,
|
||||
jobs: [
|
||||
{ jobId: 'job-doodstream', hoster: 'doodstream.com', status: 'done' },
|
||||
{ jobId: 'job-voe', hoster: 'voe.sx', status: 'done' },
|
||||
{ jobId: 'job-vidmoly', hoster: 'vidmoly.me', status: 'error' },
|
||||
{ jobId: 'job-byse', hoster: 'byse.sx', status: 'preview' }
|
||||
{ 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), [
|
||||
@@ -74,11 +74,13 @@ test('prepareGroups creates one Windows manifest with a stable token and immutab
|
||||
assert.doesNotThrow(() => JSON.stringify(prepared.groups));
|
||||
});
|
||||
|
||||
test('prepareGroups reuses persisted metadata across a partial retry', () => {
|
||||
test('prepareGroups reuses confirmed metadata across a partial retry', () => {
|
||||
const queueJobs = queueFixture();
|
||||
policy.prepareGroups(queueJobs, queueJobs, () => 'cleanup-1', 'win32');
|
||||
queueJobs[0].sourceCleanupCompletedHosters = ['doodstream.com', 'voe.sx'];
|
||||
queueJobs[1].sourceCleanupCompletedHosters = ['doodstream.com', 'voe.sx'];
|
||||
for (const job of queueJobs) {
|
||||
job.sourceCleanupMetadataVersion = 2;
|
||||
job.sourceCleanupConfirmedHosters = ['doodstream.com', 'voe.sx'];
|
||||
}
|
||||
queueJobs[0].status = 'preview';
|
||||
queueJobs[1].status = 'preview';
|
||||
|
||||
@@ -93,7 +95,7 @@ test('prepareGroups reuses persisted metadata across a partial retry', () => {
|
||||
'vidmoly.me',
|
||||
'byse.sx'
|
||||
]);
|
||||
assert.deepEqual(prepared.groups[0].completedHosters, ['doodstream.com', 'voe.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');
|
||||
});
|
||||
|
||||
@@ -163,7 +165,7 @@ test('removeRequirement drops only an explicitly discarded unstarted hoster', ()
|
||||
}
|
||||
});
|
||||
|
||||
test('markCompleted preserves successful hosters for later retries', () => {
|
||||
test('markCompleted keeps successful hosters provisional for the current round', () => {
|
||||
const queueJobs = queueFixture();
|
||||
policy.prepareGroups(queueJobs, queueJobs, () => 'cleanup-1', 'win32');
|
||||
|
||||
@@ -171,11 +173,8 @@ test('markCompleted preserves successful hosters for later retries', () => {
|
||||
policy.markCompleted(queueJobs, queueJobs[2], 'win32');
|
||||
|
||||
for (const job of queueJobs) {
|
||||
assert.deepEqual(job.sourceCleanupCompletedHosters, [
|
||||
'doodstream.com',
|
||||
'voe.sx',
|
||||
'vidmoly.me'
|
||||
]);
|
||||
assert.deepEqual(job.sourceCleanupConfirmedHosters, []);
|
||||
assert.deepEqual(job.sourceCleanupProvisionalHosters, ['vidmoly.me']);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -203,3 +202,127 @@ test('applyFingerprints attaches Main fingerprints by token and includes them in
|
||||
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']);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ function group(file, overrides = {}) {
|
||||
token: 'cleanup-1',
|
||||
file,
|
||||
requiredHosters: ['voe.sx', 'byse.sx'],
|
||||
completedHosters: [],
|
||||
confirmedHosters: [],
|
||||
jobs: [
|
||||
{ jobId: 'job-voe', file, hoster: 'voe.sx', status: 'pending' },
|
||||
{ jobId: 'job-byse', file, hoster: 'byse.sx', status: 'pending' }
|
||||
@@ -81,7 +81,7 @@ test('fingerprints a regular file and keeps the registered manifest immutable',
|
||||
assert.equal(typeof fingerprints['cleanup-1'].ino, 'number');
|
||||
|
||||
manifest.requiredHosters.splice(1, 1);
|
||||
manifest.completedHosters.push('byse.sx');
|
||||
manifest.confirmedHosters.push('byse.sx');
|
||||
manifest.jobs[1].hoster = 'voe.sx';
|
||||
await cleanup.settle({
|
||||
token: 'cleanup-1',
|
||||
@@ -182,7 +182,13 @@ test('combines previous successes with a successful retry without relaxing other
|
||||
await t.test('last retry completes the immutable group', async (subtest) => {
|
||||
const { file } = await makeSource(subtest, 'retry-completes.bin');
|
||||
const { cleanup } = makeCleanup();
|
||||
const manifest = group(file, { completedHosters: ['voe.sx'] });
|
||||
const manifest = group(file, {
|
||||
confirmedHosters: ['voe.sx'],
|
||||
jobs: [
|
||||
{ jobId: 'job-voe', file, hoster: 'voe.sx', status: 'pending', currentRound: false },
|
||||
{ jobId: 'job-byse', file, hoster: 'byse.sx', status: 'pending', currentRound: true }
|
||||
]
|
||||
});
|
||||
await cleanup.registerGroups([manifest]);
|
||||
await cleanup.settle({ token: 'cleanup-1', jobId: 'job-byse', file, hoster: 'byse.sx', status: 'done' });
|
||||
|
||||
@@ -196,11 +202,11 @@ test('combines previous successes with a successful retry without relaxing other
|
||||
const { cleanup, audits } = makeCleanup();
|
||||
const manifest = group(file, {
|
||||
requiredHosters: ['voe.sx', 'byse.sx', 'vidmoly.me'],
|
||||
completedHosters: ['voe.sx'],
|
||||
confirmedHosters: ['voe.sx'],
|
||||
jobs: [
|
||||
{ jobId: 'job-voe', file, hoster: 'voe.sx', status: 'done' },
|
||||
{ jobId: 'job-byse', file, hoster: 'byse.sx', status: 'pending' },
|
||||
{ jobId: 'job-vidmoly', file, hoster: 'vidmoly.me', status: 'error' }
|
||||
{ jobId: 'job-voe', file, hoster: 'voe.sx', status: 'pending', currentRound: false },
|
||||
{ jobId: 'job-byse', file, hoster: 'byse.sx', status: 'pending', currentRound: true },
|
||||
{ jobId: 'job-vidmoly', file, hoster: 'vidmoly.me', status: 'error', currentRound: false }
|
||||
]
|
||||
});
|
||||
await cleanup.registerGroups([manifest]);
|
||||
@@ -467,3 +473,118 @@ test('rechecks the staged file and restores a replacement without deleting it',
|
||||
assert.equal((await fs.promises.readFile(file, 'utf-8')), 'replacement data');
|
||||
assert.equal(audits.at(-1).outcome, 'source-changed');
|
||||
});
|
||||
|
||||
test('keeps the source after done, failed history persistence, and an aborted retry of the same hoster', async (t) => {
|
||||
const { file } = await makeSource(t, 'history-barrier-retry.bin');
|
||||
const firstRound = makeCleanup();
|
||||
const firstManifest = group(file, {
|
||||
requiredHosters: ['voe.sx'],
|
||||
jobs: [{ jobId: 'job-voe', file, hoster: 'voe.sx', status: 'pending', currentRound: true }]
|
||||
});
|
||||
await firstRound.cleanup.registerGroups([firstManifest]);
|
||||
await firstRound.cleanup.settle({ token: 'cleanup-1', jobId: 'job-voe', file, hoster: 'voe.sx', status: 'done' });
|
||||
|
||||
assert.deepEqual(await firstRound.cleanup.finishBatch({ historyPersisted: false, queuePersisted: true }), ['blocked']);
|
||||
assert.equal(await exists(file), true);
|
||||
|
||||
const retryRound = makeCleanup();
|
||||
const retryManifest = group(file, {
|
||||
requiredHosters: ['voe.sx'],
|
||||
completedHosters: ['voe.sx'],
|
||||
jobs: [{ jobId: 'job-voe', file, hoster: 'voe.sx', status: 'pending', currentRound: true }]
|
||||
});
|
||||
await retryRound.cleanup.registerGroups([retryManifest]);
|
||||
await retryRound.cleanup.settle({ token: 'cleanup-1', jobId: 'job-voe', file, hoster: 'voe.sx', status: 'aborted' });
|
||||
|
||||
assert.deepEqual(await retryRound.cleanup.finishBatch({ historyPersisted: true, queuePersisted: true }), ['blocked']);
|
||||
assert.equal(await exists(file), true);
|
||||
});
|
||||
|
||||
test('deletes after a persisted partial round and a successful retry of the remaining hoster', async (t) => {
|
||||
const { file } = await makeSource(t, 'partial-round-retry.bin');
|
||||
const firstRound = makeCleanup();
|
||||
const firstManifest = group(file, {
|
||||
jobs: [
|
||||
{ jobId: 'job-voe', file, hoster: 'voe.sx', status: 'pending', currentRound: true },
|
||||
{ jobId: 'job-byse', file, hoster: 'byse.sx', status: 'pending', currentRound: true }
|
||||
]
|
||||
});
|
||||
await firstRound.cleanup.registerGroups([firstManifest]);
|
||||
await firstRound.cleanup.settle({ token: 'cleanup-1', jobId: 'job-voe', file, hoster: 'voe.sx', status: 'done' });
|
||||
await firstRound.cleanup.settle({ token: 'cleanup-1', jobId: 'job-byse', file, hoster: 'byse.sx', status: 'error' });
|
||||
|
||||
assert.deepEqual(await firstRound.cleanup.finishBatch({ historyPersisted: true, queuePersisted: true }), ['blocked']);
|
||||
assert.equal(await exists(file), true);
|
||||
|
||||
const retryRound = makeCleanup();
|
||||
const retryManifest = group(file, {
|
||||
confirmedHosters: ['voe.sx'],
|
||||
jobs: [
|
||||
{ jobId: 'job-voe', file, hoster: 'voe.sx', status: 'pending', currentRound: false },
|
||||
{ jobId: 'job-byse', file, hoster: 'byse.sx', status: 'pending', currentRound: true }
|
||||
]
|
||||
});
|
||||
await retryRound.cleanup.registerGroups([retryManifest]);
|
||||
await retryRound.cleanup.settle({ token: 'cleanup-1', jobId: 'job-byse', file, hoster: 'byse.sx', status: 'done' });
|
||||
|
||||
assert.deepEqual(await retryRound.cleanup.finishBatch({ historyPersisted: true, queuePersisted: true }), ['deleted']);
|
||||
assert.equal(await exists(file), false);
|
||||
});
|
||||
|
||||
test('does not trust a legacy completion after a failed queue barrier and restart', async (t) => {
|
||||
const { file } = await makeSource(t, 'queue-barrier-restart.bin');
|
||||
const firstRound = makeCleanup();
|
||||
const firstManifest = group(file, {
|
||||
requiredHosters: ['voe.sx'],
|
||||
jobs: [{ jobId: 'job-voe', file, hoster: 'voe.sx', status: 'pending', currentRound: true }]
|
||||
});
|
||||
await firstRound.cleanup.registerGroups([firstManifest]);
|
||||
await firstRound.cleanup.settle({ token: 'cleanup-1', jobId: 'job-voe', file, hoster: 'voe.sx', status: 'done' });
|
||||
|
||||
assert.deepEqual(await firstRound.cleanup.finishBatch({ historyPersisted: true, queuePersisted: false }), ['blocked']);
|
||||
assert.equal(await exists(file), true);
|
||||
|
||||
const restartedRound = makeCleanup();
|
||||
const restartedManifest = group(file, {
|
||||
requiredHosters: ['voe.sx'],
|
||||
completedHosters: ['voe.sx'],
|
||||
jobs: [{ jobId: 'job-voe', file, hoster: 'voe.sx', status: 'pending', currentRound: true }]
|
||||
});
|
||||
await restartedRound.cleanup.registerGroups([restartedManifest]);
|
||||
await restartedRound.cleanup.settle({ token: 'cleanup-1', jobId: 'job-voe', file, hoster: 'voe.sx', status: 'error' });
|
||||
|
||||
assert.deepEqual(await restartedRound.cleanup.finishBatch({ historyPersisted: true, queuePersisted: true }), ['blocked']);
|
||||
assert.equal(await exists(file), true);
|
||||
});
|
||||
|
||||
test('lets a current non-done retry override an earlier confirmed completion', async (t) => {
|
||||
const { file } = await makeSource(t, 'confirmed-hoster-retry.bin');
|
||||
const { cleanup, audits } = makeCleanup();
|
||||
const manifest = group(file, {
|
||||
requiredHosters: ['voe.sx'],
|
||||
confirmedHosters: ['voe.sx'],
|
||||
jobs: [{ jobId: 'job-voe', file, hoster: 'voe.sx', status: 'done', currentRound: true }]
|
||||
});
|
||||
await cleanup.registerGroups([manifest]);
|
||||
await cleanup.settle({ token: 'cleanup-1', jobId: 'job-voe', file, hoster: 'voe.sx', status: 'aborted' });
|
||||
|
||||
assert.deepEqual(await cleanup.finishBatch({ historyPersisted: true, queuePersisted: true }), ['blocked']);
|
||||
assert.equal(await exists(file), true);
|
||||
assert.deepEqual(audits[0].blockingStatuses, [{ hoster: 'voe.sx', status: 'aborted' }]);
|
||||
});
|
||||
|
||||
test('re-registering a hoster for the current round invalidates its earlier current success', async (t) => {
|
||||
const { file } = await makeSource(t, 'same-batch-retry.bin');
|
||||
const { cleanup, audits } = makeCleanup();
|
||||
const manifest = group(file, {
|
||||
requiredHosters: ['voe.sx'],
|
||||
jobs: [{ jobId: 'job-voe', file, hoster: 'voe.sx', status: 'pending', currentRound: true }]
|
||||
});
|
||||
await cleanup.registerGroups([manifest]);
|
||||
await cleanup.settle({ token: 'cleanup-1', jobId: 'job-voe', file, hoster: 'voe.sx', status: 'done' });
|
||||
await cleanup.registerGroups([manifest]);
|
||||
|
||||
assert.deepEqual(await cleanup.finishBatch({ historyPersisted: true, queuePersisted: true }), ['blocked']);
|
||||
assert.equal(await exists(file), true);
|
||||
assert.deepEqual(audits[0].blockingStatuses, [{ hoster: 'voe.sx', status: 'pending' }]);
|
||||
});
|
||||
|
||||
@@ -1711,6 +1711,35 @@ setTimeout(async () => {
|
||||
|
||||
const importedQueueMerge = await wc.executeJavaScript('(() => { queueJobs = [{ id: "ui-live-queue-after-import", file: "C:/ui/live-after-import.bin", fileName: "live-after-import.bin", hoster: "byse.sx", status: "queued", bytesTotal: 12 }]; rebuildJobIndex(); const imported = structuredClone(config); imported.globalSettings = { ...(imported.globalSettings || {}), webhookUrl: "https://queue-merge.invalid/imported", pendingQueue: { savedAt: 1, queueJobs: [{ id: "ui-stale-import-queue" }] } }; applyImportedConfig(imported, "Importiert"); return { webhookUrl: config.globalSettings.webhookUrl, ids: config.globalSettings.pendingQueue?.queueJobs?.map(job => job.id) || [] }; })()');
|
||||
check('Imported settings keep the live local queue in renderer memory', importedQueueMerge.webhookUrl === 'https://queue-merge.invalid/imported' && importedQueueMerge.ids.join('|') === 'ui-live-queue-after-import');
|
||||
const sourceCleanupDisabledRetry = await wc.executeJavaScript('(() => { const previousSetting = config.globalSettings?.deleteSourceAfterSuccessfulUpload; config.globalSettings = { ...(config.globalSettings || {}), deleteSourceAfterSuccessfulUpload: false }; queueJobs = [{ id: "ui-cleanup-disabled-retry", file: "C:/ui/cleanup-disabled.bin", fileName: "cleanup-disabled.bin", hoster: "voe.sx", status: "preview", bytesTotal: 10, sourceCleanupMetadataVersion: 2, sourceCleanupToken: "ui-cleanup-disabled-token", sourceCleanupRequiredHosters: ["voe.sx"], sourceCleanupConfirmedHosters: ["voe.sx"] }]; rebuildJobIndex(); const preparation = prepareSourceCleanup(queueJobs); config.globalSettings.deleteSourceAfterSuccessfulUpload = previousSetting; return { groups: preparation.groups.length, revokedHosters: preparation.revokedHosters || [], confirmedHosters: queueJobs[0].sourceCleanupConfirmedHosters || [] }; })()');
|
||||
check('Retry revokes a confirmed cleanup hoster even while source deletion is disabled', sourceCleanupDisabledRetry.groups === 0 && sourceCleanupDisabledRetry.revokedHosters.join('|') === 'voe.sx' && sourceCleanupDisabledRetry.confirmedHosters.length === 0);
|
||||
let sourceCleanupRevocationSaveCalls = 0;
|
||||
ipcMain.removeHandler('save-pending-queue');
|
||||
ipcMain.handle('save-pending-queue', async () => {
|
||||
sourceCleanupRevocationSaveCalls++;
|
||||
if (sourceCleanupRevocationSaveCalls === 1) throw new Error('injected cleanup revocation save failure');
|
||||
return true;
|
||||
});
|
||||
const sourceCleanupRevocationRetry = await wc.executeJavaScript('(async () => { queueJobs = [{ id: "ui-cleanup-revocation-retry", file: "C:/ui/cleanup-revocation-retry.bin", fileName: "cleanup-revocation-retry.bin", hoster: "voe.sx", status: "preview", bytesTotal: 10, sourceCleanupMetadataVersion: 2, sourceCleanupToken: "ui-cleanup-revocation-retry-token", sourceCleanupRequiredHosters: ["voe.sx"], sourceCleanupConfirmedHosters: ["voe.sx"] }]; rebuildJobIndex(); const firstPreparation = prepareSourceCleanup(queueJobs); let firstFailed = false; try { await persistSourceCleanupRevocations(firstPreparation); } catch { firstFailed = true; } const secondPreparation = prepareSourceCleanup(queueJobs); let secondSucceeded = true; try { await persistSourceCleanupRevocations(secondPreparation); } catch { secondSucceeded = false; } return { firstFailed, secondSucceeded, secondRevocations: secondPreparation.revokedHosters || [] }; })()');
|
||||
const sourceCleanupRevocationCallsBeforeRecovery = sourceCleanupRevocationSaveCalls;
|
||||
restoreInitialIpcHandler('save-pending-queue');
|
||||
await wc.executeJavaScript('flushConfigWrites()');
|
||||
check('A failed cleanup revocation save stays mandatory for the next start attempt', sourceCleanupRevocationRetry.firstFailed === true && sourceCleanupRevocationRetry.secondSucceeded === true && sourceCleanupRevocationRetry.secondRevocations.length === 0 && sourceCleanupRevocationCallsBeforeRecovery >= 2);
|
||||
const sourceCleanupFinalizationGate = await wc.executeJavaScript('(async () => { if (typeof sourceCleanupFinalizationPending === "undefined") return { available: false }; queueJobs = [{ id: "ui-cleanup-finalizing", file: "C:/ui/cleanup-finalizing.bin", fileName: "cleanup-finalizing.bin", hoster: "voe.sx", status: "done", bytesTotal: 10, result: { download_url: "https://example.invalid/finalizing" } }]; rebuildJobIndex(); selectedJobIds.clear(); selectedJobIds.add(queueJobs[0].id); sourceCleanupFinalizationPending = true; updateQueueActionButtons(); const disabled = document.getElementById("reuploadSelectedBtn")?.disabled === true; await retrySelectedJobs(); const status = queueJobs[0].status; sourceCleanupFinalizationPending = false; selectedJobIds.clear(); updateQueueActionButtons(); return { available: true, disabled, status }; })()');
|
||||
check('Final cleanup persistence blocks a new retry until the handshake settles', sourceCleanupFinalizationGate.available === true && sourceCleanupFinalizationGate.disabled === true && sourceCleanupFinalizationGate.status === 'done');
|
||||
let sourceCleanupFinalizationPayload = null;
|
||||
ipcMain.removeHandler('complete-upload-finalization');
|
||||
ipcMain.handle('complete-upload-finalization', (_event, payload) => {
|
||||
sourceCleanupFinalizationPayload = payload;
|
||||
return false;
|
||||
});
|
||||
const sourceCleanupRollback = await wc.executeJavaScript('(async () => { queuePersistThrottle.cancel(); await flushConfigWrites(); queueJobs = [{ id: "ui-cleanup-voe", file: "C:/ui/cleanup-round.bin", fileName: "cleanup-round.bin", hoster: "voe.sx", status: "preview", bytesTotal: 10 }, { id: "ui-cleanup-byse", file: "C:/ui/cleanup-round.bin", fileName: "cleanup-round.bin", hoster: "byse.sx", status: "error", bytesTotal: 10 }]; rebuildJobIndex(); window.SourceCleanupPolicy.prepareGroups(queueJobs, queueJobs, () => "ui-cleanup-token", "win32"); queueJobs[0].status = "done"; window.SourceCleanupPolicy.markCompleted(queueJobs, queueJobs[0], "win32"); const before = buildPersistedQueueState(); if (typeof completeSourceCleanupFinalization !== "function") return { available: false, before }; const result = await completeSourceCleanupFinalization({ finalizationId: "ui-cleanup-finalization", historyPersisted: true }); const after = buildPersistedQueueState(); return { available: true, result, before, after }; })()');
|
||||
const sourceCleanupBeforeJobs = sourceCleanupRollback.before?.queueJobs || [];
|
||||
const sourceCleanupAfterJobs = sourceCleanupRollback.after?.queueJobs || [];
|
||||
const sourceCleanupPromotedJobs = sourceCleanupFinalizationPayload?.pendingQueue?.queueJobs || [];
|
||||
const sourceCleanupRollbackOk = sourceCleanupRollback.available === true && sourceCleanupRollback.result === false && sourceCleanupBeforeJobs.length === 2 && sourceCleanupBeforeJobs.every(job => !Object.prototype.hasOwnProperty.call(job, 'sourceCleanupProvisionalHosters') && !Object.prototype.hasOwnProperty.call(job, 'sourceCleanupCompletedHosters') && (job.sourceCleanupConfirmedHosters || []).length === 0) && sourceCleanupPromotedJobs.length === 2 && sourceCleanupPromotedJobs.every(job => (job.sourceCleanupConfirmedHosters || []).join('|') === 'voe.sx') && sourceCleanupAfterJobs.length === 2 && sourceCleanupAfterJobs.every(job => (job.sourceCleanupConfirmedHosters || []).length === 0);
|
||||
check('Final queue persistence promotes only inside the handshake and rolls back failed saves', sourceCleanupRollbackOk);
|
||||
restoreInitialIpcHandler('complete-upload-finalization');
|
||||
await wc.executeJavaScript('document.getElementById("copyToast")?.classList.remove("show")');
|
||||
|
||||
console.log('\\n=== History View ===');
|
||||
|
||||
Reference in New Issue
Block a user