const test = require('node:test'); const assert = require('node:assert/strict'); const { EventEmitter } = require('node:events'); const { execFileSync, spawnSync } = require('node:child_process'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const { createStartupWindow, resolveStartupLanguage, createStartupQuery } = require('../lib/startup-renderer'); class TestBrowserWindow extends EventEmitter { constructor(options) { super(); this.webContents = new EventEmitter(); this.options = options; this.showCalls = 0; this.startupEvents = []; this.loadError = new Error('renderer load failed'); this.loadOptions = null; } once(eventName, listener) { this.startupEvents.push(`listen:${eventName}`); return super.once(eventName, listener); } show() { this.showCalls++; } loadFile(target, options) { this.startupEvents.push(`load:${target}`); this.loadOptions = options; return this.loadError ? Promise.reject(this.loadError) : Promise.resolve(); } } test('production startup never forces software compositing', () => { const projectRoot = path.join(__dirname, '..'); const pending = [path.join(projectRoot, 'main.js'), path.join(projectRoot, 'lib')]; const sourceFiles = []; while (pending.length) { const target = pending.pop(); const stat = fs.statSync(target); if (stat.isDirectory()) { for (const entry of fs.readdirSync(target)) pending.push(path.join(target, entry)); } else if (target.endsWith('.js')) { sourceFiles.push(target); } } for (const sourceFile of sourceFiles) { const source = fs.readFileSync(sourceFile, 'utf8'); assert.doesNotMatch(source, /disableHardwareAcceleration|disable-gpu(?:-compositing)?/u, path.relative(projectRoot, sourceFile)); } }); test('Windows compositor paints the full hidden surface with an RDP session environment', { skip: process.platform !== 'win32' }, () => { const projectRoot = path.join(__dirname, '..'); const probeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mhu-rdp-compositor-')); const probePath = path.join(probeRoot, 'probe.cjs'); const preloadPath = path.join(probeRoot, 'preload.cjs'); const outputPath = path.join(probeRoot, 'result.json'); const userDataPath = path.join(probeRoot, 'user-data'); fs.writeFileSync(preloadPath, ` const { contextBridge } = require('electron'); const managedOnlineBackupProbeCalls = []; const folderMonitorProbeCalls = []; let automationProbe = { history: [], uploadLog: [], paused: false, automationStatusSequence: [], historyError: '', addResult: null, addMode: '', addError: '', startResult: null, startError: '', saveSettingsError: '', dryScan: { files: [], reachable: true, trigger: 'test' }, readCalls: { history: 0, uploadLog: 0, inspect: 0, status: 0, testScan: 0, reconcile: 0 }, mutationCalls: [], logs: [], savedSettings: [] }; const managedOnlineBackupIds = { a: 'AAAAAAAAAAAAAAAAAAAAAA', b: 'AQEBAQEBAQEBAQEBAQEBAQ', c: 'AgICAgICAgICAgICAgICAg', d: 'AwMDAwMDAwMDAwMDAwMDAw', e: 'BAQEBAQEBAQEBAQEBAQEBA' }; const managedOnlineBackupListResponses = [ { ok: true, warningCode: 'KEYRING_DECRYPT_FAILED', warning: 'Gespeicherter Online-Sicherungsschlüssel konnte nicht entschlüsselt werden', entries: [ { id: managedOnlineBackupIds.a, displayKey: 'MHU2-ABCD…1234', createdAt: '2026-08-20T08:00:00.000Z' }, { id: managedOnlineBackupIds.b, displayKey: 'MHU2-ZYXW…9876', createdAt: '2026-08-22T10:00:00.000Z' }, { id: 'BBBBBBBBBBBBBBBBBBBBBB', displayKey: 'MHU2-FAIL…1111', createdAt: '2026-08-23T10:00:00.000Z' }, { id: managedOnlineBackupIds.c, displayKey: 'invalid', createdAt: '2026-08-24T10:00:00.000Z' } ] }, { ok: false, entries: [], code: 'KEYRING_DECRYPT_FAILED', error: 'Gespeicherter Online-Sicherungsschlüssel konnte nicht entschlüsselt werden' }, { ok: false, entries: [], code: 'KEYRING_DECRYPT_FAILED', error: 'Gespeicherter Online-Sicherungsschlüssel konnte nicht entschlüsselt werden' }, { ok: true, entries: [ { id: managedOnlineBackupIds.c, displayKey: 'MHU2-QWER…4321', createdAt: '2026-08-23T12:00:00.000Z' }, { id: managedOnlineBackupIds.a, displayKey: 'MHU2-ABCD…1234', createdAt: '2026-08-20T08:00:00.000Z' } ] }, null, { ok: false, entries: [], code: 'KEYRING_ID_MISMATCH', error: 'Gespeicherte Online-Sicherungskennung stimmt nicht mit dem Schlüssel überein' } ]; let managedOnlineBackupListIndex = 0; let managedOnlineBackupCreateIndex = 0; const pendingManagedOnlineBackupLists = new Map(); let pendingManagedOnlineBackupCopy = null; let pendingManagedOnlineBackupDelete = null; contextBridge.exposeInMainWorld('api', { onUpdateAvailable() {}, onUpdateProgress() {}, onPrepareClose() {}, getConfig() { return new Promise(() => {}); }, remoteStatus() { return Promise.resolve({ running: false, port: 0, clientCount: 0 }); }, diagnosticsStatus() { return Promise.resolve({ running: false }); }, diagnosticsGetSettings() { return Promise.resolve({}); }, listManagedOnlineBackups() { managedOnlineBackupListIndex++; managedOnlineBackupProbeCalls.push(['list', managedOnlineBackupListIndex]); if (managedOnlineBackupListIndex === 5) { return new Promise(resolve => { pendingManagedOnlineBackupLists.set(managedOnlineBackupListIndex, resolve); }); } return Promise.resolve(managedOnlineBackupListResponses[managedOnlineBackupListIndex - 1]); }, releaseManagedOnlineBackupList(index) { const resolve = pendingManagedOnlineBackupLists.get(index); pendingManagedOnlineBackupLists.delete(index); resolve({ ok: true, entries: [ { id: managedOnlineBackupIds.e, displayKey: 'MHU2-AUTH…9999', createdAt: '2026-08-26T10:00:00.000Z' }, { id: managedOnlineBackupIds.d, displayKey: 'MHU2-DFGH…2468', createdAt: '2026-08-25T10:00:00.000Z' }, { id: managedOnlineBackupIds.a, displayKey: 'MHU2-ABCD…1234', createdAt: '2026-08-20T08:00:00.000Z' } ] }); }, createManagedOnlineBackup() { managedOnlineBackupCreateIndex++; managedOnlineBackupProbeCalls.push(['create', managedOnlineBackupCreateIndex]); const entry = managedOnlineBackupCreateIndex === 1 ? { id: managedOnlineBackupIds.c, displayKey: 'MHU2-QWER…4321', createdAt: '2026-08-23T12:00:00.000Z' } : { id: managedOnlineBackupIds.d, displayKey: 'MHU2-DFGH…2468', createdAt: '2026-08-25T10:00:00.000Z' }; return Promise.resolve({ ok: true, entry }); }, copyManagedOnlineBackup(id) { managedOnlineBackupProbeCalls.push(['copy', id]); return new Promise(resolve => { pendingManagedOnlineBackupCopy = resolve; }); }, releaseManagedOnlineBackupCopy() { const resolve = pendingManagedOnlineBackupCopy; pendingManagedOnlineBackupCopy = null; resolve({ ok: true }); }, deleteManagedOnlineBackup(id) { managedOnlineBackupProbeCalls.push(['delete', id]); return new Promise(resolve => { pendingManagedOnlineBackupDelete = { id, resolve }; }); }, releaseManagedOnlineBackupDelete() { const pending = pendingManagedOnlineBackupDelete; pendingManagedOnlineBackupDelete = null; pending.resolve({ ok: true, removedId: pending.id, notFound: false }); }, getManagedOnlineBackupProbeCalls() { return managedOnlineBackupProbeCalls; }, configureAutomationProbe(value = {}) { automationProbe = { history: Array.isArray(value.history) ? value.history : [], uploadLog: Array.isArray(value.uploadLog) ? value.uploadLog : [], paused: value.paused === true, automationStatusSequence: Array.isArray(value.automationStatusSequence) ? value.automationStatusSequence.map(entry => ({ ...entry })) : [], historyError: String(value.historyError || ''), addResult: value.addResult || null, addMode: String(value.addMode || ''), addError: String(value.addError || ''), startResult: value.startResult || null, startError: String(value.startError || ''), 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: [], logs: [], savedSettings: [] }; }, getAutomationProbeState() { return { readCalls: { ...automationProbe.readCalls }, mutationCalls: automationProbe.mutationCalls.map(value => [...value]), logs: [...automationProbe.logs], savedSettings: automationProbe.savedSettings.map(value => JSON.parse(JSON.stringify(value))) }; }, inspectImportFiles(entries, existingPaths) { automationProbe.readCalls.inspect++; const candidates = Array.isArray(entries) ? entries : []; 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: duplicates.length, unavailableCount: unavailable.length, acceptedCount: accepted.length, accepted, duplicates, unavailable }); }, getHistory() { automationProbe.readCalls.history++; if (automationProbe.historyError) return Promise.reject(new Error(automationProbe.historyError)); return Promise.resolve(automationProbe.history); }, readOwnUploadLog() { automationProbe.readCalls.uploadLog++; return Promise.resolve(automationProbe.uploadLog); }, automationGetStatus() { automationProbe.readCalls.status++; if (automationProbe.automationStatusSequence.length > 0) return Promise.resolve(automationProbe.automationStatusSequence.shift()); return Promise.resolve({ paused: automationProbe.paused }); }, folderMonitorTestScan() { automationProbe.readCalls.testScan++; return Promise.resolve(automationProbe.dryScan); }, folderMonitorReconcile() { automationProbe.readCalls.reconcile++; return Promise.resolve(automationProbe.dryScan); }, debugLog(value) { automationProbe.logs.push(String(value)); }, 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) { folderMonitorProbeCalls.push(['save', payload?.queueJobs?.length || 0]); automationProbe.mutationCalls.push(['save', payload?.queueJobs?.length || 0]); return Promise.resolve(true); }, addJobsToBatch(payload) { folderMonitorProbeCalls.push(['inject', payload?.jobs?.length || 0]); automationProbe.mutationCalls.push(['inject', payload?.jobs?.length || 0]); if (automationProbe.addError) return Promise.reject(new Error(automationProbe.addError)); if (automationProbe.addMode === 'partial-consistent' && payload?.jobs?.length >= 4) { return Promise.resolve({ added: payload.jobs.length - 2, alreadyInBatchJobIds: [payload.jobs[1].id], skippedJobs: [{ jobId: payload.jobs[2].id, reason: 'Kein gültiger Account' }] }); } return Promise.resolve(automationProbe.addResult || { added: payload?.jobs?.length || 0 }); }, startUpload(payload) { automationProbe.mutationCalls.push(['start', payload?.jobs?.length || 0]); if (automationProbe.startError) return Promise.reject(new Error(automationProbe.startError)); return Promise.resolve(automationProbe.startResult || { started: true }); }, getFolderMonitorProbeCalls() { return folderMonitorProbeCalls; } }); `, 'utf8'); const appDialogBehaviorScript = `(async () => { setupListeners(); const settle = promise => Promise.race([ promise, new Promise(resolve => setTimeout(() => resolve('pending'), 30)) ]); const safePromise = showAppConfirm({ title: 'Safe', message: 'Safe default', confirmText: 'Continue', danger: true }); const safeFocus = document.activeElement?.id; document.getElementById('appAlertCancelBtn').click(); const safeResult = await safePromise; const removalPromise = showAppConfirm({ title: 'Remove', message: 'Remove now', confirmText: 'Remove', danger: true, defaultAction: 'confirm' }); const removalFocus = document.activeElement?.id; document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })); const removalEnterResult = await settle(removalPromise); if (removalEnterResult === 'pending') document.getElementById('appAlertCancelBtn').click(); const cancelPromise = showAppConfirm({ title: 'Remove', message: 'Cancel intentionally', confirmText: 'Remove', danger: true, defaultAction: 'confirm' }); document.getElementById('appAlertCancelBtn').focus(); document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })); const cancelFocusedEnter = await settle(cancelPromise); document.getElementById('appAlertCancelBtn').click(); const cancelResult = await cancelPromise; const realShowAppConfirm = showAppConfirm; const removalCalls = []; showAppConfirm = options => { removalCalls.push({ title: options.title, defaultAction: options.defaultAction }); return Promise.resolve(false); }; selectedRecentIds.clear(); selectedRecentIds.add(1); sessionFilesData = [{ order: 1, link: 'https://example.invalid/a', filename: 'a.mp4', host: 'byse.sx' }]; await deleteSelectedRecentFiles(); await clearAllRecentFiles(); queueJobs = [{ id: 'dialog-remove', file: 'C:/dialog-remove.mp4', fileName: 'dialog-remove.mp4', hoster: 'byse.sx', status: 'queued' }]; selectedJobIds.clear(); selectedJobIds.add('dialog-remove'); rebuildJobIndex(); await handleContextAction('delete-selected'); await handleContextAction('delete-all'); showAppConfirm = realShowAppConfirm; queueJobs = []; selectedJobIds.clear(); selectedRecentIds.clear(); sessionFilesData = []; rebuildJobIndex(); return { safeFocus, safeResult, removalFocus, removalEnterResult, cancelFocusedEnter, cancelResult, removalCalls }; })()`; const settingsSearchBehaviorScript = `(async () => { setUiLanguage('de'); document.querySelector('[data-view="settings"]')?.click(); renderSettings(); await new Promise(resolve => setTimeout(resolve, 0)); const search = document.getElementById('settingsSearchInput'); search.value = 'erfolgreich löschen'; search.dispatchEvent(new Event('input', { bubbles: true })); const resultButtons = [...document.querySelectorAll('.settings-search-result')]; const targetResult = resultButtons.find(button => button.textContent.includes('Nach erfolgreichem Upload löschen')); const germanState = { navigationHidden: document.querySelector('.settings-navigation')?.hidden, resultsHidden: document.getElementById('settingsSearchResults')?.hidden, count: resultButtons.length, path: targetResult?.querySelector('.settings-search-result-path')?.textContent.trim(), marks: [...(targetResult?.querySelectorAll('mark') || [])].map(mark => mark.textContent.toLowerCase()), liveStatus: document.getElementById('settingsSearchStatus')?.textContent.trim() }; const inspectPaths = value => { search.value = value; search.dispatchEvent(new Event('input', { bubbles: true })); return [...document.querySelectorAll('.settings-search-result-path')].map(element => element.textContent.trim()); }; const updatePaths = inspectPaths('update'); const exceptionalPaths = [ 'schlüssel importieren', 'neuen schlüssel erzeugen', 'online-backups verwalten', 'datei exportieren', 'datei importieren' ].map(value => inspectPaths(value)); const stableSectionPath = inspectPaths('ordnerpfad')[0]; const umlautAliasPath = inspectPaths('loeschen').find(path => path.includes('Nach erfolgreichem Upload löschen')); search.value = 'online-backups verwalten'; search.dispatchEvent(new Event('input', { bubbles: true })); document.querySelector('.settings-search-result')?.click(); await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); const managedBackupNavigation = { page: document.querySelector('.settings-subpage.active')?.dataset.subpage, focus: document.activeElement?.id, highlighted: document.querySelector('.online-backup-managed')?.classList.contains('settings-search-target-highlight') }; search.value = 'erfolgreich löschen'; search.dispatchEvent(new Event('input', { bubbles: true })); document.querySelector('.settings-search-result')?.click(); await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); const navigationState = { page: document.querySelector('.settings-subpage.active')?.dataset.subpage, focus: document.activeElement?.id, highlighted: document.querySelector('.source-delete-option')?.classList.contains('settings-search-target-highlight'), searchValue: search.value, navigationHidden: document.querySelector('.settings-navigation')?.hidden, resultsHidden: document.getElementById('settingsSearchResults')?.hidden }; setUiLanguage('en'); search.value = 'delete source'; search.dispatchEvent(new Event('input', { bubbles: true })); const englishPath = [...document.querySelectorAll('.settings-search-result-path')] .map(element => element.textContent.trim()) .find(text => text.includes('Delete after successful upload')); search.value = ''; search.dispatchEvent(new Event('input', { bubbles: true })); const clearedState = { page: document.querySelector('.settings-subpage.active')?.dataset.subpage, navigationHidden: document.querySelector('.settings-navigation')?.hidden, resultsHidden: document.getElementById('settingsSearchResults')?.hidden, emptyHidden: document.getElementById('settingsSearchEmpty')?.hidden }; return { germanState, updatePaths, exceptionalPaths, stableSectionPath, umlautAliasPath, managedBackupNavigation, navigationState, englishPath, clearedState }; })()`; const folderMonitorBehaviorScript = `(async () => { setUiLanguage('de'); renderSettings(); await new Promise(resolve => setTimeout(resolve, 0)); const keys = ['enabled', 'recursive', 'existing', 'duplicates', 'auto-start', 'hosters']; const readTooltips = () => Object.fromEntries(keys.map(key => { const element = document.querySelector('[data-folder-monitor-help="' + key + '"]'); return [key, { tooltip: element?.dataset.tooltip, label: element?.getAttribute('aria-label') }]; })); const germanTooltips = readTooltips(); const gridScope = { ordinary: document.getElementById('alwaysOnTopInput').closest('.checkbox-row').classList.contains('folder-monitor-help-row'), folderMonitor: document.getElementById('fmEnabledInput').closest('.checkbox-row').classList.contains('folder-monitor-help-row') }; setUiLanguage('en'); await new Promise(resolve => setTimeout(resolve, 0)); const englishTooltips = readTooltips(); const actions = { disabledWhileIdle: resolveFolderMonitorQueueAction({ autoStart: false, uploading: false, healthCheckRunning: false }), disabledWhileUploading: resolveFolderMonitorQueueAction({ autoStart: false, uploading: true, healthCheckRunning: false }), enabledWhileIdle: resolveFolderMonitorQueueAction({ autoStart: true, uploading: false, healthCheckRunning: false }), enabledWhileUploading: resolveFolderMonitorQueueAction({ autoStart: true, uploading: true, healthCheckRunning: false }), enabledDuringHealthCheck: resolveFolderMonitorQueueAction({ autoStart: true, uploading: false, healthCheckRunning: true }) }; const resetQueue = autoStart => { config = { hosters: Object.fromEntries(HOSTERS.map(hoster => [hoster, []])), globalSettings: { folderMonitor: { hosters: ['doodstream.com'], autoStart } } }; hosterSettings = {}; selectedUploadHosters = []; selectedFiles = []; queueJobs = []; uploading = true; healthCheckRunning = false; rebuildJobIndex(); }; resetQueue(false); await handleFolderMonitorFiles(['C:\\\\folder-monitor-queue-only.mkv']); await new Promise(resolve => setTimeout(resolve, 0)); const queueOnly = { statuses: queueJobs.map(job => job.status), injectCalls: window.api.getFolderMonitorProbeCalls().filter(call => call[0] === 'inject').length }; resetQueue(true); await handleFolderMonitorFiles(['C:\\\\folder-monitor-inject.mkv']); await new Promise(resolve => setTimeout(resolve, 0)); const autoStart = { statuses: queueJobs.map(job => job.status), injectCalls: window.api.getFolderMonitorProbeCalls().filter(call => call[0] === 'inject').length }; const originalStartUpload = startUpload; let startCalls = 0; startUpload = () => { startCalls++; return Promise.resolve(); }; const prepareManualSelection = (filePath, autoStartValue, isUploading) => { const file = { path: filePath, name: filePath.split('\\\\').pop(), size: 1 }; resetQueue(autoStartValue); uploading = isUploading; _pendingFiles = [file]; _pendingImportInspection = { candidateCount: 1, duplicateCount: 0, unavailableCount: 0, accepted: [file] }; _pendingImportInspections = 0; const input = document.createElement('input'); input.type = 'checkbox'; input.dataset.hosterModal = 'doodstream.com'; input.checked = true; document.getElementById('hosterModalList').replaceChildren(input); markPendingFolderMonitorFiles([file], autoStartValue); return file; }; const injectBeforeManualQueue = window.api.getFolderMonitorProbeCalls().filter(call => call[0] === 'inject').length; prepareManualSelection('C:\\\\folder-monitor-manual-queue.mkv', false, true); applyHosterSelection(); await new Promise(resolve => setTimeout(resolve, 0)); const manualQueueOnly = { statuses: queueJobs.map(job => job.status), injectCalls: window.api.getFolderMonitorProbeCalls().filter(call => call[0] === 'inject').length - injectBeforeManualQueue }; const injectBeforeManualAuto = window.api.getFolderMonitorProbeCalls().filter(call => call[0] === 'inject').length; prepareManualSelection('C:\\\\folder-monitor-manual-inject.mkv', true, true); applyHosterSelection(); await new Promise(resolve => setTimeout(resolve, 0)); const manualAutoStartRunning = { statuses: queueJobs.map(job => job.status), injectCalls: window.api.getFolderMonitorProbeCalls().filter(call => call[0] === 'inject').length - injectBeforeManualAuto }; prepareManualSelection('C:\\\\folder-monitor-manual-start.mkv', true, false); applyHosterSelection(); await new Promise(resolve => setTimeout(resolve, 0)); const manualAutoStartIdle = { startCalls }; startUpload = originalStartUpload; queueJobs = [ { id: 'active-same-name', file: 'C:\\\\active\\\\same-name.mkv', fileName: 'same-name.mkv', hoster: 'doodstream.com', status: 'queued', bytesTotal: 1 }, { id: 'waiting-same-name', file: 'D:\\\\watched\\\\same-name.mkv', fileName: 'same-name.mkv', hoster: 'doodstream.com', status: 'preview', bytesTotal: 1 } ]; rebuildJobIndex(); applySummaryResults({ files: [{ name: 'same-name.mkv', size: 1, results: [{ hoster: 'doodstream.com', status: 'done', file_code: 'active-code' }] }] }); const sameBasenameResult = Object.fromEntries(queueJobs.map(job => [job.id, { status: job.status, code: job.result?.file_code || null }])); return { germanTooltips, englishTooltips, gridScope, actions, queueOnly, autoStart, manualQueueOnly, manualAutoStartRunning, manualAutoStartIdle, sameBasenameResult }; })()`; const automationPipelineScript = `(async () => { const clone = value => JSON.parse(JSON.stringify(value)); const captureMutationFingerprint = async () => { const api = await window.api.getAutomationProbeState(); return { queueJobs: clone(queueJobs), selectedFiles: clone(selectedFiles), counters: { sessionDone: _sessionDoneCount, sessionError: _sessionErrorCount }, pendingFiles: clone(_pendingFiles), pendingInspection: clone(_pendingImportInspection), pendingInspections: _pendingImportInspections, pendingAutoStart: clone([..._pendingFolderMonitorAutoStart]), config: clone(config), monitorSettings: clone(config.globalSettings?.folderMonitor || {}), api: { mutationCalls: api.mutationCalls, logs: api.logs, savedSettings: api.savedSettings } }; }; const hosters = ['doodstream.com', 'voe.sx', 'vidmoly.me', 'byse.sx']; const candidates = Array.from({ length: 500 }, (_, index) => ({ path: 'C:\\\\watch\\\\candidate-' + String(index).padStart(3, '0') + '.mkv', name: 'candidate-' + String(index).padStart(3, '0') + '.mkv', size: index >= 55 && index < 75 ? 2 * 1024 * 1024 : 512 * 1024, mtimeMs: index, filterMatched: index < 430, unavailable: index >= 50 && index < 55 })); const history = [{ id: 'history', files: candidates.slice(0, 25).map(file => ({ path: file.path, name: file.name, results: [{ hoster: hosters[0], status: 'done' }] })) }]; const uploadLog = candidates.slice(25, 50).map(file => ({ fileName: file.name, hoster: hosters[0] })); config = { hosters: Object.fromEntries(HOSTERS.map(hoster => [hoster, []])), hosterSettings: {}, globalSettings: { folderMonitor: { enabled: true, folderPath: 'C:\\\\watch', hosters, autoStart: false, queueLimitJobs: 1500, paused: false, telemetry: { dateKey: '2026-08-26', detected: 7, queued: 3, skipped: 2, deferred: 1 } } } }; hosterSettings = { 'doodstream.com': { maxSizeMb: 1 } }; selectedUploadHosters = []; selectedFiles = [{ path: 'C:\\\\manual\\\\selected.mkv', name: 'selected.mkv', size: 1 }]; queueJobs = Array.from({ length: 300 }, (_, index) => ({ id: 'existing-' + index, file: 'C:\\\\queue\\\\existing-' + index + '.mkv', fileName: 'existing-' + index + '.mkv', hoster: hosters[index % hosters.length], status: 'queued', bytesTotal: 1 })); _sessionDoneCount = 4; _sessionErrorCount = 5; _pendingFiles = [{ path: 'C:\\\\pending\\\\pending.mkv', name: 'pending.mkv', size: 1 }]; _pendingImportInspection = { candidateCount: 1, accepted: clone(_pendingFiles) }; _pendingImportInspections = 1; _pendingFolderMonitorAutoStart.clear(); _pendingFolderMonitorAutoStart.set('C:\\\\pending\\\\pending.mkv', true); rebuildJobIndex(); window.api.configureAutomationProbe({ history, uploadLog, paused: false }); const before = await captureMutationFingerprint(); const preview = await evaluateAutomationCandidates(candidates, { dryRun: true, trigger: 'test' }); const after = await captureMutationFingerprint(); const dryReads = (await window.api.getAutomationProbeState()).readCalls; const dry = { fingerprintEqual: JSON.stringify(after) === JSON.stringify(before), summary: preview.summary, frozen: Object.isFrozen(preview) && Object.isFrozen(preview.summary) && Object.isFrozen(preview.admittedFiles) && Object.isFrozen(preview.deferredFiles), reads: dryReads }; window.api.configureAutomationProbe({ dryScan: { files: [candidates[55]], reachable: true, trigger: 'test' }, paused: false }); const manualTestBefore = await captureMutationFingerprint(); const manualTestPreview = await runFolderMonitorTestScan(); const manualTestAfter = await captureMutationFingerprint(); const manualTestProbe = await window.api.getAutomationProbeState(); const manualTest = { fingerprintEqual: JSON.stringify(manualTestAfter) === JSON.stringify(manualTestBefore), 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 = { enabled: true, folderPath: 'C:\\\\watch', hosters, autoStart: false, queueLimitJobs: 15000, paused: false, telemetry: { dateKey: new Date().toLocaleDateString('en-CA'), detected: 0, queued: 0, skipped: 0, deferred: 0 } }; hosterSettings = { 'doodstream.com': { maxSizeMb: 2 }, 'voe.sx': { maxSizeMb: 2 } }; selectedUploadHosters = ['clouddrop.cc']; selectedFiles = []; queueJobs = Array.from({ length: currentCount }, (_, index) => ({ id: 'capacity-' + index, file: 'C:\\\\capacity\\\\existing-' + index + '.mkv', fileName: 'existing-' + index + '.mkv', hoster: hosters[index % hosters.length], status: 'queued', bytesTotal: 1 })); _pendingFiles = []; _pendingImportInspection = null; _pendingImportInspections = 0; _pendingFolderMonitorAutoStart.clear(); uploading = false; rebuildJobIndex(); window.api.configureAutomationProbe({ paused: false }); }; const atomicCandidates = [ { path: 'C:\\\\watch\\\\a.mkv', name: 'a.mkv', size: 1024 * 1024, mtimeMs: 1, filterMatched: true }, { path: 'C:\\\\watch\\\\b.mkv', name: 'b.mkv', size: 3 * 1024 * 1024, mtimeMs: 2, filterMatched: true } ]; configureAtomicState(14998); selectedFiles = [{ path: 'C:\\\\manual\\\\unplanned.mkv', name: 'unplanned.mkv', size: 1 }]; selectedUploadHosters = ['clouddrop.cc']; 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 = { state: statusSnapshot.state, currentJobCount: statusSnapshot.currentJobCount, availableSlots: statusSnapshot.availableSlots, queueLimited: statusSnapshot.queueLimited, 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) => ({ id: 'stale-' + index, file: 'C:\\\\capacity\\\\stale-' + index + '.mkv', fileName: 'stale-' + index + '.mkv', hoster: hosters[index], status: 'queued', bytesTotal: 1 }))); rebuildJobIndex(); const staleResult = await applyAutomationEvaluation(staleEvaluation); const stale = { plannedBeforeApply: staleEvaluation.admittedFiles.map(file => file.name), admittedAfterApply: staleResult.admittedFiles.map(file => file.name), 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 cleanupState = job => ({ token: job.sourceCleanupToken, required: clone(job.sourceCleanupRequiredHosters), completed: clone(job.sourceCleanupCompletedHosters), fingerprint: clone(job.sourceCleanupFingerprint) }); const runCleanupRollbackCase = async (name, probe) => { configureAtomicState(0); config.globalSettings.deleteSourceAfterSuccessfulUpload = true; const filePath = 'C:\\\\cleanup\\\\' + name + '.mkv'; const target = { ...makePauseRaceJob(name + '.mkv'), id: 'cleanup-target-' + name, file: filePath, sourceCleanupToken: 'preview-token-' + name, sourceCleanupRequiredHosters: ['doodstream.com'], sourceCleanupCompletedHosters: [], sourceCleanupFingerprint: { size: 11, mtimeMs: 22 } }; const sibling = { ...makePauseRaceJob(name + '-done.mkv'), id: 'cleanup-sibling-' + name, file: filePath, fileName: name + '.mkv', hoster: 'voe.sx', status: 'done', sourceCleanupToken: 'done-token-' + name, sourceCleanupRequiredHosters: ['voe.sx'], sourceCleanupCompletedHosters: ['voe.sx'], sourceCleanupFingerprint: { size: 33, mtimeMs: 44 } }; queueJobs = [target, sibling]; uploading = true; rebuildJobIndex(); window.api.configureAutomationProbe({ paused: false, ...probe }); const before = JSON.stringify([cleanupState(target), cleanupState(sibling)]); const result = await startSelectedUpload([target]); const after = JSON.stringify([cleanupState(target), cleanupState(sibling)]); uploading = false; return { cleanupByteIdentical: after === before, targetStatus: target.status, siblingStatus: sibling.status, result }; }; const cleanupRollback = { paused: await runCleanupRollbackCase('paused', { addResult: { error: 'Automatik ist pausiert', added: 0 } }), error: await runCleanupRollbackCase('error', { addResult: { error: 'token=secret-value', added: 0 } }), exception: await runCleanupRollbackCase('exception', { addError: 'token=secret-value' }), unconfirmed: await runCleanupRollbackCase('unconfirmed', { addResult: { added: 0 } }) }; const cleanupPresenceState = job => Object.fromEntries([ 'sourceCleanupToken', 'sourceCleanupRequiredHosters', 'sourceCleanupCompletedHosters', 'sourceCleanupFingerprint' ].map(field => [field, { present: Object.prototype.hasOwnProperty.call(job, field), value: Object.prototype.hasOwnProperty.call(job, field) ? clone(job[field]) : null }])); const setupCrossPathCleanup = name => { configureAtomicState(0); config.globalSettings.deleteSourceAfterSuccessfulUpload = true; selectedUploadHosters = ['doodstream.com']; const token = 'cross-token-' + name; const target = { ...makePauseRaceJob(name + '.mkv'), id: 'cross-target-' + name, file: 'C:\\\\cleanup-target\\\\' + name + '.mkv', sourceCleanupToken: token, sourceCleanupRequiredHosters: ['doodstream.com'] }; const sibling = { ...makePauseRaceJob(name + '-done.mkv'), id: 'cross-sibling-' + name, file: 'D:\\\\cleanup-sibling\\\\' + name + '.mkv', hoster: 'voe.sx', status: 'done', sourceCleanupToken: token }; const unrelated = { ...makePauseRaceJob(name + '-unrelated.mkv'), id: 'cross-unrelated-' + name, file: 'E:\\\\cleanup-unrelated\\\\' + name + '.mkv', hoster: 'vidmoly.me', status: 'done' }; queueJobs = [target, sibling, unrelated]; rebuildJobIndex(); return { target, sibling, unrelated, token }; }; const crossPathResult = (fixture, before, result) => ({ byteIdentical: JSON.stringify(fixture.map(cleanupPresenceState)) === before, statuses: fixture.map(job => job.status), result }); const originalAlertForCrossPath = showAppAlert; showAppAlert = async () => {}; let fixture = setupCrossPathCleanup('start-upload'); let fixtureJobs = [fixture.target, fixture.sibling, fixture.unrelated]; let fixtureBefore = JSON.stringify(fixtureJobs.map(cleanupPresenceState)); window.api.configureAutomationProbe({ paused: false, startResult: { error: 'Automatik ist pausiert' } }); const startUploadCleanup = crossPathResult(fixtureJobs, fixtureBefore, await startUpload()); fixture = setupCrossPathCleanup('inactive-selected'); fixtureJobs = [fixture.target, fixture.sibling, fixture.unrelated]; fixtureBefore = JSON.stringify(fixtureJobs.map(cleanupPresenceState)); uploading = false; window.api.configureAutomationProbe({ paused: false, startResult: { started: false } }); const inactiveSelectedCleanup = crossPathResult(fixtureJobs, fixtureBefore, await startSelectedUpload([fixture.target])); fixture = setupCrossPathCleanup('active-selected'); fixtureJobs = [fixture.target, fixture.sibling, fixture.unrelated]; fixtureBefore = JSON.stringify(fixtureJobs.map(cleanupPresenceState)); uploading = true; window.api.configureAutomationProbe({ paused: false, addResult: { error: 'Automatik ist pausiert', added: 0 } }); const activeSelectedCleanup = crossPathResult(fixtureJobs, fixtureBefore, await startSelectedUpload([fixture.target])); fixture = setupCrossPathCleanup('manual-modal'); fixture.target.automationAdmission = true; fixtureJobs = [fixture.target, fixture.sibling, fixture.unrelated]; fixtureBefore = JSON.stringify(fixtureJobs.map(cleanupPresenceState)); uploading = true; _pendingFiles = [{ path: fixture.target.file, name: fixture.target.fileName, size: 1 }]; _pendingImportInspection = { candidateCount: 1, duplicateCount: 0, unavailableCount: 0, accepted: _pendingFiles.slice() }; _pendingImportInspections = 0; _pendingFolderMonitorAutoStart.clear(); const crossManualInput = document.createElement('input'); crossManualInput.type = 'checkbox'; crossManualInput.dataset.hosterModal = 'doodstream.com'; crossManualInput.checked = true; document.getElementById('hosterModalList').replaceChildren(crossManualInput); window.api.configureAutomationProbe({ paused: false, addResult: { error: 'Automatik ist pausiert', added: 0 } }); const manualModalCleanup = crossPathResult(fixtureJobs, fixtureBefore, await applyHosterSelection()); fixture = setupCrossPathCleanup('automation'); queueJobs = [fixture.sibling, fixture.unrelated]; rebuildJobIndex(); config.globalSettings.folderMonitor.hosters = ['doodstream.com']; config.globalSettings.folderMonitor.autoStart = true; uploading = true; const originalCreateAutomationPreviewJob = createAutomationPreviewJob; createAutomationPreviewJob = (file, hoster) => ({ ...originalCreateAutomationPreviewJob(file, hoster), sourceCleanupToken: fixture.token }); const automationFile = { path: fixture.target.file, name: fixture.target.fileName, size: 1, mtimeMs: 1 }; window.api.configureAutomationProbe({ paused: false, addResult: { error: 'Automatik ist pausiert', added: 0 } }); const automationEvaluation = await evaluateAutomationCandidates([automationFile], { dryRun: false, trigger: 'watcher' }); const automationBeforeJobs = [fixture.sibling, fixture.unrelated]; const automationBefore = JSON.stringify(automationBeforeJobs.map(cleanupPresenceState)); const automationResult = await applyAutomationEvaluation(automationEvaluation); const automationTarget = queueJobs.find(job => job.file === automationFile.path); createAutomationPreviewJob = originalCreateAutomationPreviewJob; showAppAlert = originalAlertForCrossPath; uploading = false; const crossPathCleanupRollback = { startUpload: startUploadCleanup, inactiveSelected: inactiveSelectedCleanup, activeSelected: activeSelectedCleanup, manualModal: manualModalCleanup, automation: { byteIdentical: JSON.stringify(automationBeforeJobs.map(cleanupPresenceState)) === automationBefore, targetCleanup: cleanupPresenceState(automationTarget), statuses: [automationTarget.status, fixture.sibling.status, fixture.unrelated.status], result: { ok: automationResult.ok, error: automationResult.error } } }; const createPartialJobs = prefix => hosters.map((hoster, index) => ({ ...makePauseRaceJob(prefix + '-' + index + '.mkv'), id: prefix + '-' + index, file: 'C:\\\\partial\\\\' + prefix + '-' + index + '.mkv', hoster })); const runPartialSelected = async consistent => { configureAtomicState(0); config.globalSettings.deleteSourceAfterSuccessfulUpload = true; const jobs = createPartialJobs(consistent ? 'consistent' : 'inconsistent'); queueJobs = jobs; uploading = true; rebuildJobIndex(); const before = jobs.map(job => JSON.stringify(cleanupPresenceState(job))); window.api.configureAutomationProbe({ paused: false, addResult: { added: consistent ? 2 : 1, alreadyInBatchJobIds: [jobs[1].id], skippedJobs: [{ jobId: jobs[2].id, reason: 'Kein gültiger Account' }] } }); const result = await startSelectedUpload(jobs); const after = jobs.map(job => JSON.stringify(cleanupPresenceState(job))); uploading = false; return { result, statuses: jobs.map(job => job.status), unconfirmedRestored: [0, 3].map(index => after[index] === before[index]), confirmedPrepared: [1, 2].map(index => after[index] !== before[index]) }; }; const partialSelectedConsistent = await runPartialSelected(true); const partialSelectedInconsistent = await runPartialSelected(false); configureAtomicState(0); config.globalSettings.deleteSourceAfterSuccessfulUpload = true; config.globalSettings.folderMonitor.hosters = hosters.slice(); config.globalSettings.folderMonitor.autoStart = true; selectedFiles = []; uploading = true; window.api.configureAutomationProbe({ paused: false, addMode: 'partial-consistent' }); const partialAutomationFile = { path: 'C:\\\\partial\\\\automation.mkv', name: 'automation.mkv', size: 1, mtimeMs: 1 }; const partialAutomationEvaluation = await evaluateAutomationCandidates([partialAutomationFile], { dryRun: false, trigger: 'watcher' }); const partialAutomationResult = await applyAutomationEvaluation(partialAutomationEvaluation); const partialAutomationStatuses = Object.fromEntries(queueJobs .filter(job => job.file === partialAutomationFile.path) .map(job => [job.hoster, job.status])); uploading = false; configureAtomicState(0); config.globalSettings.deleteSourceAfterSuccessfulUpload = true; selectedFiles = []; uploading = true; const partialManualFile = { path: 'C:\\\\partial\\\\manual.mkv', name: 'manual.mkv', size: 1 }; _pendingFiles = [partialManualFile]; _pendingImportInspection = { candidateCount: 1, duplicateCount: 0, unavailableCount: 0, accepted: [partialManualFile] }; _pendingImportInspections = 0; _pendingFolderMonitorAutoStart.clear(); const partialManualInputs = hosters.map(hoster => { const input = document.createElement('input'); input.type = 'checkbox'; input.dataset.hosterModal = hoster; input.checked = true; return input; }); document.getElementById('hosterModalList').replaceChildren(...partialManualInputs); window.api.configureAutomationProbe({ paused: false, addMode: 'partial-consistent' }); const partialManualResult = await applyHosterSelection(); const partialManualStatuses = Object.fromEntries(queueJobs .filter(job => job.file === partialManualFile.path) .map(job => [job.hoster, job.status])); uploading = false; const partialAddOutcomes = { selectedConsistent: partialSelectedConsistent, selectedInconsistent: partialSelectedInconsistent, automation: { result: { ok: partialAutomationResult.ok, error: partialAutomationResult.error || null, admitted: partialAutomationResult.admittedFiles.map(file => file.name) }, statuses: partialAutomationStatuses }, manual: { result: partialManualResult, statuses: partialManualStatuses } }; 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 = false; window.api.configureAutomationProbe({ paused: false, automationStatusSequence: [{ paused: false }, { paused: true }] }); const betweenFile = { path: 'C:\\\\between\\\\paused.mkv', name: 'paused.mkv', size: 1, mtimeMs: 1 }; const betweenEvaluation = await evaluateAutomationCandidates([betweenFile], { dryRun: false, trigger: 'watcher' }); const betweenResult = await applyAutomationEvaluation(betweenEvaluation); const betweenProbe = await window.api.getAutomationProbeState(); const pauseBetweenApplyAndStart = { result: { ok: betweenResult.ok, error: betweenResult.error || null, warning: betweenResult.warning || null, admitted: betweenResult.admittedFiles.map(file => file.name) }, status: queueJobs.find(job => job.file === betweenFile.path)?.status || null, queuedTelemetry: config.globalSettings.folderMonitor.telemetry.queued, startCalls: betweenProbe.mutationCalls.filter(call => call[0] === 'start').length }; const runStartAcceptanceCase = async (name, probe) => { configureAtomicState(0); selectedFiles = []; selectedUploadHosters = ['doodstream.com']; const job = makePauseRaceJob(name + '.mkv'); queueJobs = [job]; uploading = false; rebuildJobIndex(); window.api.configureAutomationProbe({ paused: false, ...probe }); const originalAlert = showAppAlert; showAppAlert = async () => {}; const result = await startUpload(); showAppAlert = originalAlert; return { result, status: job.status, uploading }; }; const startAcceptance = { accepted: await runStartAcceptanceCase('accepted', { startResult: { started: true } }), unconfirmed: await runStartAcceptanceCase('unconfirmed', { startResult: { started: false } }), exception: await runStartAcceptanceCase('exception', { startError: 'token=secret-value' }) }; const originalShowCopyToastForAutomation = showCopyToast; const feedbackMessages = []; showCopyToast = message => { feedbackMessages.push(String(message)); }; configureAtomicState(0); selectedFiles = []; config.globalSettings.folderMonitor.hosters = ['doodstream.com']; config.globalSettings.folderMonitor.autoStart = false; hosterSettings = {}; window.api.configureAutomationProbe({ paused: false, saveSettingsError: 'token=telemetry-secret' }); const watcherWarningResult = await handleFolderMonitorFiles([ { path: 'C:\\\\feedback\\\\watcher-warning.mkv', name: 'watcher-warning.mkv', size: 1, mtimeMs: 1 } ]); const watcherWarningFeedback = feedbackMessages.splice(0); failedConfigWriteOperations.length = 0; configureAtomicState(0); selectedFiles = []; config.globalSettings.folderMonitor.hosters = ['doodstream.com']; config.globalSettings.folderMonitor.autoStart = true; hosterSettings = {}; uploading = true; window.api.configureAutomationProbe({ paused: false, addResult: { error: 'token=secret-value', added: 0 } }); const watcherErrorResult = await handleFolderMonitorFiles([ { path: 'C:\\\\feedback\\\\watcher-error.mkv', name: 'watcher-error.mkv', size: 1, mtimeMs: 1 } ]); const watcherErrorFeedback = feedbackMessages.splice(0); uploading = false; configureAtomicState(0); selectedFiles = []; config.globalSettings.folderMonitor.hosters = []; config.globalSettings.folderMonitor.autoStart = false; hosterSettings = {}; const modalWarningFile = { path: 'C:\\\\feedback\\\\modal-warning.mkv', name: 'modal-warning.mkv', size: 1, mtimeMs: 1 }; _pendingFiles = [modalWarningFile]; _pendingImportInspection = { candidateCount: 1, duplicateCount: 0, unavailableCount: 0, accepted: [modalWarningFile] }; _pendingImportInspections = 0; _pendingFolderMonitorAutoStart.clear(); _pendingFolderMonitorAutoStart.set(modalWarningFile.path, false); const modalWarningInput = document.createElement('input'); modalWarningInput.type = 'checkbox'; modalWarningInput.dataset.hosterModal = 'doodstream.com'; modalWarningInput.checked = true; document.getElementById('hosterModalList').replaceChildren(modalWarningInput); document.getElementById('hosterModal').style.display = 'flex'; window.api.configureAutomationProbe({ paused: false, saveSettingsError: 'token=telemetry-secret' }); const modalWarningResult = await applyHosterSelection(); const modalWarningFeedback = feedbackMessages.splice(0); failedConfigWriteOperations.length = 0; showCopyToast = originalShowCopyToastForAutomation; const fulfilledFeedback = { watcherWarning: { result: { ok: watcherWarningResult.ok, warning: watcherWarningResult.warning || null, error: watcherWarningResult.error || null }, feedback: watcherWarningFeedback }, watcherError: { result: { ok: watcherErrorResult.ok, warning: watcherErrorResult.warning || null, error: watcherErrorResult.error || null }, feedback: watcherErrorFeedback }, modalWarning: { result: modalWarningResult, feedback: modalWarningFeedback, pending: _pendingFiles.length, markers: _pendingFolderMonitorAutoStart.size, modalOpen: document.getElementById('hosterModal').style.display === 'flex', queueStatus: queueJobs.find(job => job.file === modalWarningFile.path)?.status || null }, secretExposed: JSON.stringify({ watcherWarningFeedback, watcherErrorFeedback, modalWarningFeedback, modalWarningResult }).includes('secret') }; 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 }); const pausedJob = { id: 'paused-preview', file: 'C:\\\\manual\\\\paused-preview.mkv', fileName: 'paused-preview.mkv', hoster: hosters[0], status: 'preview', bytesTotal: 1 }; queueJobs = [pausedJob]; selectedFiles = [{ path: pausedJob.file, name: pausedJob.fileName, size: 1 }]; selectedUploadHosters = [hosters[0]]; rebuildJobIndex(); await startUpload(); uploading = true; await startSelectedUpload([pausedJob]); uploading = false; const pausedAutomaticEvaluation = await evaluateAutomationCandidates([ { path: 'C:\\\\watch\\\\paused-auto.mkv', name: 'paused-auto.mkv', size: 1, mtimeMs: 1, filterMatched: true } ], { dryRun: false, trigger: 'watcher' }); const pausedAutomaticResult = await applyAutomationEvaluation(pausedAutomaticEvaluation); await coordinateImportEntries([ { path: 'C:\\\\manual\\\\allowed-preview.mkv', name: 'allowed-preview.mkv', size: 1 } ]); const input = document.createElement('input'); input.type = 'checkbox'; input.dataset.hosterModal = hosters[0]; input.checked = true; document.getElementById('hosterModalList').replaceChildren(input); await applyHosterSelection(); const pausedProbe = await window.api.getAutomationProbeState(); const paused = { uploading, statuses: Object.fromEntries(queueJobs.map(job => [job.fileName, job.status])), automaticApplied: pausedAutomaticResult.admittedFiles.length, startCalls: pausedProbe.mutationCalls.filter(call => call[0] === 'start').length, injectCalls: pausedProbe.mutationCalls.filter(call => call[0] === 'inject').length }; return { dry, manualTest, historyEvidence, pendingDedup, manualHostTransactional, atomic, status, persistedQueueExactness, stale, replannedEligibility, mainPauseResponses, cleanupRollback, crossPathCleanupRollback, partialAddOutcomes, pauseBetweenApplyAndStart, startAcceptance, fulfilledFeedback, injectionOutcomes, paused }; })()`; const onlineBackupBehaviorScript = `(async () => { const ids = { a: 'AAAAAAAAAAAAAAAAAAAAAA', b: 'AQEBAQEBAQEBAQEBAQEBAQ', c: 'AgICAgICAgICAgICAgICAg', d: 'AwMDAwMDAwMDAwMDAwMDAw', e: 'BAQEBAQEBAQEBAQEBAQEBA' }; const fixture = document.createElement('section'); fixture.innerHTML = '