fix: harden import preflight admission

Keep configured size-limit counts identical to real queue admission, invalidate cancelled import generations, preserve explicit empty destination choices, normalize Windows namespace aliases, inspect files through one read handle, and retain exact rejection balances.
This commit is contained in:
Sucukdeluxe
2026-08-16 22:01:16 +02:00
parent 88a3ee0937
commit 3da1b08514
5 changed files with 190 additions and 49 deletions
+9 -3
View File
@@ -478,7 +478,7 @@ setTimeout(async () => {
if (!duplicateDropDebuggerWasAttached && wc.debugger.isAttached()) wc.debugger.detach();
try { fs.unlinkSync(duplicateDropFixture); } catch {}
}
check('Dropping a file already in the upload jobs explains the duplicate instead of doing nothing', duplicateDropState?.modal === 'none' && duplicateDropState.pending === 0 && duplicateDropState.shown === true && duplicateDropState.toast === 'Auswahl ist bereits in den Upload-Aufträgen.');
check('Dropping a file already in the upload jobs explains the exact duplicate balance instead of doing nothing', duplicateDropState?.modal === 'none' && duplicateDropState.pending === 0 && duplicateDropState.shown === true && duplicateDropState.toast === 'Kandidaten: 1 · Bereits vorhanden / dupliziert: 1 · Durch Dateinamenfilter ausgeschlossen: 0 · Fehlend / unlesbar / leer: 0 · Akzeptierte Dateien: 0');
const startDisabled = await wc.executeJavaScript('document.getElementById("startUploadBtn")?.disabled');
check('Start button disabled initially', startDisabled === true);
@@ -1577,6 +1577,12 @@ setTimeout(async () => {
const importPreflightMinimumFit = await wc.executeJavaScript('(() => { const card = document.querySelector("#hosterModal .modal-card"); const summary = document.getElementById("importPlanSummary"); const cardRect = card?.getBoundingClientRect(); return Boolean(cardRect && summary && cardRect.left >= 0 && cardRect.right <= innerWidth && cardRect.top >= 0 && cardRect.bottom <= innerHeight && summary.scrollWidth <= summary.clientWidth + 1 && [...summary.querySelectorAll("dd")].every(value => value.getBoundingClientRect().width > 0)); })()');
await setWindowBounds(importPreflightBounds);
check('Import preflight remains contained and readable at the minimum window size', importPreflightMinimumFit === true);
const importPreflightAdmission = await wc.executeJavaScript('(async () => { await applyHosterSelection(); return { selected: selectedFiles.map(file => file.name), jobs: queueJobs.map(job => ({ file: job.fileName, hoster: job.hoster, status: job.status })) }; })()');
check('Configured size-limit pairs never create preview jobs and fully ineligible files leave no selected-file residue', importPreflightAdmission.selected.length === 2 && importPreflightAdmission.selected.includes('Existing.720p.bin') && importPreflightAdmission.selected.includes('Episode.720p.mkv') && !importPreflightAdmission.selected.includes('Large.720p.custom') && importPreflightAdmission.jobs.length === 2 && importPreflightAdmission.jobs.every(job => job.file !== 'Large.720p.custom' && job.hoster === filenameFilterImport.available[0] && job.status === 'preview'));
const explicitEmptyHosterSelection = await wc.executeJavaScript('(async () => { const fixtures = ' + JSON.stringify(importPreflightFixtures) + '; cancelHosterModal(); selectedFiles = []; _pendingFiles = []; _pendingImportInspection = null; queueJobs = []; rebuildJobIndex(); selectedUploadHosters = getAvailableHosters().slice(0, 1).map(item => item.name); const inspect = entries => ({ candidateCount: entries.length, duplicateCount: 0, filteredCount: 0, unavailableCount: 0, acceptedCount: entries.length, accepted: entries.map(entry => ({ path: entry.path, name: entry.name, size: entry.size })), duplicates: [], filtered: [], unavailable: [] }); await coordinateImportEntries([{ path: fixtures.floatingAccepted, name: "Floating.720p.mkv", size: 7 }], async entries => inspect(entries)); const selected = document.querySelector("input[data-hoster-modal]:checked"); selected?.click(); let releaseSecond; const second = coordinateImportEntries([{ path: fixtures.desktopAccepted, name: "Desktop.720p.mkv", size: 7 }], async entries => new Promise(resolve => { releaseSecond = () => resolve(inspect(entries)); })); const disabledWhilePending = document.getElementById("confirmHosterModalBtn").disabled; for (let index = 0; index < 20 && !releaseSecond; index++) await Promise.resolve(); releaseSecond?.(); await second; return { disabledWhilePending, checked: [...document.querySelectorAll("input[data-hoster-modal]:checked")].map(input => input.dataset.hosterModal), targets: Number(document.getElementById("importPlanTargets").textContent) }; })()');
check('A later inspection preserves an explicitly empty hoster selection and confirmation stays disabled while it is pending', explicitEmptyHosterSelection.disabledWhilePending === true && explicitEmptyHosterSelection.checked.length === 0 && explicitEmptyHosterSelection.targets === 0);
const cancelledImportGeneration = await wc.executeJavaScript('(async () => { const fixtures = ' + JSON.stringify(importPreflightFixtures) + '; cancelHosterModal(); selectedFiles = []; _pendingFiles = []; _pendingImportInspection = null; queueJobs = []; rebuildJobIndex(); selectedUploadHosters = getAvailableHosters().slice(0, 1).map(item => item.name); const result = entry => ({ candidateCount: 1, duplicateCount: 0, filteredCount: 0, unavailableCount: 0, acceptedCount: 1, accepted: [{ path: entry.path, name: entry.name, size: entry.size }], duplicates: [], filtered: [], unavailable: [] }); let runningCalls = 0; let queuedCalls = 0; let releaseRunning; const runningEntry = { path: fixtures.floatingAccepted, name: "Floating.720p.mkv", size: 7 }; const queuedEntry = { path: fixtures.desktopAccepted, name: "Desktop.720p.mkv", size: 7 }; const running = coordinateImportEntries([runningEntry], async () => { runningCalls++; return new Promise(resolve => { releaseRunning = () => resolve(result(runningEntry)); }); }); for (let index = 0; index < 20 && !releaseRunning; index++) await Promise.resolve(); const queued = coordinateImportEntries([queuedEntry], async () => { queuedCalls++; return result(queuedEntry); }); const disabledWhilePending = document.getElementById("confirmHosterModalBtn").disabled; cancelHosterModal(); releaseRunning?.(); await Promise.all([running, queued]); await new Promise(resolve => setTimeout(resolve, 25)); return { runningCalls, queuedCalls, disabledWhilePending, modal: document.getElementById("hosterModal").style.display, pending: _pendingFiles.length, inspection: _pendingImportInspection }; })()');
check('Cancelling invalidates running and queued results from the old import generation', cancelledImportGeneration.runningCalls === 1 && cancelledImportGeneration.queuedCalls === 0 && cancelledImportGeneration.disabledWhilePending === true && cancelledImportGeneration.modal !== 'flex' && cancelledImportGeneration.pending === 0 && cancelledImportGeneration.inspection === null);
const filenameFilterDropPaths = await wc.executeJavaScript('(async () => { const fixtures = ' + JSON.stringify(importPreflightFixtures) + '; cancelHosterModal(); await addDropTargetEntries([{ path: fixtures.floatingAccepted }, { path: fixtures.floatingFiltered }]); const floating = _pendingFiles.map(file => file.name); cancelHosterModal(); await addDroppedFiles([{ path: fixtures.desktopAccepted, name: "Desktop.720p.mkv", size: 7, type: "video/x-matroska" }, { path: fixtures.desktopFiltered, name: "Desktop.1080p.mkv", size: 7, type: "video/x-matroska" }]); const desktop = _pendingFiles.map(file => file.name); cancelHosterModal(); return { floating, desktop }; })()');
check('Filename filter applies identically to floating and native desktop drops', JSON.stringify(filenameFilterDropPaths.floating) === JSON.stringify(['Floating.720p.mkv']) && JSON.stringify(filenameFilterDropPaths.desktop) === JSON.stringify(['Desktop.720p.mkv']));
await wc.executeJavaScript('(() => { selectedFiles = []; _pendingFiles = []; queueJobs = []; rebuildJobIndex(); const toast = document.getElementById("copyToast"); toast.textContent = ""; toast.classList.remove("show"); config.globalSettings.folderMonitor = { ...(config.globalSettings.folderMonitor || {}), hosters: ["voe.sx"], autoStart: false }; })()');
@@ -1585,8 +1591,8 @@ setTimeout(async () => {
const filenameFilterFolderMonitor = await wc.executeJavaScript('(() => ({ files: selectedFiles.map(file => file.name), jobs: queueJobs.map(job => job.fileName), modal: document.getElementById("hosterModal")?.style.display, toast: document.getElementById("copyToast")?.textContent }))()');
check('Filename filter also applies to monitored folders with preset destinations', JSON.stringify(filenameFilterFolderMonitor.files) === JSON.stringify(['Watched.720p.mkv']) && filenameFilterFolderMonitor.jobs.every(name => name === 'Watched.720p.mkv') && filenameFilterFolderMonitor.modal !== 'flex' && /1 von 2/.test(filenameFilterFolderMonitor.toast || ''));
await wc.executeJavaScript('selectedFiles = []; queueJobs = []; rebuildJobIndex(); updateUploadView()');
const filenameFilterRejectAll = await wc.executeJavaScript('(async () => { const rejected = ' + JSON.stringify(importPreflightFixtures.rejected) + '; cancelHosterModal(); const toast = document.getElementById("copyToast"); toast.textContent = ""; toast.classList.remove("show"); const action = document.getElementById("filenameFilterActionInput"); action.value = "exclude"; action.dispatchEvent(new Event("change", { bubbles: true })); const rows = [...document.querySelectorAll("[data-filename-filter-condition]")]; rows[0].querySelector("[data-filename-filter-value]").value = "1080p"; rows[0].querySelector("[data-filename-filter-value]").dispatchEvent(new Event("input", { bubbles: true })); rows.slice(1).forEach(row => row.querySelector("[data-filename-filter-remove]")?.click()); config.globalSettings.filenameFilter = readFilenameFilterSettings(); await addPathsToQueue([{ path: rejected, name: "Only.1080p.mkv", size: 7 }]); return { modal: document.getElementById("hosterModal")?.style.display, pending: _pendingFiles.length, toast: toast.textContent, shown: toast.classList.contains("show") }; })()');
check('A fully excluded import stays out of the queue and explains the result', filenameFilterRejectAll.modal !== 'flex' && filenameFilterRejectAll.pending === 0 && filenameFilterRejectAll.shown === true && filenameFilterRejectAll.toast === 'Keine Dateien wurden akzeptiert.');
const filenameFilterRejectAll = await wc.executeJavaScript('(async () => { const rejected = ' + JSON.stringify(importPreflightFixtures.rejected) + '; cancelHosterModal(); const toast = document.getElementById("copyToast"); toast.textContent = ""; toast.classList.remove("show"); const action = document.getElementById("filenameFilterActionInput"); action.value = "exclude"; action.dispatchEvent(new Event("change", { bubbles: true })); const rows = [...document.querySelectorAll("[data-filename-filter-condition]")]; rows[0].querySelector("[data-filename-filter-value]").value = "1080p"; rows[0].querySelector("[data-filename-filter-value]").dispatchEvent(new Event("input", { bubbles: true })); rows.slice(1).forEach(row => row.querySelector("[data-filename-filter-remove]")?.click()); config.globalSettings.filenameFilter = readFilenameFilterSettings(); await addPathsToQueue([{ path: rejected, name: "Only.1080p.mkv", size: 7 }]); const german = toast.textContent; setUiLanguage("en"); toast.textContent = ""; toast.classList.remove("show"); await addPathsToQueue([{ path: rejected, name: "Only.1080p.mkv", size: 7 }]); const english = toast.textContent; setUiLanguage("de"); return { modal: document.getElementById("hosterModal")?.style.display, pending: _pendingFiles.length, german, english, shown: toast.classList.contains("show") }; })()');
check('A fully excluded import stays out of the queue and keeps an exact bilingual balance visible', filenameFilterRejectAll.modal !== 'flex' && filenameFilterRejectAll.pending === 0 && filenameFilterRejectAll.shown === true && filenameFilterRejectAll.german === 'Kandidaten: 1 · Bereits vorhanden / dupliziert: 0 · Durch Dateinamenfilter ausgeschlossen: 1 · Fehlend / unlesbar / leer: 0 · Akzeptierte Dateien: 0' && filenameFilterRejectAll.english === 'Candidates: 1 · Already present / duplicated: 0 · Excluded by filename filter: 1 · Missing / unreadable / empty: 0 · Accepted files: 0');
await wc.executeJavaScript('hosterSettings = window.__importPreflightHosterSettings; delete window.__importPreflightHosterSettings; true');
fs.rmSync(importPreflightFolder, { recursive: true, force: true });
await wc.executeJavaScript('(() => { const enabled = document.getElementById("filenameFilterEnabledInput"); enabled.checked = false; enabled.dispatchEvent(new Event("change", { bubbles: true })); return saveSettings({ feedbackText: "Gespeichert" }); })()');