Fix desktop settings legacy override

This commit is contained in:
Mohamed Boudra
2026-05-03 22:57:33 +07:00
parent f0d96f8e5a
commit 6d8c7c4d2d
2 changed files with 93 additions and 11 deletions
@@ -94,6 +94,58 @@ describe("desktop-settings", () => {
expect(files).toEqual(["desktop-settings.json"]); expect(files).toEqual(["desktop-settings.json"]);
}); });
it("does not let stale legacy renderer settings override an explicit desktop patch", async () => {
const userDataPath = await createTempUserDataDir();
directories.add(userDataPath);
const store = createDesktopSettingsStore({ userDataPath });
const patched = await store.patch({
daemon: {
manageBuiltInDaemon: false,
},
});
const migrated = await store.migrateLegacyRendererSettings({
manageBuiltInDaemon: true,
releaseChannel: "beta",
});
const persisted = JSON.parse(await readFile(settingsFilePath(userDataPath), "utf8")) as {
migrations: { legacyRendererSettingsImported: boolean };
settings: DesktopSettings;
};
expect(patched.daemon.manageBuiltInDaemon).toBe(false);
expect(migrated.daemon.manageBuiltInDaemon).toBe(false);
expect(migrated.releaseChannel).toBe("stable");
expect(persisted.migrations.legacyRendererSettingsImported).toBe(true);
expect(persisted.settings.daemon.manageBuiltInDaemon).toBe(false);
});
it("does not rewrite existing settings while reading them", async () => {
const userDataPath = await createTempUserDataDir();
directories.add(userDataPath);
const raw = JSON.stringify({
version: 1,
settings: {
releaseChannel: "stable",
daemon: {
manageBuiltInDaemon: false,
keepRunningAfterQuit: true,
},
},
migrations: {
legacyRendererSettingsImported: false,
},
});
await writeFile(settingsFilePath(userDataPath), raw);
const store = createDesktopSettingsStore({ userDataPath });
const settings = await store.get();
const persisted = await readFile(settingsFilePath(userDataPath), "utf8");
expect(settings.daemon.manageBuiltInDaemon).toBe(false);
expect(persisted).toBe(raw);
});
it("migrates desktop-owned values from legacy renderer settings once", async () => { it("migrates desktop-owned values from legacy renderer settings once", async () => {
const userDataPath = await createTempUserDataDir(); const userDataPath = await createTempUserDataDir();
directories.add(userDataPath); directories.add(userDataPath);
@@ -1,4 +1,4 @@
import { access, mkdir, readFile, rename, writeFile } from "node:fs/promises"; import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import type { AppReleaseChannel } from "../features/auto-updater.js"; import type { AppReleaseChannel } from "../features/auto-updater.js";
@@ -58,10 +58,17 @@ function coerceBoolean(value: unknown): boolean | null {
return typeof value === "boolean" ? value : null; return typeof value === "boolean" ? value : null;
} }
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
return error instanceof Error;
}
function buildDefaultDocument(): PersistedDesktopSettingsDocument { function buildDefaultDocument(): PersistedDesktopSettingsDocument {
return { return {
version: 1, version: 1,
settings: DEFAULT_DESKTOP_SETTINGS, settings: {
releaseChannel: DEFAULT_DESKTOP_SETTINGS.releaseChannel,
daemon: { ...DEFAULT_DESKTOP_SETTINGS.daemon },
},
migrations: { migrations: {
legacyRendererSettingsImported: false, legacyRendererSettingsImported: false,
}, },
@@ -159,6 +166,10 @@ function mergeDesktopSettings(
}; };
} }
function hasLegacyRendererOwnedPatch(patch: DesktopSettingsPatch): boolean {
return patch.releaseChannel !== undefined || patch.daemon?.manageBuiltInDaemon !== undefined;
}
function coerceDocument(input: unknown): PersistedDesktopSettingsDocument { function coerceDocument(input: unknown): PersistedDesktopSettingsDocument {
if (!isRecord(input)) { if (!isRecord(input)) {
return buildDefaultDocument(); return buildDefaultDocument();
@@ -201,19 +212,31 @@ export function createDesktopSettingsStore({
return cachedDocument; return cachedDocument;
} }
let raw: string;
try { try {
await access(filePath); raw = await readFile(filePath, "utf8");
} catch { } catch (error) {
if (!isNodeError(error) || error.code !== "ENOENT") {
throw error;
}
const document = buildDefaultDocument(); const document = buildDefaultDocument();
await persistDocument(document); await persistDocument(document);
return document; return document;
} }
const document = coerceDocument(JSON.parse(raw));
cachedDocument = document;
return document;
}
async function loadWritableDocument(): Promise<PersistedDesktopSettingsDocument> {
const document = await loadDocument();
await persistDocument(document);
return document;
}
async function initializeLegacyRendererMigration(): Promise<PersistedDesktopSettingsDocument> {
try { try {
const raw = await readFile(filePath, "utf8"); return await loadDocument();
const document = coerceDocument(JSON.parse(raw));
await persistDocument(document);
return document;
} catch { } catch {
const document = buildDefaultDocument(); const document = buildDefaultDocument();
await persistDocument(document); await persistDocument(document);
@@ -228,17 +251,24 @@ export function createDesktopSettingsStore({
}, },
async patch(patch: unknown): Promise<DesktopSettings> { async patch(patch: unknown): Promise<DesktopSettings> {
const current = await loadDocument(); const current = await loadWritableDocument();
const next = mergeDesktopSettings(current.settings, coerceDesktopSettingsPatch(patch)); const coercedPatch = coerceDesktopSettingsPatch(patch);
const next = mergeDesktopSettings(current.settings, coercedPatch);
await persistDocument({ await persistDocument({
...current, ...current,
settings: next, settings: next,
migrations: {
...current.migrations,
legacyRendererSettingsImported:
current.migrations.legacyRendererSettingsImported ||
hasLegacyRendererOwnedPatch(coercedPatch),
},
}); });
return next; return next;
}, },
async migrateLegacyRendererSettings(legacySettings: unknown): Promise<DesktopSettings> { async migrateLegacyRendererSettings(legacySettings: unknown): Promise<DesktopSettings> {
const current = await loadDocument(); const current = await initializeLegacyRendererMigration();
if (current.migrations.legacyRendererSettingsImported) { if (current.migrations.legacyRendererSettingsImported) {
return current.settings; return current.settings;
} }