mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-07-20 11:51:53 +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>
92 lines
4.3 KiB
JavaScript
92 lines
4.3 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');
|
|
|
|
// config-bootstrap imports `{ app } from 'electron'`, which in plain node resolves
|
|
// to the electron path string (no throw); migrateUserData never touches it.
|
|
const {
|
|
migrateUserData,
|
|
schedulePendingDeletion,
|
|
consumePendingReset,
|
|
} = loadTs('src/main/config-bootstrap.ts');
|
|
|
|
function tmpAppData() {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'feedback-bootstrap-'));
|
|
}
|
|
|
|
test('migrates a legacy userData folder into the new one when the new dir is missing', () => {
|
|
const parent = tmpAppData();
|
|
const legacy = path.join(parent, 'slopsmith-desktop');
|
|
fs.mkdirSync(path.join(legacy, 'slopsmith-config'), { recursive: true });
|
|
fs.writeFileSync(path.join(legacy, 'slopsmith-config', 'config.json'), '{"dlc_dir":"/x"}');
|
|
fs.writeFileSync(path.join(legacy, 'slopsmith-desktop.json'), '{"lanAccess":false}');
|
|
|
|
const newUserData = path.join(parent, 'feedback-desktop');
|
|
const res = migrateUserData(newUserData, '2026-06-26T00:00:00.000Z');
|
|
|
|
assert.equal(res.migrated, true);
|
|
assert.equal(res.from, legacy);
|
|
assert.ok(fs.existsSync(path.join(newUserData, 'slopsmith-desktop.json')), 'top-level file copied');
|
|
assert.ok(fs.existsSync(path.join(newUserData, 'slopsmith-config', 'config.json')), 'nested file copied');
|
|
assert.ok(fs.existsSync(path.join(newUserData, 'userdata-migrated.json')), 'migration marker written');
|
|
// Legacy left in place as a fallback (copy, not move).
|
|
assert.ok(fs.existsSync(path.join(legacy, 'slopsmith-desktop.json')), 'legacy preserved');
|
|
});
|
|
|
|
test('does NOT overwrite when the new userData dir already exists', () => {
|
|
const parent = tmpAppData();
|
|
const legacy = path.join(parent, 'fee[dB]ack');
|
|
fs.mkdirSync(legacy, { recursive: true });
|
|
fs.writeFileSync(path.join(legacy, 'old.json'), 'legacy');
|
|
|
|
const newUserData = path.join(parent, 'feedback-desktop');
|
|
fs.mkdirSync(newUserData, { recursive: true });
|
|
fs.writeFileSync(path.join(newUserData, 'keep.json'), 'mine');
|
|
|
|
const res = migrateUserData(newUserData, '2026-06-26T00:00:00.000Z');
|
|
|
|
assert.equal(res.migrated, false);
|
|
assert.match(res.reason, /already exists/);
|
|
assert.ok(fs.existsSync(path.join(newUserData, 'keep.json')), 'existing data untouched');
|
|
assert.ok(!fs.existsSync(path.join(newUserData, 'old.json')), 'legacy not copied over existing dir');
|
|
});
|
|
|
|
test('no-op when there is no legacy folder to migrate from', () => {
|
|
const parent = tmpAppData();
|
|
const newUserData = path.join(parent, 'feedback-desktop');
|
|
const res = migrateUserData(newUserData, '2026-06-26T00:00:00.000Z');
|
|
assert.equal(res.migrated, false);
|
|
assert.match(res.reason, /no legacy/);
|
|
assert.ok(!fs.existsSync(newUserData), 'new dir not created when nothing to migrate');
|
|
});
|
|
|
|
test('schedulePendingDeletion + consumePendingReset defer and then apply deletions', () => {
|
|
const userData = tmpAppData();
|
|
const held1 = path.join(userData, 'Local Storage');
|
|
const held2 = path.join(userData, 'Crashpad');
|
|
fs.mkdirSync(held1, { recursive: true });
|
|
fs.mkdirSync(held2, { recursive: true });
|
|
fs.writeFileSync(path.join(held1, 'leveldb'), 'x');
|
|
|
|
assert.equal(schedulePendingDeletion(userData, [held1]), true);
|
|
assert.equal(schedulePendingDeletion(userData, [held2, held1]), true); // merge + de-dup
|
|
assert.equal(schedulePendingDeletion(userData, []), true); // nothing to do
|
|
const manifest = path.join(userData, 'pending-reset.json');
|
|
assert.deepEqual(JSON.parse(fs.readFileSync(manifest, 'utf8')).sort(), [held1, held2].sort());
|
|
|
|
// Nothing deleted yet — deletion is deferred to next launch.
|
|
assert.ok(fs.existsSync(held1));
|
|
|
|
const applied = consumePendingReset(userData);
|
|
assert.deepEqual(applied.sort(), [held1, held2].sort());
|
|
assert.ok(!fs.existsSync(held1), 'deferred path removed on consume');
|
|
assert.ok(!fs.existsSync(held2), 'deferred path removed on consume');
|
|
assert.ok(!fs.existsSync(manifest), 'manifest cleared after consume');
|
|
|
|
// Idempotent: a second consume with no manifest is a no-op.
|
|
assert.deepEqual(consumePendingReset(userData), []);
|
|
});
|