The byse suspect size-memo (softened in v3.3.73 to arm only after two confirmed suspect rejections on the same account and only for strictly larger files) still occasionally pre-skips a primary account with "Bekanntes Größen-Limit auf diesem Account", which the user finds annoying when they would rather just let every file attempt the upload for real. Add a per-hoster opt-out: a new "Größen-Limit merken" checkbox (Accounts tab, default ON so existing behavior is preserved for everyone) that, when unchecked, disables the memo-based pre-skip entirely for that hoster. - config-store.js / upload-manager.js DEFAULT_SETTINGS: sizeMemoEnabled: true. - upload-manager.js _suspectMemoBlocks(): returns false up front when the hoster has sizeMemoEnabled === false. That single guard covers BOTH consult sites — the pre-job block (skips the guaranteed-fail upload) and the alternate-account skip — since both route through _suspectMemoBlocks. The memo is still recorded; it is simply never allowed to block. This moves the flow toward "fail open": with the toggle off, every file always gets a real attempt on its account. - The genuine suspect-reject alternates walk is gated on err.suspectReject, not the memo, so disabling the memo does not weaken real failover — a file the account actually rejects still rolls over to the next account. - renderer/app.js: checkbox data-hs="sizeMemoEnabled", checked when hs.sizeMemoEnabled !== false; persisted by the existing generic checkbox path in saveHosterSettingsFromDom(). The running manager picks it up on the next batch (fresh UploadManager) and immediately on save (updateSettings). Tests: suspect-reject-alternates gains a disabled-memo case proving the larger third file still gets a real primary attempt (key1 tried 3x, no suspect-memo-skip event); config-store gains a default-true + persist-false case. Full suite 305/305. Reviewed by a 4-lens adversarial workflow (completeness / composition / persistence-ui / conventions): 0 findings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
234 lines
9.9 KiB
JavaScript
234 lines
9.9 KiB
JavaScript
const { describe, it, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
const ConfigStore = require('../lib/config-store');
|
|
|
|
let tmpDir;
|
|
let store;
|
|
|
|
function createStore() {
|
|
const fakeApp = {
|
|
isPackaged: false,
|
|
getPath: () => tmpDir
|
|
};
|
|
// ConfigStore uses path.join(__dirname, '..') for non-packaged
|
|
// We override by setting filePath directly
|
|
store = new ConfigStore(fakeApp);
|
|
store.filePath = path.join(tmpDir, 'electron-config.json');
|
|
return store;
|
|
}
|
|
|
|
describe('ConfigStore', () => {
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cfg-test-'));
|
|
store = createStore();
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('load returns defaults when file does not exist', () => {
|
|
const config = store.load();
|
|
assert.ok(config.hosters);
|
|
assert.ok(config.hosters['doodstream.com']);
|
|
assert.ok(config.hosters['voe.sx']);
|
|
assert.ok(config.hosters['vidmoly.me']);
|
|
assert.ok(config.hosters['byse.sx']);
|
|
assert.ok(config.hosterSettings);
|
|
assert.equal(config.hosterSettings['doodstream.com'].retries, 3);
|
|
assert.equal(config.hosterSettings['doodstream.com'].parallelCount, 2);
|
|
assert.equal(config.globalSettings.alwaysOnTop, false);
|
|
assert.equal(config.globalSettings.shutdownAfterFinish, 'nothing');
|
|
assert.equal(config.globalSettings.logFilePath, '');
|
|
assert.equal(config.globalSettings.resumeQueueOnLaunch, true);
|
|
assert.equal(config.globalSettings.parallelUploadCount, 0);
|
|
assert.equal(config.globalSettings.scaleParallelUploads, false);
|
|
assert.equal(config.globalSettings.pendingQueue, null);
|
|
assert.deepEqual(config.history, []);
|
|
});
|
|
|
|
it('save then load round-trips', async () => {
|
|
await store.save({ hosters: { 'doodstream.com': [{ id: 'test-1', enabled: true, authType: 'api', apiKey: 'test-key-123' }] } });
|
|
const config = store.load();
|
|
assert.equal(config.hosters['doodstream.com'][0].apiKey, 'test-key-123');
|
|
});
|
|
|
|
it('default logMode is "single"', () => {
|
|
const config = store.load();
|
|
assert.equal(config.globalSettings.logMode, 'single');
|
|
});
|
|
|
|
it('regression: legacy sessionLog:true on disk normalizes to logMode "daily" (NOT "session")', async () => {
|
|
// Write a config with the legacy boolean only (what an existing user has).
|
|
await store.save({ globalSettings: { sessionLog: true } });
|
|
const config = store.load();
|
|
// The misnamed legacy field MUST map to daily — mapping to "session" would
|
|
// silently change every per-day user's behaviour on upgrade.
|
|
assert.equal(config.globalSettings.logMode, 'daily');
|
|
});
|
|
|
|
it('logMode round-trips for all three values', async () => {
|
|
for (const mode of ['single', 'daily', 'session']) {
|
|
await store.save({ globalSettings: { logMode: mode } });
|
|
const config = store.load();
|
|
assert.equal(config.globalSettings.logMode, mode, `mode ${mode}`);
|
|
}
|
|
});
|
|
|
|
it('load merges with defaults for missing hosters', () => {
|
|
// Write partial config in old single-object format (triggers migration)
|
|
fs.writeFileSync(store.filePath, JSON.stringify({
|
|
hosters: { 'doodstream.com': { apiKey: 'abc' } }
|
|
}), 'utf-8');
|
|
|
|
const config = store.load();
|
|
// Old format is migrated to array
|
|
assert.ok(Array.isArray(config.hosters['doodstream.com']));
|
|
assert.equal(config.hosters['doodstream.com'][0].apiKey, 'abc');
|
|
// Other hosters should still have defaults (empty arrays)
|
|
assert.ok(Array.isArray(config.hosters['voe.sx']));
|
|
assert.equal(config.hosters['voe.sx'].length, 0);
|
|
});
|
|
|
|
it('hosterSettings merge fills gaps with defaults', () => {
|
|
fs.writeFileSync(store.filePath, JSON.stringify({
|
|
hosterSettings: { 'voe.sx': { retries: 5 } }
|
|
}), 'utf-8');
|
|
|
|
const config = store.load();
|
|
assert.equal(config.hosterSettings['voe.sx'].retries, 5);
|
|
assert.equal(config.hosterSettings['voe.sx'].parallelCount, 2); // default
|
|
assert.equal(config.hosterSettings['voe.sx'].maxSpeedKbs, 0); // default
|
|
assert.equal(config.hosterSettings['voe.sx'].logToFile, true); // default on
|
|
});
|
|
|
|
it('logToFile defaults to true for every hoster', () => {
|
|
const config = store.load();
|
|
for (const name of ['doodstream.com', 'voe.sx', 'vidmoly.me', 'byse.sx', 'clouddrop.cc']) {
|
|
assert.equal(config.hosterSettings[name].logToFile, true, `${name} should default logToFile=true`);
|
|
}
|
|
});
|
|
|
|
it('sizeMemoEnabled defaults to true for every hoster and persists when disabled', async () => {
|
|
const fresh = store.load();
|
|
for (const name of ['doodstream.com', 'voe.sx', 'vidmoly.me', 'byse.sx', 'clouddrop.cc']) {
|
|
assert.equal(fresh.hosterSettings[name].sizeMemoEnabled, true, `${name} should default sizeMemoEnabled=true`);
|
|
}
|
|
await store.save({ hosterSettings: { 'byse.sx': { sizeMemoEnabled: false } } });
|
|
const config = store.load();
|
|
assert.equal(config.hosterSettings['byse.sx'].sizeMemoEnabled, false, 'explicit false preserved');
|
|
assert.equal(config.hosterSettings['voe.sx'].sizeMemoEnabled, true, 'other hoster still defaults on');
|
|
});
|
|
|
|
it('logToFile=false persists and survives reload', async () => {
|
|
await store.save({ hosterSettings: { 'voe.sx': { logToFile: false } } });
|
|
const config = store.load();
|
|
assert.equal(config.hosterSettings['voe.sx'].logToFile, false, 'explicit false preserved');
|
|
assert.equal(config.hosterSettings['byse.sx'].logToFile, true, 'other hoster still defaults on');
|
|
});
|
|
|
|
it('save only updates provided sections', async () => {
|
|
// Save hoster settings first
|
|
await store.save({ hosterSettings: { 'doodstream.com': { retries: 10, maxSpeedKbs: 0, parallelCount: 2, restartBelowKbs: 0, timeIntervalSec: 0, maxSizeMb: 0 } } });
|
|
// Save hosters credentials separately (array format)
|
|
await store.save({ hosters: { 'doodstream.com': [{ id: 'test-1', enabled: true, authType: 'api', apiKey: 'key123' }] } });
|
|
|
|
const config = store.load();
|
|
assert.equal(config.hosters['doodstream.com'][0].apiKey, 'key123');
|
|
assert.equal(config.hosterSettings['doodstream.com'].retries, 10); // preserved
|
|
});
|
|
|
|
it('appendHistory keeps complete history without truncation', async () => {
|
|
for (let i = 0; i < 105; i++) {
|
|
await store.appendHistory({ id: `batch-${i}`, timestamp: new Date().toISOString(), files: [] });
|
|
}
|
|
const history = store.loadHistory();
|
|
assert.equal(history.length, 105);
|
|
assert.equal(history[0].id, 'batch-0');
|
|
assert.equal(history[104].id, 'batch-104');
|
|
});
|
|
|
|
it('clearHistory empties the array', async () => {
|
|
await store.appendHistory({ id: 'test', files: [] });
|
|
assert.equal(store.loadHistory().length, 1);
|
|
await store.clearHistory();
|
|
assert.equal(store.loadHistory().length, 0);
|
|
});
|
|
|
|
it('rotationCursors default to an empty object', () => {
|
|
const config = store.load();
|
|
assert.deepEqual(config.rotationCursors, {});
|
|
});
|
|
|
|
it('saveRotationCursors round-trips and survives reload', async () => {
|
|
await store.saveRotationCursors({ 'byse.sx': 7, 'voe.sx': 2 });
|
|
const config = store.load();
|
|
assert.equal(config.rotationCursors['byse.sx'], 7);
|
|
assert.equal(config.rotationCursors['voe.sx'], 2);
|
|
});
|
|
|
|
it('an unrelated save() does not clobber persisted rotationCursors', async () => {
|
|
await store.saveRotationCursors({ 'byse.sx': 3 });
|
|
await store.save({ globalSettings: { alwaysOnTop: true } });
|
|
const config = store.load();
|
|
assert.equal(config.rotationCursors['byse.sx'], 3, 'cursor preserved across a settings save');
|
|
assert.equal(config.globalSettings.alwaysOnTop, true);
|
|
});
|
|
|
|
it('saveRotationCursors does not disturb credentials', async () => {
|
|
await store.save({ hosters: { 'byse.sx': [{ id: 'k1', enabled: true, authType: 'api', apiKey: 'secret-key' }] } });
|
|
await store.saveRotationCursors({ 'byse.sx': 1 });
|
|
const config = store.load();
|
|
assert.equal(config.hosters['byse.sx'][0].apiKey, 'secret-key');
|
|
assert.equal(config.rotationCursors['byse.sx'], 1);
|
|
});
|
|
|
|
it('corrupted JSON falls back to defaults', () => {
|
|
fs.writeFileSync(store.filePath, '{invalid json!!!', 'utf-8');
|
|
const config = store.load();
|
|
assert.ok(config.hosters);
|
|
assert.ok(config.hosterSettings);
|
|
assert.deepEqual(config.history, []);
|
|
});
|
|
|
|
it('globalSettings merge preserves partial values', () => {
|
|
fs.writeFileSync(store.filePath, JSON.stringify({
|
|
globalSettings: { alwaysOnTop: true }
|
|
}), 'utf-8');
|
|
|
|
const config = store.load();
|
|
assert.equal(config.globalSettings.alwaysOnTop, true);
|
|
assert.equal(config.globalSettings.shutdownAfterFinish, 'nothing'); // default
|
|
assert.equal(config.globalSettings.resumeQueueOnLaunch, true);
|
|
assert.equal(config.globalSettings.parallelUploadCount, 0);
|
|
assert.equal(config.globalSettings.scaleParallelUploads, false);
|
|
assert.equal(config.globalSettings.logFilePath, '');
|
|
});
|
|
|
|
it('concurrent saves preserve both sections', async () => {
|
|
const save1 = store.save({ hosters: { 'doodstream.com': [{ id: 'c1', enabled: true, authType: 'api', apiKey: 'concurrent-key' }] } });
|
|
const save2 = store.save({ globalSettings: { alwaysOnTop: true } });
|
|
await Promise.all([save1, save2]);
|
|
const config = store.load();
|
|
assert.equal(config.hosters['doodstream.com'][0].apiKey, 'concurrent-key');
|
|
assert.equal(config.globalSettings.alwaysOnTop, true);
|
|
});
|
|
|
|
it('backup recovery when main file is corrupted', () => {
|
|
// Write valid config first
|
|
fs.writeFileSync(store.filePath, JSON.stringify({
|
|
hosters: { 'doodstream.com': [{ id: 'bak-1', authType: 'api', apiKey: 'from-backup' }] },
|
|
hosterSettings: {}, globalSettings: {}, history: []
|
|
}), 'utf-8');
|
|
// Copy to backup
|
|
fs.copyFileSync(store.filePath, store.filePath + '.bak');
|
|
// Corrupt main file
|
|
fs.writeFileSync(store.filePath, 'CORRUPTED!!!', 'utf-8');
|
|
const config = store.load();
|
|
assert.equal(config.hosters['doodstream.com'][0].apiKey, 'from-backup');
|
|
});
|
|
});
|