mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-07-21 12:22:34 +00:00
Eliminates the fragile "delete the config folder before upgrading" tester
instruction, which was wrong-by-OS because the userData folder name was
derived inconsistently per platform (fee[dB]ack on macOS, slopsmith-desktop
on Linux/Windows).
A. Deterministic paths + migration framework
- Pin the userData name on every OS via app.setName('feedback-desktop') +
build.extraMetadata.name; brand (productName 'fee[dB]ack') unchanged.
- One-time userData migration copies a legacy folder into the new one so
upgraded users don't start fresh (atomic copy-then-rename, fail-soft).
Runs before the single-instance lock / crashReporter, which would otherwise
create userData and defeat the "new dir doesn't exist" gate.
- config-migrations.ts: versioned, ordered, idempotent, fail-soft migration
runner stamped in CONFIG_DIR/config_version.json; logs the active CONFIG_DIR
at startup (closes the Linux ~/.local/share/slopsmith shared-config gap).
B. In-app "Reset / repair configuration" (Settings panel)
- Granular options: reset app settings & caches, clear plugin state & cached
Python deps, and full reset with default-OFF opt-ins for installed plugins /
song library / ML caches.
- config-paths.ts is the single source of truth for per-OS path enumeration;
the song library, installed plugins and ML caches are structurally confined
to optInExtras and never wiped by the safe/full categories.
- Reset stops the backend, deletes immediate paths, includes SQLite WAL/SHM
sidecars + the migration stamp on full reset, and defers Chromium/Crashpad
state to next launch (consumed before any window reopens it). ML caches honor
TORCH_HOME/HF_HOME. Empty selection is a no-op (backend left running).
- SECURITY: destructive resets require a native main-process confirmation
dialog — the renderer bridge is reachable by plugin scripts, so a
renderer-only confirm is not a sufficient gate.
Tests: node:test suites for path enumeration (per-OS + library/plugins
preserved), migration idempotency/fail-soft, reset delete pipeline guarantees,
userData migration, and deferred-deletion schedule/consume. `npm test` green
(adds a test script). codex review --base origin/main clean.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
126 lines
6.0 KiB
JavaScript
126 lines
6.0 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { loadTs } = require('./_load-ts');
|
|
|
|
// resetConfig itself is electron/python-bound, but its delete logic is the pure
|
|
// enumerateConfigPaths → buildDeleteSet → deletePaths pipeline (config-paths.ts).
|
|
// We exercise that pipeline against a real on-disk fake tree to prove the
|
|
// "library & plugins preserved" guarantees end-to-end.
|
|
const { enumerateConfigPaths, buildDeleteSet, deletePaths } = loadTs('src/main/config-paths.ts');
|
|
|
|
function buildFakeTree() {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'feedback-reset-'));
|
|
const userData = path.join(root, 'feedback-desktop');
|
|
const configDir = path.join(userData, 'slopsmith-config');
|
|
const pluginsDir = path.join(userData, 'plugins');
|
|
const dlcDir = path.join(root, 'Library'); // song library lives OUTSIDE userData
|
|
const cacheBase = path.join(root, '.cache');
|
|
|
|
const write = (p, c = 'x') => {
|
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
fs.writeFileSync(p, c);
|
|
};
|
|
|
|
// App settings & caches
|
|
write(path.join(userData, 'slopsmith-desktop.json'));
|
|
write(path.join(userData, 'slopsmith-audio-settings.json'));
|
|
write(path.join(userData, 'soundfonts', 'FluidR3_GM.sf2'));
|
|
write(path.join(userData, 'known-plugins.xml'));
|
|
// Plugin state & python deps
|
|
write(path.join(configDir, 'plugin_state.json'));
|
|
write(path.join(configDir, 'pip_packages', 'somepkg', '__init__.py'));
|
|
write(path.join(configDir, 'plugin_data', 'foo.json'));
|
|
// Backend DBs + config (incl. WAL/SHM sidecars left by an abrupt stop)
|
|
write(path.join(configDir, 'web_library.db'));
|
|
write(path.join(configDir, 'web_library.db-wal'));
|
|
write(path.join(configDir, 'web_library.db-shm'));
|
|
write(path.join(configDir, 'config.json'));
|
|
write(path.join(configDir, 'config_version.json'));
|
|
// Protected: installed plugin, song library, ML caches
|
|
write(path.join(pluginsDir, 'my-plugin', 'plugin.json'));
|
|
write(path.join(dlcDir, 'song.psarc'));
|
|
write(path.join(cacheBase, 'torch', 'model.pt'));
|
|
write(path.join(cacheBase, 'huggingface', 'blob'));
|
|
|
|
const env = {
|
|
platform: process.platform,
|
|
userData,
|
|
home: root,
|
|
configDir,
|
|
dlcDir,
|
|
pluginsDir,
|
|
cacheBase,
|
|
torchHome: path.join(cacheBase, 'torch'),
|
|
hfHome: path.join(cacheBase, 'huggingface'),
|
|
};
|
|
return { root, env, userData, configDir, pluginsDir, dlcDir, cacheBase };
|
|
}
|
|
|
|
function run(selection, env) {
|
|
const cats = enumerateConfigPaths(env);
|
|
return deletePaths(buildDeleteSet(selection, cats));
|
|
}
|
|
|
|
function protectedIntact(t) {
|
|
assert.ok(fs.existsSync(path.join(t.pluginsDir, 'my-plugin', 'plugin.json')), 'installed plugin preserved');
|
|
assert.ok(fs.existsSync(path.join(t.dlcDir, 'song.psarc')), 'song library preserved');
|
|
assert.ok(fs.existsSync(path.join(t.cacheBase, 'torch', 'model.pt')), 'ML cache preserved');
|
|
}
|
|
|
|
test('appSettings reset removes desktop prefs/caches but preserves DBs, plugins and library', () => {
|
|
const t = buildFakeTree();
|
|
run({ appSettings: true }, t.env);
|
|
assert.ok(!fs.existsSync(path.join(t.userData, 'slopsmith-desktop.json')), 'pref deleted');
|
|
assert.ok(!fs.existsSync(path.join(t.userData, 'soundfonts')), 'soundfont cache deleted');
|
|
assert.ok(fs.existsSync(path.join(t.configDir, 'web_library.db')), 'DB preserved');
|
|
assert.ok(fs.existsSync(path.join(t.configDir, 'plugin_state.json')), 'plugin state preserved');
|
|
protectedIntact(t);
|
|
});
|
|
|
|
test('pluginState reset clears plugin state/py-deps but keeps installed plugins, DBs and library', () => {
|
|
const t = buildFakeTree();
|
|
run({ pluginState: true }, t.env);
|
|
assert.ok(!fs.existsSync(path.join(t.configDir, 'plugin_state.json')), 'plugin_state deleted');
|
|
assert.ok(!fs.existsSync(path.join(t.configDir, 'pip_packages')), 'pip_packages deleted');
|
|
assert.ok(!fs.existsSync(path.join(t.configDir, 'plugin_data')), 'plugin_data deleted');
|
|
assert.ok(fs.existsSync(path.join(t.configDir, 'web_library.db')), 'DB preserved');
|
|
assert.ok(fs.existsSync(path.join(t.userData, 'slopsmith-desktop.json')), 'app prefs preserved');
|
|
protectedIntact(t);
|
|
});
|
|
|
|
test('fullReset (no opt-ins) wipes config DBs but still preserves library and installed plugins', () => {
|
|
const t = buildFakeTree();
|
|
run({ fullReset: true }, t.env);
|
|
assert.ok(!fs.existsSync(path.join(t.configDir, 'web_library.db')), 'DB deleted');
|
|
assert.ok(!fs.existsSync(path.join(t.configDir, 'web_library.db-wal')), 'WAL sidecar deleted');
|
|
assert.ok(!fs.existsSync(path.join(t.configDir, 'web_library.db-shm')), 'SHM sidecar deleted');
|
|
assert.ok(!fs.existsSync(path.join(t.configDir, 'config.json')), 'config.json deleted');
|
|
assert.ok(!fs.existsSync(path.join(t.configDir, 'config_version.json')), 'migration stamp deleted');
|
|
assert.ok(!fs.existsSync(path.join(t.userData, 'slopsmith-desktop.json')), 'app prefs deleted');
|
|
assert.ok(!fs.existsSync(path.join(t.configDir, 'plugin_state.json')), 'plugin state deleted');
|
|
protectedIntact(t);
|
|
});
|
|
|
|
test('opt-in flags remove the protected trees', () => {
|
|
const t = buildFakeTree();
|
|
run(
|
|
{ fullReset: true, alsoInstalledPlugins: true, alsoSongLibrary: true, alsoMlCaches: true },
|
|
t.env,
|
|
);
|
|
assert.ok(!fs.existsSync(t.pluginsDir), 'installed plugins removed on opt-in');
|
|
assert.ok(!fs.existsSync(t.dlcDir), 'song library removed on opt-in');
|
|
assert.ok(!fs.existsSync(path.join(t.cacheBase, 'torch')), 'torch cache removed on opt-in');
|
|
assert.ok(!fs.existsSync(path.join(t.cacheBase, 'huggingface')), 'hf cache removed on opt-in');
|
|
});
|
|
|
|
test('deletePaths is fail-soft on a non-existent path', () => {
|
|
const t = buildFakeTree();
|
|
const missing = path.join(t.root, 'does-not-exist');
|
|
const [entry] = deletePaths([missing]);
|
|
assert.equal(entry.ok, true);
|
|
assert.equal(entry.existed, false);
|
|
});
|