fix: Prevent cached settings writes from resetting desktop preferences

Desktop settings writes update settings.json through SettingsService, while plugin
state writes use the shared CLI settings cache. Reset the shared cache after a
successful atomic SettingsService write so later plugin updates merge against the
fresh file instead of an older in-memory snapshot.

Constraint: Desktop general settings and plugin state share ~/.claude/settings.json but use different write paths
Rejected: Move plugin writes onto SettingsService | broader API change than needed for this regression
Confidence: high
Scope-risk: narrow
Directive: Keep cache invalidation aligned across all settings.json write paths
Tested: bun test src/server/__tests__/settings.test.ts -t "cached CLI settings"
Tested: bun test src/server/__tests__/settings.test.ts src/server/__tests__/plugins.test.ts
Tested: cd desktop && bun run test -- generalSettings settingsStore pluginsSettings
Tested: bun run check:server
Tested: git diff --check
Not-tested: Full bun run verify still fails on unrelated dirty desktop changed-line coverage and CSS color-mix gate
This commit is contained in:
程序员阿江(Relakkes) 2026-05-15 10:54:40 +08:00
parent 62980223cf
commit e292d83282
2 changed files with 40 additions and 0 deletions

View File

@ -21,6 +21,11 @@ import {
} from '../../utils/secureStorage/macOsKeychainHelpers.js'
import type { OpenAIOAuthTokens } from '../../services/openaiAuth/types.js'
import { getModelOptions } from '../../utils/model/modelOptions.js'
import {
getSettingsForSource,
updateSettingsForSource,
} from '../../utils/settings/settings.js'
import { resetSettingsCache } from '../../utils/settings/settingsCache.js'
// ─── Test helpers ─────────────────────────────────────────────────────────────
@ -40,6 +45,7 @@ let originalAnthropicDefaultOpusModel: string | undefined
async function setup() {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-test-'))
resetSettingsCache()
originalConfigDir = process.env.CLAUDE_CONFIG_DIR
originalHome = process.env.HOME
originalUserProfile = process.env.USERPROFILE
@ -72,6 +78,7 @@ async function teardown() {
plainTextStorage.delete()
clearKeychainCache()
clearOpenAIOAuthTokenCache()
resetSettingsCache()
if (originalConfigDir !== undefined) {
process.env.CLAUDE_CONFIG_DIR = originalConfigDir
@ -222,6 +229,37 @@ describe('SettingsService', () => {
expect(settings.model).toBe('claude-haiku-4-5')
})
it('should not let cached CLI settings overwrite desktop settings updates', async () => {
const svc = new SettingsService()
await svc.updateUserSettings({
enabledPlugins: {
'demo@test-market': false,
},
})
expect(getSettingsForSource('userSettings')?.enabledPlugins?.['demo@test-market']).toBe(false)
await svc.updateUserSettings({
language: 'chinese',
desktopNotificationsEnabled: true,
alwaysThinkingEnabled: false,
})
const { error } = updateSettingsForSource('userSettings', {
enabledPlugins: {
...getSettingsForSource('userSettings')?.enabledPlugins,
'demo@test-market': true,
},
})
expect(error).toBeNull()
const settings = await svc.getUserSettings()
expect(settings.language).toBe('chinese')
expect(settings.desktopNotificationsEnabled).toBe(true)
expect(settings.alwaysThinkingEnabled).toBe(false)
expect((settings.enabledPlugins as Record<string, unknown>)['demo@test-market']).toBe(true)
})
it('should read and write project settings', async () => {
const projectRoot = path.join(tmpDir, 'myproject')
await fs.mkdir(path.join(projectRoot, '.claude'), { recursive: true })

View File

@ -15,6 +15,7 @@ import * as os from 'os'
import { ApiError } from '../middleware/errorHandler.js'
import { normalizeJsonObject, readRecoverableJsonFile } from './recoverableJsonFile.js'
import { ensurePersistentStorageUpgraded } from './persistentStorageMigrations.js'
import { resetSettingsCache } from '../../utils/settings/settingsCache.js'
const VALID_PERMISSION_MODES = [
'default',
@ -129,6 +130,7 @@ export class SettingsService {
await fs.mkdir(dir, { recursive: true })
await fs.writeFile(tmpFile, contents, 'utf-8')
await fs.rename(tmpFile, filePath)
resetSettingsCache()
return
} catch (err) {
lastError = err