fix: prevent folder-monitor reuploads after completion

Discover base, daily, session, and fallback upload logs across configured, Desktop, and application-data locations without blocking the Electron main process.

Treat in-session completion keys as authoritative automation evidence so removing finished rows cannot cause periodic reconciliation to enqueue and upload the same file again.

Add real handler and done-remove-reconcile regressions covering custom extensions, mixed-case log names, fallback paths, stale evidence, and asynchronous log reads.
This commit is contained in:
Sucukdeluxe
2026-08-27 13:22:35 +02:00
parent c0153acfb2
commit 58b92856a6
6 changed files with 182 additions and 27 deletions
+16 -1
View File
@@ -1,6 +1,6 @@
const { test } = require('node:test');
const assert = require('node:assert');
const { normalizeLogMode, resolveLogFileName, formatDateStamp, formatSessionStamp } = require('../lib/log-mode');
const { normalizeLogMode, resolveLogFileName, formatDateStamp, formatSessionStamp, isManagedUploadLogFileName } = require('../lib/log-mode');
// --- normalizeLogMode ---
@@ -88,6 +88,21 @@ test('resolveLogFileName: unknown mode is treated as single', () => {
);
});
test('managed upload-log discovery includes session logs and excludes unrelated logs', () => {
assert.equal(typeof isManagedUploadLogFileName, 'function');
const options = { baseName: 'fileuploader', ext: '.log' };
assert.equal(isManagedUploadLogFileName('fileuploader.log', options), true);
assert.equal(isManagedUploadLogFileName('fileuploader-2026-08-27.log', options), true);
assert.equal(isManagedUploadLogFileName('fileuploader-session-2026-08-27_05-40-59-1234.log', options), true);
assert.equal(isManagedUploadLogFileName('27-08-2026-mdu-session-05-40-599797.log', options), true);
assert.equal(isManagedUploadLogFileName('FILEUPLOADER-2026-08-27.LOG', options), true);
assert.equal(isManagedUploadLogFileName('27-08-2026-MDU-SESSION-05-40-599797.LOG', options), true);
assert.equal(isManagedUploadLogFileName('upload-audit.log', options), false);
assert.equal(isManagedUploadLogFileName('account-rotation.log', options), false);
assert.equal(isManagedUploadLogFileName('upload-debug.log', options), false);
assert.equal(isManagedUploadLogFileName('27-08-2026-mdu-session-05-40-599797.txt', options), false);
});
// --- stripModeStampFromFileName ---
const { stripModeStampFromFileName } = require('../lib/log-mode');
+51
View File
@@ -222,6 +222,57 @@ test('packages every Electron preload referenced by the main process', () => {
assert.equal(packageJson.build.win.signAndEditExecutable, false);
});
test('read-own-upload-log discovers base daily session and both fallback directories without synchronous reads', async () => {
const mainSource = fs.readFileSync(path.join(projectRoot, 'main.js'), 'utf8');
const blockStart = mainSource.indexOf("ipcMain.handle('read-own-upload-log'");
const blockEnd = mainSource.indexOf("\nipcMain.handle('import-upload-log'", blockStart);
assert.notEqual(blockStart, -1);
assert.notEqual(blockEnd, -1);
const handlers = new Map();
const configured = 'C:\\configured';
const desktop = 'C:\\desktop';
const userData = 'C:\\user-data';
const entriesByDirectory = new Map([
[configured, ['custom.txt', 'custom-2026-08-27.txt', '27-08-2026-mdu-session-05-40-111111.txt', 'upload-audit.log']],
[desktop, ['FILEUPLOADER-2026-08-26.LOG', '26-08-2026-MDU-SESSION-05-40-222222.LOG', 'account-rotation.log']],
[userData, ['fileuploader-2026-08-25.log', '25-08-2026-mdu-session-05-40-333333.log', 'upload-debug.log']]
]);
const fileNames = new Map();
for (const [directory, names] of entriesByDirectory) {
for (const name of names) {
if (name.includes('audit') || name.includes('rotation') || name.includes('debug')) continue;
fileNames.set(path.win32.join(directory, name), `${name}.mkv`);
}
}
const fakeFs = {
readdirSync: directory => entriesByDirectory.get(directory) || [],
existsSync: filePath => fileNames.has(filePath),
readFileSync: () => { throw new Error('synchronous read forbidden'); },
promises: {
readFile: async filePath => require('../lib/upload-log').formatUploadLogLine(
new Date(2026, 7, 27, 5, 40, 0),
'voe.sx',
'https://voe.sx/e/test',
fileNames.get(filePath)
)
}
};
vm.runInNewContext(mainSource.slice(blockStart, blockEnd), {
_resolveUploadLogTarget: () => ({ path: path.win32.join(configured, '27-08-2026-mdu-session-05-40-111111.txt') }),
app: { getPath: name => name === 'desktop' ? desktop : userData },
fs: fakeFs,
getBaseLogFilePath: () => path.win32.join(configured, 'custom.txt'),
getSafeDesktopDir: () => desktop,
ipcMain: { handle: (channel, handler) => handlers.set(channel, handler) },
isManagedUploadLogFileName: require('../lib/log-mode').isManagedUploadLogFileName,
parseUploadLogLine: require('../lib/upload-log').parseUploadLogLine,
path: path.win32
});
const entries = await handlers.get('read-own-upload-log')();
assert.deepEqual([...entries.map(entry => entry.fileName)].sort(), [...fileNames.values()].sort());
});
test('exposes managed online backup operations through narrow IPC boundaries', () => {
const preloadSource = fs.readFileSync(path.join(projectRoot, 'preload.js'), 'utf8');
const mainSource = fs.readFileSync(path.join(projectRoot, 'main.js'), 'utf8');
+59 -1
View File
@@ -685,6 +685,57 @@ contextBridge.exposeInMainWorld('api', {
acceptedNames: historyEvaluation.candidates.map(file => file.name).sort(),
resultingJobs: historyEvaluation.summary.resultingJobs
};
_completedUploadKeys.clear();
const completedFile = { path: 'C:\\history\\completed-in-session.mkv', name: 'completed-in-session.mkv', size: 1 };
config.globalSettings.removeFromQueueOnDone = true;
config.globalSettings.folderMonitor = {
enabled: true,
folderPath: 'C:\\history',
hosters: ['doodstream.com'],
autoStart: false,
queueLimitJobs: 15000,
paused: false
};
const completedJob = {
id: 'completed-in-session',
file: completedFile.path,
fileName: completedFile.name,
hoster: 'doodstream.com',
status: 'queued',
bytesTotal: 1,
automationAdmission: true
};
queueJobs = [completedJob];
selectedFiles = [];
uploading = false;
rebuildJobIndex();
window.api.configureAutomationProbe({ paused: false, history: [], uploadLog: [] });
handleProgress({
jobId: completedJob.id,
fileName: completedJob.fileName,
hoster: completedJob.hoster,
status: 'done',
bytesUploaded: 1,
bytesTotal: 1,
progress: 1,
result: { download_url: 'https://doodstream.com/d/completed-in-session' }
});
_doneRemovalCoalescer?.drainSync();
automationEvidenceSnapshotGeneration++;
automationEvidenceSnapshotCache = null;
const removedAfterDone = !queueJobs.some(job => job.id === completedJob.id);
const completedKeyPresent = _completedUploadKeys.has(completedFile.path + '|doodstream.com');
const completedResult = await handleFolderMonitorFiles([completedFile]);
const completedProbe = await window.api.getAutomationProbeState();
const completedEvidence = {
removedAfterDone,
completedKeyPresent,
admittedFiles: completedResult.admittedFiles.length,
matchingQueueJobs: queueJobs.filter(job => normalizeAutomationPath(job.file) === normalizeAutomationPath(completedFile.path)).length,
startOrInjectCalls: completedProbe.mutationCalls.filter(call => call[0] === 'start' || call[0] === 'inject').length
};
_completedUploadKeys.clear();
config.globalSettings.removeFromQueueOnDone = false;
config.globalSettings.folderMonitor = {
enabled: true,
folderPath: 'C:\\\\pending',
@@ -2020,7 +2071,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, historyEvidence, pendingDedup, parallelAdmission, evidenceSnapshotDrain, separatedEventEvidence, distinctParallel, disjointClassification, manualHostTransactional, atomic, status, zeroAdmission, stress, persistedQueueExactness, stale, replannedEligibility, mainPauseResponses, cleanupRollback, crossPathCleanupRollback, partialAddOutcomes, collisionResolver, collisionAdmission, pauseBetweenApplyAndStart, startAcceptance, fulfilledFeedback, injectionOutcomes, pauseMarker, resumeQueue, paused };
return { dry, manualTest, historyEvidence, completedEvidence, pendingDedup, parallelAdmission, evidenceSnapshotDrain, separatedEventEvidence, distinctParallel, disjointClassification, manualHostTransactional, atomic, status, zeroAdmission, stress, persistedQueueExactness, stale, replannedEligibility, mainPauseResponses, cleanupRollback, crossPathCleanupRollback, partialAddOutcomes, collisionResolver, collisionAdmission, pauseBetweenApplyAndStart, startAcceptance, fulfilledFeedback, injectionOutcomes, pauseMarker, resumeQueue, paused };
})()`;
const automationControlCenterScript = `(async () => {
const waitFor = async predicate => {
@@ -2799,6 +2850,13 @@ app.whenReady().then(async () => {
acceptedNames: ['aborted.mkv', 'all-failed.mkv', 'error.mkv', 'same-name.mkv', 'same-name.mkv', 'skipped.mkv'],
resultingJobs: 6
});
assert.deepEqual(result.automationPipeline.completedEvidence, {
removedAfterDone: true,
completedKeyPresent: true,
admittedFiles: 0,
matchingQueueJobs: 0,
startOrInjectCalls: 0
});
assert.deepEqual(result.automationPipeline.pendingDedup, {
evaluatedNames: ['new.mkv'],
pendingPaths: ['c:/pending/overlap.mkv'],