diff --git a/PROJECT_MEMORY.md b/PROJECT_MEMORY.md index bac5c21..f6c7514 100644 --- a/PROJECT_MEMORY.md +++ b/PROJECT_MEMORY.md @@ -6,6 +6,7 @@ Multi-Hoster-Upload ist eine Electron-Desktopanwendung für Windows, die große ## Aktueller Zustand +- Ordnerüberwachungstest nach `2.1.49`: IPC verwendet gespeicherte Ordnerregeln in einer isolierten FolderMonitor-Instanz statt der möglicherweise unkonfigurierten laufenden Überwachung. Funktioniert bei deaktivierter, aktiver und pausierter Überwachung; kein Watcher-/Timerstart, keine neuen Dateien/Statusereignisse am produktiven Monitor, keine Änderung seiner Generation oder Duplikatreservierungen. Fehlender Pfad, unerreichbarer Ordner und Scanfehler werden über feste, übersetzte Fehlermeldungen angezeigt; unbekannte Fehler bleiben bereinigt. Regressionen für die drei Betriebszustände, parallele Tests, Fehlerfälle und IPC-Verdrahtung ergänzt; Entwicklung automatisch neu gestartet. - Automatik-Einstellungen nach `2.1.49`: gemeinsames 210-px-Beschriftungsraster für Wiederholungen, Ordnerpfad, Queue-Limit, Abgleichintervall, Dateifilter und Verzögerung. Felder 40 px hoch, Zahlenfelder 120 px breit, Dropdowns auf passende Breiten begrenzt und Pfeile 14 px nach innen versetzt. Hinweise stehen unter ihrem jeweiligen Feld; Container-Abfragen schalten bei schmalem Inhalt auf einspaltige Darstellung. Ordnerüberwachungstest steht nach Verhalten/Hoster-Vorauswahl als Abschlussaktion. IDs und Speicherlogik unverändert. Electron-Regression mit echtem Template bei 1000/760/360 px, Screenshot visuell geprüft; noch kein Release. - Online-Backup-Seite nach `2.1.49`: drei klar abgegrenzte Bereiche in Reihenfolge „Schlüssel erstellen“, „Schlüssel importieren“, „Auf diesem Gerät erstellt“. Vorhandene IDs, Import-/Exportlogik, Statusmeldungen und Suchziele bleiben erhalten. Neue Überschriften auf Deutsch/Englisch; konsistente Karten ohne Akzentstreifen. Electron-Test nutzt jetzt das echte Seitenmarkup und prüft Modulzuordnung, Ausrichtung sowie Überlauf bei 1000/600/360 px. Lokale Entwicklerversion automatisch neu gestartet; breiter Screenshot visuell geprüft. Noch nicht veröffentlicht. - Lokale Entwicklung auf Nutzerwunsch dauerhaft mit automatischem Neustart: `.artifacts/dev-watch.cjs` überwacht Main-/Preload-Dateien, `lib`, `renderer` und den lokalen Bootstrap `.artifacts/menu-test.cjs`. Start über `node .artifacts/dev-watch.cjs`; separater Fenstertitel „Multi-Hoster – Entwicklung (automatischer Neustart)“. Persistentes Testprofil unter `%LOCALAPPDATA%/Multi-Hoster-Upload-Development`, ohne automatische Accountchecks/Ordnerüberwachung/Remote-Steuerung initialisiert. Installierte Anwendung bleibt getrennt. Neustart durch CSS-Dateiänderung anhand gewechselter Electron-PID und erneut sichtbarem Fenster verifiziert. Bei weiteren Code-/UI-Änderungen diesen Watcher verwenden und den Neustart prüfen. @@ -93,6 +94,7 @@ npm audit --omit=dev ## Zuletzt verifiziert +- Test bei deaktivierter Ordnerüberwachung: 844 Haupttests, 18 Servertests und Lint erfolgreich. Watcher-Neustart bestätigt. Read-only-Test bleibt unabhängig vom Produktivzustand, bekannte Fehler werden konkret angezeigt. Noch nicht veröffentlicht. - Automatik-Layout: 840 Haupttests, 18 Servertests und Lint erfolgreich. Feldhöhen, gemeinsame linke Kante, Label-/Hinweispositionen, Dropdownbreiten/Pfeilabstand und Abschlussposition der Testaktion über echte Electron-DOM-Messungen geprüft. Watcher-Neustart und sichtbare Entwicklungsinstanz bestätigt. Noch nicht veröffentlicht. - Getrennte Online-Backup-Module: 839 Haupttests, 18 Servertests und Lint erfolgreich. Echtes Modulmarkup in Electron bei drei Fensterbreiten geprüft; Entwicklerversion nach Renderer-Änderung mit neuem Prozess sichtbar bestätigt. Noch kein Release. - Gültigkeitsdauer-Layout: neue Electron-Regression erfolgreich, Screenshot visuell geprüft, Lint und 18 Servertests erfolgreich. Hauptlauf 838/839 erfolgreich: Menü-Fokustest reagierte einmal zu früh auf asynchronen Fokus. Test wartet jetzt begrenzt auf den tatsächlichen Fokus statt pauschal 240 ms; kompletter Wiederholungslauf der betroffenen Datei 25/25 erfolgreich. Kein Release erstellt. diff --git a/lib/folder-monitor.js b/lib/folder-monitor.js index 9b3a5a1..b7023e6 100644 --- a/lib/folder-monitor.js +++ b/lib/folder-monitor.js @@ -81,6 +81,22 @@ class FolderMonitor extends EventEmitter { this._deactivate({ clearSeen: true, paused: false }); } + async testScan(settings) { + if (!String(settings?.folderPath || '').trim()) { + return this._result([], false, false, 'test', { error: 'Kein Ordnerpfad angegeben' }); + } + const isolated = new FolderMonitor({ + access: this._access, + stat: this._stat, + walkFolder: this._walkFolder, + now: this._now + }); + isolated.configure({ ...settings }); + const result = await isolated.scan({ emitFiles: false, trigger: 'test' }); + if (!result.reachable) return { ...result, error: 'Ordner nicht erreichbar' }; + return result; + } + status() { return Object.freeze({ running: this.running, diff --git a/main.js b/main.js index 71e706c..bf92fe9 100644 --- a/main.js +++ b/main.js @@ -3712,7 +3712,7 @@ ipcMain.handle('automation:resume', () => enqueueAutomationLifecycle(async gener })); ipcMain.handle('folder-monitor:test-scan', () => { - return folderMonitor.scan({ emitFiles: false, trigger: 'test' }); + return folderMonitor.testScan(configStore.load().globalSettings?.folderMonitor || {}); }); ipcMain.handle('folder-monitor:reconcile', () => { diff --git a/renderer/app.js b/renderer/app.js index b55ad2a..cdf8afd 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -927,7 +927,10 @@ async function applyAutomationEvaluation(evaluation) { async function runFolderMonitorTestScan() { const result = await window.api.folderMonitorTestScan(); - if (result?.error) throw new Error('Ordnerüberwachung konnte nicht getestet werden.'); + if (result?.error) { + const knownErrors = ['Kein Ordnerpfad angegeben', 'Ordner nicht erreichbar', 'Ordnerscan fehlgeschlagen']; + throw new Error(knownErrors.includes(result.error) ? result.error : 'Ordnerüberwachung konnte nicht getestet werden.'); + } return evaluateAutomationCandidates(result?.files || [], { dryRun: true, trigger: result?.trigger || 'test' }); } @@ -1226,9 +1229,10 @@ async function runAutomationTestOverlay() { const evaluation = await runFolderMonitorTestScan(); if (generation !== automationTestGeneration) return; renderAutomationTestViewState({ loading: false, summary: evaluation.summary, error: '' }); - } catch { + } catch (error) { if (generation !== automationTestGeneration) return; - renderAutomationTestViewState({ loading: false, summary: null, error: 'Ordnerüberwachung konnte nicht getestet werden.' }); + const knownErrors = ['Kein Ordnerpfad angegeben', 'Ordner nicht erreichbar', 'Ordnerscan fehlgeschlagen']; + renderAutomationTestViewState({ loading: false, summary: null, error: knownErrors.includes(error?.message) ? error.message : 'Ordnerüberwachung konnte nicht getestet werden.' }); } } diff --git a/tests/folder-monitor.test.js b/tests/folder-monitor.test.js index d164e37..9f1a453 100644 --- a/tests/folder-monitor.test.js +++ b/tests/folder-monitor.test.js @@ -19,6 +19,75 @@ function createWatcherHarness() { return { calls, monitor: new FolderMonitor({ watch, ...timers }) }; } +test('test scan works while inactive, active or paused without changing monitor state', async () => { + const folderPath = path.join(os.tmpdir(), 'monitor-read-only-test'); + for (const state of ['inactive', 'active', 'paused']) { + const { monitor, events } = createScanHarness({ files: [ + { path: path.join(folderPath, 'video.mkv'), size: 12, mtimeMs: 1 }, + { path: path.join(folderPath, 'note.txt'), size: 2, mtimeMs: 2 } + ] }); + if (state === 'active') monitor.start({ folderPath: 'different-live-folder' }); + if (state === 'paused') monitor.configure({ folderPath: 'different-paused-folder', paused: true }); + const before = monitor.status(); + const generation = monitor._generation; + const settings = monitor._settings; + const statuses = events.statuses.length; + const seen = [...monitor._seenFiles]; + const result = await monitor.testScan({ enabled: false, folderPath, extensions: 'mkv', filterMode: 'include', recursive: true }); + assert.equal(result.reachable, true); + assert.deepEqual(result.files.map(file => file.filterMatched), [true, false]); + assert.deepEqual(monitor.status(), before); + assert.equal(monitor._generation, generation); + assert.equal(monitor._settings, settings); + assert.deepEqual([...monitor._seenFiles], seen); + assert.equal(events.statuses.length, statuses); + assert.equal(events.newFiles.length, 0); + monitor.stop(); + } +}); + +test('test scan uses independent settings for concurrent read-only requests', async () => { + const visited = []; + const monitor = new FolderMonitor({ + access: async () => {}, + walkFolder: async (folder, options) => { visited.push([folder, options.recursive]); return []; }, + watch: () => { throw new Error('No watcher may be started'); }, + setIntervalFn: () => { throw new Error('No timer may be started'); } + }); + await Promise.all([ + monitor.testScan({ folderPath: 'first', recursive: false }), + monitor.testScan({ folderPath: 'second', recursive: true }) + ]); + assert.deepEqual(visited, [['first', false], ['second', true]]); + assert.equal(monitor.running, false); + assert.equal(monitor.status().folderPath, ''); +}); + +test('test scan distinguishes missing, unavailable and unreadable folders safely', async () => { + const missing = await new FolderMonitor().testScan({}); + assert.equal(missing.error, 'Kein Ordnerpfad angegeben'); + const unavailable = await new FolderMonitor({ access: async () => { throw new Error('private path'); } }).testScan({ folderPath: 'test' }); + assert.equal(unavailable.error, 'Ordner nicht erreichbar'); + const failed = await new FolderMonitor({ access: async () => {}, walkFolder: async () => { throw new Error('private path'); } }).testScan({ folderPath: 'test' }); + assert.equal(failed.error, 'Ordnerscan fehlgeschlagen'); +}); + +test('test scan IPC reads persisted settings instead of depending on the active watcher', async () => { + const vm = require('node:vm'); + const source = fs.readFileSync(path.join(__dirname, '../main.js'), 'utf8'); + const start = source.indexOf("ipcMain.handle('folder-monitor:test-scan'"); + const end = source.indexOf('\n});', start) + 4; + const settings = { enabled: false, folderPath: 'test-folder' }; + let handler; + const expected = { files: [], reachable: true }; + vm.runInNewContext(source.slice(start, end), { + ipcMain: { handle: (_name, callback) => { handler = callback; } }, + configStore: { load: () => ({ globalSettings: { folderMonitor: settings } }) }, + folderMonitor: { testScan: snapshot => { assert.equal(snapshot, settings); return expected; } } + }); + assert.equal(await handler(), expected); +}); + function createManualTimers() { const intervals = new Set(); const timeouts = new Set();