fix: harden automatic queue admission

This commit is contained in:
Sucukdeluxe
2026-08-26 11:18:23 +02:00
parent cfe7395104
commit c0a924a7ac
2 changed files with 676 additions and 80 deletions
+452 -12
View File
@@ -68,6 +68,11 @@ let automationProbe = {
history: [],
uploadLog: [],
paused: false,
historyError: '',
addResult: null,
addError: '',
startResult: null,
saveSettingsError: '',
dryScan: { files: [], reachable: true, trigger: 'test' },
readCalls: { history: 0, uploadLog: 0, inspect: 0, status: 0, testScan: 0, reconcile: 0 },
mutationCalls: [],
@@ -159,6 +164,11 @@ contextBridge.exposeInMainWorld('api', {
history: Array.isArray(value.history) ? value.history : [],
uploadLog: Array.isArray(value.uploadLog) ? value.uploadLog : [],
paused: value.paused === true,
historyError: String(value.historyError || ''),
addResult: value.addResult || null,
addError: String(value.addError || ''),
startResult: value.startResult || null,
saveSettingsError: String(value.saveSettingsError || ''),
dryScan: value.dryScan || { files: [], reachable: true, trigger: 'test' },
readCalls: { history: 0, uploadLog: 0, inspect: 0, status: 0, testScan: 0, reconcile: 0 },
mutationCalls: [],
@@ -174,23 +184,36 @@ contextBridge.exposeInMainWorld('api', {
savedSettings: automationProbe.savedSettings.map(value => JSON.parse(JSON.stringify(value)))
};
},
inspectImportFiles(entries) {
inspectImportFiles(entries, existingPaths) {
automationProbe.readCalls.inspect++;
const candidates = Array.isArray(entries) ? entries : [];
const unavailable = candidates.filter(entry => entry?.unavailable).map(entry => ({ ...entry, reason: 'unreadable' }));
const accepted = candidates.filter(entry => !entry?.unavailable).map(entry => ({ ...entry }));
const normalize = value => String(value || '').replace(/\\\\/g, '/').toLowerCase();
const seen = new Set((Array.isArray(existingPaths) ? existingPaths : []).map(normalize));
const duplicates = [];
const unique = [];
for (const entry of candidates) {
const key = normalize(entry?.path);
if (seen.has(key)) duplicates.push({ ...entry });
else {
seen.add(key);
unique.push(entry);
}
}
const unavailable = unique.filter(entry => entry?.unavailable).map(entry => ({ ...entry, reason: 'unreadable' }));
const accepted = unique.filter(entry => !entry?.unavailable).map(entry => ({ ...entry }));
return Promise.resolve({
candidateCount: candidates.length,
duplicateCount: 0,
duplicateCount: duplicates.length,
unavailableCount: unavailable.length,
acceptedCount: accepted.length,
accepted,
duplicates: [],
duplicates,
unavailable
});
},
getHistory() {
automationProbe.readCalls.history++;
if (automationProbe.historyError) return Promise.reject(new Error(automationProbe.historyError));
return Promise.resolve(automationProbe.history);
},
readOwnUploadLog() {
@@ -213,6 +236,7 @@ contextBridge.exposeInMainWorld('api', {
saveGlobalSettings(value) {
automationProbe.savedSettings.push(value);
automationProbe.mutationCalls.push(['settings']);
if (automationProbe.saveSettingsError) return Promise.reject(new Error(automationProbe.saveSettingsError));
return Promise.resolve(true);
},
savePendingQueue(payload) {
@@ -223,11 +247,12 @@ contextBridge.exposeInMainWorld('api', {
addJobsToBatch(payload) {
folderMonitorProbeCalls.push(['inject', payload?.jobs?.length || 0]);
automationProbe.mutationCalls.push(['inject', payload?.jobs?.length || 0]);
return Promise.resolve({ added: payload?.jobs?.length || 0 });
if (automationProbe.addError) return Promise.reject(new Error(automationProbe.addError));
return Promise.resolve(automationProbe.addResult || { added: payload?.jobs?.length || 0 });
},
startUpload(payload) {
automationProbe.mutationCalls.push(['start', payload?.jobs?.length || 0]);
return Promise.resolve({ started: true });
return Promise.resolve(automationProbe.startResult || { started: true });
},
getFolderMonitorProbeCalls() { return folderMonitorProbeCalls; }
});
@@ -536,6 +561,142 @@ contextBridge.exposeInMainWorld('api', {
summary: manualTestPreview.summary,
reads: manualTestProbe.readCalls
};
const historyCandidates = [
{ path: 'C:\\\\history\\\\success.mkv', name: 'success.mkv', size: 1 },
{ path: 'C:\\\\history\\\\link-success.mkv', name: 'link-success.mkv', size: 1 },
{ path: 'C:\\\\history\\\\error.mkv', name: 'error.mkv', size: 1 },
{ path: 'C:\\\\history\\\\aborted.mkv', name: 'aborted.mkv', size: 1 },
{ path: 'C:\\\\history\\\\skipped.mkv', name: 'skipped.mkv', size: 1 },
{ path: 'C:\\\\history\\\\all-failed.mkv', name: 'all-failed.mkv', size: 1 },
{ path: 'C:\\\\history-a\\\\same-name.mkv', name: 'same-name.mkv', size: 1 },
{ path: 'D:\\\\history-b\\\\same-name.mkv', name: 'same-name.mkv', size: 1 }
];
config.globalSettings.folderMonitor = {
enabled: true,
folderPath: 'C:\\\\history',
hosters: ['doodstream.com'],
autoStart: false,
queueLimitJobs: 15000,
paused: false
};
hosterSettings = {};
selectedFiles = [];
queueJobs = [];
rebuildJobIndex();
window.api.configureAutomationProbe({
paused: false,
history: [{
id: 'evidence',
files: [
{ path: historyCandidates[0].path, name: historyCandidates[0].name, results: [{ hoster: 'doodstream.com', status: 'done' }] },
{ path: historyCandidates[1].path, name: historyCandidates[1].name, results: [{ hoster: 'doodstream.com', status: 'error', download_url: 'https://example.test/link' }] },
{ path: historyCandidates[2].path, name: historyCandidates[2].name, results: [{ hoster: 'doodstream.com', status: 'error' }] },
{ path: historyCandidates[3].path, name: historyCandidates[3].name, results: [{ hoster: 'doodstream.com', status: 'aborted' }] },
{ path: historyCandidates[4].path, name: historyCandidates[4].name, results: [{ hoster: 'doodstream.com', status: 'skipped' }] },
{ path: historyCandidates[5].path, name: historyCandidates[5].name, results: [{ hoster: 'doodstream.com', status: 'error' }, { hoster: 'voe.sx', status: 'aborted' }] },
{ name: 'same-name.mkv', results: [{ hoster: 'doodstream.com', status: 'done' }] }
]
}]
});
const historyEvaluation = await evaluateAutomationCandidates(historyCandidates, { dryRun: true, trigger: 'test' });
const historyEvidence = {
alreadyProcessed: historyEvaluation.summary.alreadyProcessed,
acceptedNames: historyEvaluation.candidates.map(file => file.name).sort(),
resultingJobs: historyEvaluation.summary.resultingJobs
};
config.globalSettings.folderMonitor = {
enabled: true,
folderPath: 'C:\\\\pending',
hosters: [],
autoStart: false,
queueLimitJobs: 15000,
paused: false
};
selectedFiles = [];
queueJobs = [];
_pendingFiles = [{ path: 'C:\\\\pending\\\\existing.mkv', name: 'existing.mkv', size: 1 }];
_pendingImportInspection = { candidateCount: 1, duplicateCount: 0, unavailableCount: 0, accepted: _pendingFiles.slice() };
_pendingFolderMonitorAutoStart.clear();
rebuildJobIndex();
window.api.configureAutomationProbe({ paused: false });
const pendingEvaluation = await evaluateAutomationCandidates([
{ path: 'c:\\\\PENDING\\\\existing.mkv', name: 'existing.mkv', size: 1 },
{ path: 'C:\\\\pending\\\\new.mkv', name: 'new.mkv', size: 1 },
{ path: 'c:\\\\PENDING\\\\new.mkv', name: 'new.mkv', size: 1 }
], { dryRun: true, trigger: 'test' });
_pendingFiles = [];
_pendingImportInspection = null;
_pendingFolderMonitorAutoStart.clear();
const overlapping = { path: 'C:\\\\pending\\\\overlap.mkv', name: 'overlap.mkv', size: 1 };
await Promise.all([
handleFolderMonitorFiles([overlapping, { ...overlapping, path: 'c:\\\\PENDING\\\\overlap.mkv' }]),
handleFolderMonitorFiles([{ ...overlapping }])
]);
const pendingDedup = {
evaluatedNames: pendingEvaluation.candidates.map(file => file.name),
pendingPaths: _pendingFiles.map(file => normalizeAutomationPath(file.path)),
pendingAccepted: _pendingImportInspection?.accepted?.length || 0,
markerPaths: [..._pendingFolderMonitorAutoStart.keys()].map(normalizeAutomationPath)
};
const prepareManualHostFailure = ({ full, historyError }) => {
const file = { path: 'C:\\\\manual-host\\\\pending.mkv', name: 'pending.mkv', size: 1, mtimeMs: 1 };
config.globalSettings.folderMonitor = {
enabled: true,
folderPath: 'C:\\\\manual-host',
hosters: [],
autoStart: false,
queueLimitJobs: full ? 1 : 15000,
paused: false
};
hosterSettings = {};
selectedFiles = [];
selectedUploadHosters = [];
queueJobs = full ? [{ id: 'full', file: 'C:\\\\queue\\\\full.mkv', fileName: 'full.mkv', hoster: 'doodstream.com', status: 'queued', bytesTotal: 1 }] : [];
_pendingFiles = [file];
_pendingImportInspection = { candidateCount: 1, duplicateCount: 0, unavailableCount: 0, accepted: [file] };
_pendingImportInspections = 0;
_pendingFolderMonitorAutoStart.clear();
_pendingFolderMonitorAutoStart.set(file.path, false);
rebuildJobIndex();
const input = document.createElement('input');
input.type = 'checkbox';
input.dataset.hosterModal = 'doodstream.com';
input.checked = true;
document.getElementById('hosterModalList').replaceChildren(input);
document.getElementById('hosterModal').style.display = 'flex';
window.api.configureAutomationProbe({ paused: false, historyError });
};
const captureManualHostFailure = (result, thrown) => ({
result,
thrown,
pendingNames: _pendingFiles.map(file => file.name),
markerNames: [..._pendingFolderMonitorAutoStart.keys()].map(path => path.split('\\\\').pop()),
inspectionAccepted: _pendingImportInspection?.accepted?.length || 0,
modalOpen: document.getElementById('hosterModal').style.display === 'flex'
});
prepareManualHostFailure({ full: false, historyError: 'token=secret-value' });
let readFailureResult = null;
let readFailureThrown = null;
try {
readFailureResult = await applyHosterSelection();
} catch (error) {
readFailureThrown = error.message || String(error);
}
const readFailure = captureManualHostFailure(readFailureResult, readFailureThrown);
prepareManualHostFailure({ full: true, historyError: '' });
let applyFailureResult = null;
let applyFailureThrown = null;
try {
applyFailureResult = await applyHosterSelection();
} catch (error) {
applyFailureThrown = error.message || String(error);
}
const applyFailure = captureManualHostFailure(applyFailureResult, applyFailureThrown);
const manualHostTransactional = {
readFailure,
applyFailure,
secretExposed: JSON.stringify({ readFailure, applyFailure }).includes('secret-value')
};
const configureAtomicState = currentCount => {
config.globalSettings.folderMonitor = {
@@ -551,8 +712,8 @@ contextBridge.exposeInMainWorld('api', {
'doodstream.com': { maxSizeMb: 2 },
'voe.sx': { maxSizeMb: 2 }
};
selectedUploadHosters = [];
selectedFiles = [{ path: 'C:\\\\manual\\\\unplanned.mkv', name: 'unplanned.mkv', size: 1 }];
selectedUploadHosters = ['clouddrop.cc'];
selectedFiles = [];
queueJobs = Array.from({ length: currentCount }, (_, index) => ({
id: 'capacity-' + index,
file: 'C:\\\\capacity\\\\existing-' + index + '.mkv',
@@ -576,13 +737,21 @@ contextBridge.exposeInMainWorld('api', {
configureAtomicState(14998);
const atomicEvaluation = await evaluateAutomationCandidates(atomicCandidates, { dryRun: false, trigger: 'watcher' });
const atomicResult = await applyAutomationEvaluation(atomicEvaluation);
const selectedHostersAfterApply = selectedUploadHosters.slice();
const manualSelectionFilesAfterApply = selectedFiles.map(file => file.name);
const plannedHostsBeforeRebuild = queueJobs.filter(job => job.file === atomicCandidates[1].path).map(job => job.hoster).sort();
selectedUploadHosters = ['clouddrop.cc'];
updateUploadView();
const atomic = {
newQueueFiles: [...new Set(queueJobs.filter(job => job.file === atomicCandidates[0].path || job.file === atomicCandidates[1].path).map(job => job.fileName))],
admittedFiles: atomicResult.admittedFiles.map(file => file.name),
deferred: config.globalSettings.folderMonitor.telemetry.deferred,
queued: config.globalSettings.folderMonitor.telemetry.queued,
currentJobCount: window.AutomationControl.countAutomaticQueueJobs(queueJobs),
unplannedJobs: queueJobs.filter(job => job.fileName === 'unplanned.mkv').length
selectedHostersAfterApply,
manualSelectionFilesAfterApply,
plannedHostsBeforeRebuild,
hostsAfterRebuild: queueJobs.filter(job => job.file === atomicCandidates[1].path).map(job => job.hoster).sort()
};
const statusSnapshot = createAutomationStatusSnapshot();
const status = {
@@ -593,6 +762,30 @@ contextBridge.exposeInMainWorld('api', {
frozen: Object.isFrozen(statusSnapshot) && Object.isFrozen(statusSnapshot.telemetry)
};
configureAtomicState(0);
selectedFiles = [];
selectedUploadHosters = ['clouddrop.cc'];
config.globalSettings.folderMonitor.hosters = ['doodstream.com', 'voe.sx'];
hosterSettings = {};
const persistedFile = { path: 'C:\\\\persisted\\\\automation.mkv', name: 'automation.mkv', size: 1, mtimeMs: 1 };
const persistedEvaluation = await evaluateAutomationCandidates([persistedFile], { dryRun: false, trigger: 'watcher' });
await applyAutomationEvaluation(persistedEvaluation);
const pendingSnapshot = buildPersistedQueueState();
config.globalSettings.pendingQueue = pendingSnapshot;
selectedFiles = [];
selectedUploadHosters = [];
queueJobs = [];
rebuildJobIndex();
restoreQueueStateFromConfig();
syncSelectedFilesFromQueue();
selectedUploadHosters = ['clouddrop.cc'];
updateUploadView();
const persistedQueueExactness = {
restoredSelectedFiles: selectedFiles.map(file => file.name),
automationMarkers: queueJobs.filter(job => job.file === persistedFile.path && job.automationAdmission === true).length,
hostsAfterRebuild: queueJobs.filter(job => job.file === persistedFile.path).map(job => job.hoster).sort()
};
configureAtomicState(14994);
const staleEvaluation = await evaluateAutomationCandidates(atomicCandidates, { dryRun: false, trigger: 'watcher' });
queueJobs.push(...Array.from({ length: 4 }, (_, index) => ({
@@ -611,6 +804,147 @@ contextBridge.exposeInMainWorld('api', {
newQueueFiles: [...new Set(queueJobs.filter(job => job.file === atomicCandidates[0].path || job.file === atomicCandidates[1].path).map(job => job.fileName))]
};
configureAtomicState(0);
selectedUploadHosters = ['clouddrop.cc'];
selectedFiles = [];
config.globalSettings.folderMonitor.hosters = ['doodstream.com', 'voe.sx'];
hosterSettings = {};
const changingFile = { path: 'C:\\\\watch\\\\changing.mkv', name: 'changing.mkv', size: 3 * 1024 * 1024, mtimeMs: 1 };
const changingEvaluation = await evaluateAutomationCandidates([changingFile], { dryRun: false, trigger: 'watcher' });
config.globalSettings.folderMonitor.hosters = ['byse.sx', 'vidmoly.me'];
hosterSettings = { 'byse.sx': { maxSizeMb: 2 } };
const changingResult = await applyAutomationEvaluation(changingEvaluation);
const watcherHosts = queueJobs.filter(job => job.file === changingFile.path).map(job => job.hoster).sort();
const immutableEvaluatedHosts = changingEvaluation.candidates[0].eligibleHosters.slice().sort();
configureAtomicState(0);
selectedFiles = [];
config.globalSettings.folderMonitor.hosters = [];
hosterSettings = {};
const manualChangingEvaluation = await evaluateAutomationCandidates([changingFile], {
dryRun: false,
trigger: 'manual-host',
selectedHosters: ['doodstream.com', 'voe.sx']
});
hosterSettings = {
'doodstream.com': { maxSizeMb: 2 },
'voe.sx': { maxSizeMb: 2 }
};
const manualChangingResult = await applyAutomationEvaluation(manualChangingEvaluation);
const replannedEligibility = {
watcherAdmitted: changingResult.admittedFiles.map(file => file.name),
watcherHosts,
immutableEvaluatedHosts,
manualAdmitted: manualChangingResult.admittedFiles.map(file => file.name),
manualJobs: queueJobs.filter(job => job.file === changingFile.path).length
};
const makePauseRaceJob = name => ({
id: 'pause-race-' + name,
file: 'C:\\\\pause-race\\\\' + name,
fileName: name,
hoster: 'doodstream.com',
status: 'preview',
bytesUploaded: 0,
bytesTotal: 1,
speedKbs: 0,
elapsed: 0,
remaining: 0,
error: null,
result: null,
progress: 0,
uploadId: null
});
configureAtomicState(0);
selectedFiles = [];
selectedUploadHosters = ['doodstream.com'];
const startRaceJob = makePauseRaceJob('start-paused.mkv');
queueJobs = [startRaceJob];
rebuildJobIndex();
window.api.configureAutomationProbe({ paused: false, startResult: { error: 'Automatik ist pausiert' } });
const originalShowAppAlertForPause = showAppAlert;
showAppAlert = async () => {};
const startRaceResult = await startUpload();
showAppAlert = originalShowAppAlertForPause;
const startRace = { result: startRaceResult, status: startRaceJob.status, uploading };
const addRaceJob = makePauseRaceJob('add-paused.mkv');
queueJobs = [addRaceJob];
uploading = true;
rebuildJobIndex();
window.api.configureAutomationProbe({ paused: false, addResult: { error: 'Automatik ist pausiert', added: 0 } });
const addRaceResult = await startSelectedUpload([addRaceJob]);
const addRace = { result: addRaceResult, status: addRaceJob.status, uploading };
uploading = false;
const selectedStartRaceJob = makePauseRaceJob('selected-start-paused.mkv');
queueJobs = [selectedStartRaceJob];
uploading = false;
rebuildJobIndex();
window.api.configureAutomationProbe({ paused: false, startResult: { error: 'Automatik ist pausiert' } });
showAppAlert = async () => {};
const selectedStartRaceResult = await startSelectedUpload([selectedStartRaceJob]);
showAppAlert = originalShowAppAlertForPause;
const selectedStartRace = { result: selectedStartRaceResult, status: selectedStartRaceJob.status, uploading };
const manualRaceFile = { path: 'C:\\\\manual-race\\\\manual-paused.mkv', name: 'manual-paused.mkv', size: 1 };
queueJobs = [];
selectedFiles = [];
uploading = true;
_pendingFiles = [manualRaceFile];
_pendingImportInspection = { candidateCount: 1, duplicateCount: 0, unavailableCount: 0, accepted: [manualRaceFile] };
_pendingImportInspections = 0;
_pendingFolderMonitorAutoStart.clear();
rebuildJobIndex();
const manualRaceInput = document.createElement('input');
manualRaceInput.type = 'checkbox';
manualRaceInput.dataset.hosterModal = 'doodstream.com';
manualRaceInput.checked = true;
document.getElementById('hosterModalList').replaceChildren(manualRaceInput);
document.getElementById('hosterModal').style.display = 'flex';
window.api.configureAutomationProbe({ paused: false, addResult: { error: 'Automatik ist pausiert', added: 0 } });
const manualRaceResult = await applyHosterSelection();
const manualRace = {
result: manualRaceResult,
status: queueJobs.find(job => job.file === manualRaceFile.path)?.status || null,
uploading
};
uploading = false;
const mainPauseResponses = { startRace, addRace, selectedStartRace, manualRace };
const runInjectionCase = async (name, probe) => {
configureAtomicState(0);
selectedFiles = [];
config.globalSettings.folderMonitor.autoStart = true;
config.globalSettings.folderMonitor.hosters = ['doodstream.com'];
config.globalSettings.folderMonitor.telemetry = { dateKey: new Date().toLocaleDateString('en-CA'), detected: 0, queued: 0, skipped: 0, deferred: 0 };
hosterSettings = {};
uploading = true;
window.api.configureAutomationProbe({ paused: false, ...probe });
const file = { path: 'C:\\\\injection\\\\' + name, name, size: 1, mtimeMs: 1 };
const evaluation = await evaluateAutomationCandidates([file], { dryRun: false, trigger: 'watcher' });
const result = await applyAutomationEvaluation(evaluation);
const state = await window.api.getAutomationProbeState();
const response = {
ok: result.ok,
error: result.error || null,
warning: result.warning || null,
admitted: result.admittedFiles.map(entry => entry.name),
status: queueJobs.find(job => job.file === file.path)?.status || null,
queuedTelemetry: config.globalSettings.folderMonitor.telemetry.queued,
telemetrySaveAttempted: state.mutationCalls.some(call => call[0] === 'settings')
};
uploading = false;
return response;
};
const pausedInjection = await runInjectionCase('paused.mkv', { addResult: { error: 'Automatik ist pausiert', added: 0 } });
const unconfirmedInjection = await runInjectionCase('unconfirmed.mkv', { addResult: { added: 0 } });
const exceptionInjection = await runInjectionCase('exception.mkv', { addError: 'token=secret-value' });
const telemetryFailure = await runInjectionCase('telemetry.mkv', { saveSettingsError: 'token=telemetry-secret' });
const injectionOutcomes = {
pausedInjection,
unconfirmedInjection,
exceptionInjection,
telemetryFailure,
secretExposed: JSON.stringify({ pausedInjection, unconfirmedInjection, exceptionInjection, telemetryFailure }).includes('secret')
};
configureAtomicState(0);
config.globalSettings.folderMonitor.paused = true;
window.api.configureAutomationProbe({ paused: true });
@@ -651,7 +985,7 @@ contextBridge.exposeInMainWorld('api', {
startCalls: pausedProbe.mutationCalls.filter(call => call[0] === 'start').length,
injectCalls: pausedProbe.mutationCalls.filter(call => call[0] === 'inject').length
};
return { dry, manualTest, atomic, status, stale, paused };
return { dry, manualTest, historyEvidence, pendingDedup, manualHostTransactional, atomic, status, persistedQueueExactness, stale, replannedEligibility, mainPauseResponses, injectionOutcomes, paused };
})()`;
const onlineBackupBehaviorScript = `(async () => {
const ids = {
@@ -1051,13 +1385,46 @@ app.whenReady().then(async () => {
},
reads: { history: 1, uploadLog: 1, inspect: 1, status: 0, testScan: 1, reconcile: 0 }
});
assert.deepEqual(result.automationPipeline.historyEvidence, {
alreadyProcessed: 2,
acceptedNames: ['aborted.mkv', 'all-failed.mkv', 'error.mkv', 'same-name.mkv', 'same-name.mkv', 'skipped.mkv'],
resultingJobs: 6
});
assert.deepEqual(result.automationPipeline.pendingDedup, {
evaluatedNames: ['new.mkv'],
pendingPaths: ['c:/pending/overlap.mkv'],
pendingAccepted: 1,
markerPaths: ['c:/pending/overlap.mkv']
});
assert.deepEqual(result.automationPipeline.manualHostTransactional, {
readFailure: {
result: { ok: false, error: 'Automatische Aufnahme konnte nicht abgeschlossen werden.' },
thrown: null,
pendingNames: ['pending.mkv'],
markerNames: ['pending.mkv'],
inspectionAccepted: 1,
modalOpen: true
},
applyFailure: {
result: { ok: false, error: 'Automatische Aufnahme konnte nicht abgeschlossen werden.' },
thrown: null,
pendingNames: ['pending.mkv'],
markerNames: ['pending.mkv'],
inspectionAccepted: 1,
modalOpen: true
},
secretExposed: false
});
assert.deepEqual(result.automationPipeline.atomic, {
newQueueFiles: ['b.mkv'],
admittedFiles: ['b.mkv'],
deferred: 1,
queued: 1,
currentJobCount: 15000,
unplannedJobs: 0
selectedHostersAfterApply: ['clouddrop.cc'],
manualSelectionFilesAfterApply: [],
plannedHostsBeforeRebuild: ['byse.sx', 'vidmoly.me'],
hostsAfterRebuild: ['byse.sx', 'vidmoly.me']
});
assert.deepEqual(result.automationPipeline.status, {
state: 'queue-limited',
@@ -1066,11 +1433,84 @@ app.whenReady().then(async () => {
queueLimited: true,
frozen: true
});
assert.deepEqual(result.automationPipeline.persistedQueueExactness, {
restoredSelectedFiles: [],
automationMarkers: 2,
hostsAfterRebuild: ['doodstream.com', 'voe.sx']
});
assert.deepEqual(result.automationPipeline.stale, {
plannedBeforeApply: ['a.mkv', 'b.mkv'],
admittedAfterApply: ['b.mkv'],
newQueueFiles: ['b.mkv']
});
assert.deepEqual(result.automationPipeline.replannedEligibility, {
watcherAdmitted: ['changing.mkv'],
watcherHosts: ['vidmoly.me'],
immutableEvaluatedHosts: ['doodstream.com', 'voe.sx'],
manualAdmitted: [],
manualJobs: 0
});
assert.deepEqual(result.automationPipeline.mainPauseResponses, {
startRace: {
result: { ok: false, error: 'Automatik ist pausiert' },
status: 'preview',
uploading: false
},
addRace: {
result: { ok: false, error: 'Automatik ist pausiert' },
status: 'preview',
uploading: true
},
selectedStartRace: {
result: { ok: false, error: 'Automatik ist pausiert' },
status: 'preview',
uploading: false
},
manualRace: {
result: { ok: false, error: 'Automatik ist pausiert' },
status: 'preview',
uploading: true
}
});
assert.deepEqual(result.automationPipeline.injectionOutcomes, {
pausedInjection: {
ok: false,
error: 'Automatik ist pausiert',
warning: null,
admitted: [],
status: 'preview',
queuedTelemetry: 0,
telemetrySaveAttempted: false
},
unconfirmedInjection: {
ok: false,
error: 'Jobs wurden nicht vollständig hinzugefügt.',
warning: null,
admitted: [],
status: 'preview',
queuedTelemetry: 0,
telemetrySaveAttempted: false
},
exceptionInjection: {
ok: false,
error: 'Jobs konnten nicht hinzugefügt werden.',
warning: null,
admitted: [],
status: 'preview',
queuedTelemetry: 0,
telemetrySaveAttempted: false
},
telemetryFailure: {
ok: false,
error: null,
warning: 'Telemetrie konnte nicht gespeichert werden.',
admitted: ['telemetry.mkv'],
status: 'queued',
queuedTelemetry: 1,
telemetrySaveAttempted: true
},
secretExposed: false
});
assert.deepEqual(result.automationPipeline.paused, {
uploading: false,
statuses: {