From acf3f663252bc0772f3cd1e93b41f80a4b4eea8a Mon Sep 17 00:00:00 2001 From: winewill Date: Thu, 29 Oct 2026 17:57:09 +0800 Subject: [PATCH] refactor(desktop): share shell settings path resolution --- apps/desktop/src/settings.ts | 16 ++++++++++++---- apps/desktop/src/update-settings.ts | 5 +++-- apps/desktop/tests/update-settings.spec.ts | 14 ++++++++++++++ 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/settings.ts b/apps/desktop/src/settings.ts index a4bb67b3bd..2278c3c523 100644 --- a/apps/desktop/src/settings.ts +++ b/apps/desktop/src/settings.ts @@ -3,7 +3,7 @@ * Ownership: .agents/notes/implemented/architecture/2026-10-08-desktop-shell-configuration.md. */ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' -import { join } from 'node:path' +import { dirname, join } from 'node:path' /** Resolved shell preferences; absent fields use the defaults without rewriting existing files. */ export interface DesktopSettings { @@ -16,20 +16,28 @@ function record(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } +/** + * Resolve the shared document used by shell preferences and update channels. + * @param userData - Electron's userData directory, independent of DSH_HOME. + * @returns Absolute settings path when userData is absolute; no filesystem changes. + */ +export function desktopSettingsPath(userData: string): string { + return join(userData, 'desktop', 'settings.json') +} + /** * Read shell settings, creating the default document exclusively when missing. * @param userData - Electron's userData directory, independent of DSH_HOME. * @returns Validated settings, or defaults when reading, creating or validating fails; changes require a restart. */ export function readDesktopSettings(userData: string): DesktopSettings { - const directory = join(userData, 'desktop') - const path = join(directory, 'settings.json') + const path = desktopSettingsPath(userData) const defaults: DesktopSettings = { updates: { allowTestAuthPopupWindow: false } } try { let raw: string try { raw = readFileSync(path, 'utf8') } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error - mkdirSync(directory, { recursive: true, mode: 0o700 }) + mkdirSync(dirname(path), { recursive: true, mode: 0o700 }) try { writeFileSync(path, `${JSON.stringify(defaults, null, 2)}\n`, { flag: 'wx', mode: 0o600 }) } catch (error) { // An existing document belongs to the user, including one created during initialization. if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error diff --git a/apps/desktop/src/update-settings.ts b/apps/desktop/src/update-settings.ts index 79061e8ce6..c48a8519a9 100644 --- a/apps/desktop/src/update-settings.ts +++ b/apps/desktop/src/update-settings.ts @@ -1,7 +1,8 @@ /** Startup-only file reads and explicit UI writes for shell-owned update channels. */ import { readFile, mkdir } from 'node:fs/promises' -import { dirname, join } from 'node:path' +import { dirname } from 'node:path' import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write' +import { desktopSettingsPath } from './settings.ts' import type { DesktopUpdateChannel } from './update-channel.ts' /** A settings operation failed without applying a new runtime channel. */ @@ -43,7 +44,7 @@ export class DesktopUpdateSettings { * @param packagedChannel - Default selected by the installer build input. */ constructor(userData: string, private readonly packagedChannel: DesktopUpdateChannel) { - this.path = join(userData, 'desktop', 'settings.json') + this.path = desktopSettingsPath(userData) } /** @returns The startup channel, or a failure that must suspend ordinary updates. */ diff --git a/apps/desktop/tests/update-settings.spec.ts b/apps/desktop/tests/update-settings.spec.ts index adec819960..127eaef853 100644 --- a/apps/desktop/tests/update-settings.spec.ts +++ b/apps/desktop/tests/update-settings.spec.ts @@ -2,6 +2,7 @@ import { mkdtemp, mkdir, readFile, writeFile, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { expect, it, onTestFinished } from 'vitest' +import { readDesktopSettings } from '../src/settings.ts' import { DesktopUpdateSettings, DesktopUpdateSettingsError } from '../src/update-settings.ts' async function fixture() { @@ -48,3 +49,16 @@ it('reports unreadable configuration instead of silently choosing a default', as await mkdir(settings.path) expect(await settings.load()).toMatchObject({ kind: 'error', error: { kind: 'read' } }) }) + +it('updates the document initialized by shell settings without losing the popup preference', async () => { + const root = await mkdtemp(join(tmpdir(), 'desktop-shared-settings-')) + onTestFinished(() => rm(root, { recursive: true, force: true })) + expect(readDesktopSettings(root).updates.allowTestAuthPopupWindow).toBe(false) + const settings = new DesktopUpdateSettings(root, 'latest') + await settings.save('nightly') + expect(JSON.parse(await readFile(settings.path, 'utf8'))).toEqual({ + updates: { allowTestAuthPopupWindow: false, channel: 'nightly' }, + }) + expect(readDesktopSettings(root).updates.allowTestAuthPopupWindow).toBe(false) + expect(await settings.load()).toEqual({ kind: 'ready', channel: 'nightly' }) +})