From 6d8c7c4d2dcc63bc38b86695bef8edbca6bc662e Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 3 May 2026 22:57:33 +0700 Subject: [PATCH] Fix desktop settings legacy override --- .../src/settings/desktop-settings.test.ts | 52 +++++++++++++++++++ .../desktop/src/settings/desktop-settings.ts | 52 +++++++++++++++---- 2 files changed, 93 insertions(+), 11 deletions(-) diff --git a/packages/desktop/src/settings/desktop-settings.test.ts b/packages/desktop/src/settings/desktop-settings.test.ts index f49b7259b..cd9207c97 100644 --- a/packages/desktop/src/settings/desktop-settings.test.ts +++ b/packages/desktop/src/settings/desktop-settings.test.ts @@ -94,6 +94,58 @@ describe("desktop-settings", () => { 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 () => { const userDataPath = await createTempUserDataDir(); directories.add(userDataPath); diff --git a/packages/desktop/src/settings/desktop-settings.ts b/packages/desktop/src/settings/desktop-settings.ts index 8903d8245..d2f25d617 100644 --- a/packages/desktop/src/settings/desktop-settings.ts +++ b/packages/desktop/src/settings/desktop-settings.ts @@ -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 type { AppReleaseChannel } from "../features/auto-updater.js"; @@ -58,10 +58,17 @@ function coerceBoolean(value: unknown): boolean | null { return typeof value === "boolean" ? value : null; } +function isNodeError(error: unknown): error is NodeJS.ErrnoException { + return error instanceof Error; +} + function buildDefaultDocument(): PersistedDesktopSettingsDocument { return { version: 1, - settings: DEFAULT_DESKTOP_SETTINGS, + settings: { + releaseChannel: DEFAULT_DESKTOP_SETTINGS.releaseChannel, + daemon: { ...DEFAULT_DESKTOP_SETTINGS.daemon }, + }, migrations: { 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 { if (!isRecord(input)) { return buildDefaultDocument(); @@ -201,19 +212,31 @@ export function createDesktopSettingsStore({ return cachedDocument; } + let raw: string; try { - await access(filePath); - } catch { + raw = await readFile(filePath, "utf8"); + } catch (error) { + if (!isNodeError(error) || error.code !== "ENOENT") { + throw error; + } const document = buildDefaultDocument(); await persistDocument(document); return document; } + const document = coerceDocument(JSON.parse(raw)); + cachedDocument = document; + return document; + } + async function loadWritableDocument(): Promise { + const document = await loadDocument(); + await persistDocument(document); + return document; + } + + async function initializeLegacyRendererMigration(): Promise { try { - const raw = await readFile(filePath, "utf8"); - const document = coerceDocument(JSON.parse(raw)); - await persistDocument(document); - return document; + return await loadDocument(); } catch { const document = buildDefaultDocument(); await persistDocument(document); @@ -228,17 +251,24 @@ export function createDesktopSettingsStore({ }, async patch(patch: unknown): Promise { - const current = await loadDocument(); - const next = mergeDesktopSettings(current.settings, coerceDesktopSettingsPatch(patch)); + const current = await loadWritableDocument(); + const coercedPatch = coerceDesktopSettingsPatch(patch); + const next = mergeDesktopSettings(current.settings, coercedPatch); await persistDocument({ ...current, settings: next, + migrations: { + ...current.migrations, + legacyRendererSettingsImported: + current.migrations.legacyRendererSettingsImported || + hasLegacyRendererOwnedPatch(coercedPatch), + }, }); return next; }, async migrateLegacyRendererSettings(legacySettings: unknown): Promise { - const current = await loadDocument(); + const current = await initializeLegacyRendererMigration(); if (current.migrations.legacyRendererSettingsImported) { return current.settings; }