Files
goodbuddy/src/main/application-settings-store.test.ts
T

815 lines
29 KiB
TypeScript

import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
ApplicationSettingsStore,
applicationSettingsSchema,
applicationSettingsUpdateSchema,
defaultApplicationSettings
} from './application-settings-store'
const temporaryDirectories: string[] = []
const managedMigrationResolver = async () => ({})
function createApplicationSettingsStore(
filePath: string
): ApplicationSettingsStore {
return new ApplicationSettingsStore(filePath, managedMigrationResolver)
}
async function createStore(): Promise<{
directory: string
filePath: string
store: ApplicationSettingsStore
}> {
const directory = await mkdtemp(
join(tmpdir(), 'goodbuddy-application-settings-')
)
temporaryDirectories.push(directory)
const filePath = join(directory, 'application-settings.json')
return {
directory,
filePath,
store: createApplicationSettingsStore(filePath)
}
}
afterEach(async () => {
await Promise.all(
temporaryDirectories
.splice(0)
.map((directory) => rm(directory, { recursive: true, force: true }))
)
})
describe('ApplicationSettingsStore', () => {
it('keeps the confirmed snapshot when an atomic save fails', async () => {
const { filePath, store } = await createStore()
const confirmed = await store.get()
const changed = vi.fn()
store.onChanged(changed)
await mkdir(filePath)
await expect(store.update({ localInferenceEnabled: false })).rejects.toThrow()
expect(await store.get()).toEqual(confirmed)
expect(changed).not.toHaveBeenCalled()
})
it('defaults missing released Notes fields to enabled without resetting unrelated values', async () => {
const { filePath, store } = await createStore()
const legacy: Record<string, unknown> = { ...defaultApplicationSettings, version: 11, lastSeenReleaseNotesVersion: null, checkUpdatesOnStartup: false }
for (const key of ['applicationNavigation', 'localInferenceEnabled', 'magicNotesEnabled', 'magicNotesShowIncompleteTodoCount']) delete legacy[key]
await writeFile(filePath, JSON.stringify(legacy), 'utf8')
expect(await store.get()).toEqual({ ...defaultApplicationSettings, checkUpdatesOnStartup: false })
expect(await createApplicationSettingsStore(filePath).get()).toEqual(await store.get())
})
it('migrates released settings without replacing explicit false values or comment choices', async () => {
const { filePath, store } = await createStore()
const legacy: Record<string, unknown> = { ...defaultApplicationSettings, version: 11, lastSeenReleaseNotesVersion: null, magicNotesEnabled: false, magicNotesShowIncompleteTodoCount: false, magicNoteCommentMode: 'after-save-manual', magicNoteCommentFormat: 'structured' }
for (const key of ['applicationNavigation', 'localInferenceEnabled']) delete legacy[key]
await writeFile(filePath, JSON.stringify(legacy), 'utf8')
const migrated = await store.get()
expect(migrated).toMatchObject({ magicNotesEnabled: false, magicNotesShowIncompleteTodoCount: false, localInferenceEnabled: true, magicNoteCommentMode: 'after-save-manual', magicNoteCommentFormat: 'structured' })
await store.update({ localInferenceEnabled: false })
await store.update({ applicationNavigation: { ...migrated.applicationNavigation, order: [...migrated.applicationNavigation.order].reverse(), pinned: { ...migrated.applicationNavigation.pinned, 'local-inference': false } } })
const reloaded = await createApplicationSettingsStore(filePath).get()
expect(reloaded).toMatchObject({ magicNotesEnabled: false, magicNotesShowIncompleteTodoCount: false, localInferenceEnabled: false, magicNoteCommentMode: 'after-save-manual', magicNoteCommentFormat: 'structured' })
expect(reloaded.applicationNavigation.order).toEqual([...migrated.applicationNavigation.order].reverse())
expect(reloaded.applicationNavigation.pinned['local-inference']).toBe(false)
})
it('rejects incomplete, duplicate, unknown order and pin values without applying defaults to patches', () => {
const navigation = defaultApplicationSettings.applicationNavigation
for (const invalid of [
{ ...navigation, order: navigation.order.slice(1) },
{ ...navigation, order: ['magic-notes', 'magic-notes'] },
{ ...navigation, order: ['unknown', 'local-inference'] },
{ ...navigation, order: ['knowledge', 'local-inference'] },
{ ...navigation, order: ['heartbeat', 'local-inference'] },
{ ...navigation, pinned: { ...navigation.pinned, heartbeat: true } },
{ ...navigation, pinned: { ...navigation.pinned, knowledge: true } },
{ ...navigation, pinned: { ...navigation.pinned, extra: true } },
{ ...navigation, pinned: { knowledge: true } }
]) expect(applicationSettingsUpdateSchema.safeParse({ applicationNavigation: invalid }).success).toBe(false)
expect(applicationSettingsUpdateSchema.parse({ checkUpdatesOnStartup: false })).toEqual({ checkUpdatesOnStartup: false })
})
it('returns defaults without creating a settings file', async () => {
const { directory, store } = await createStore()
await expect(store.get()).resolves.toEqual(defaultApplicationSettings)
await expect(readdir(directory)).resolves.toEqual([])
})
it('persists versioned application preferences', async () => {
const { directory, filePath, store } = await createStore()
await expect(
store.update({
checkUpdatesOnStartup: false,
magicNotesEnabled: false,
magicNotesShowIncompleteTodoCount: false
})
).resolves.toEqual({
...defaultApplicationSettings,
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultApplicationSettings.localToolEnvironment,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false,
magicNotesEnabled: false,
magicNotesShowIncompleteTodoCount: false,
magicNoteCommentMode: 'immediate',
magicNoteCommentFormat: 'combined'
})
await expect(store.get()).resolves.toEqual({
...defaultApplicationSettings,
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultApplicationSettings.localToolEnvironment,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false,
magicNotesEnabled: false,
magicNotesShowIncompleteTodoCount: false,
magicNoteCommentMode: 'immediate',
magicNoteCommentFormat: 'combined'
})
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
...defaultApplicationSettings,
version: 12,
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultApplicationSettings.localToolEnvironment,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false,
magicNotesEnabled: false,
magicNotesShowIncompleteTodoCount: false,
magicNoteCommentMode: 'immediate',
magicNoteCommentFormat: 'combined',
lastSeenReleaseNotesVersion: null
})
expect(
(await readdir(directory)).filter((name) => name.endsWith('.tmp'))
).toEqual([])
})
it('persists the Remote Projects preference', async () => {
const { filePath, store } = await createStore()
await expect(
store.update({ remoteProjectsEnabled: true })
).resolves.toEqual({
...defaultApplicationSettings,
remoteProjectsEnabled: true
})
await expect(
createApplicationSettingsStore(filePath).get()
).resolves.toEqual({
...defaultApplicationSettings,
remoteProjectsEnabled: true
})
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
version: 12,
lastSeenReleaseNotesVersion: null,
...defaultApplicationSettings,
remoteProjectsEnabled: true
})
})
it('creates the parent directory and can reload persisted settings', async () => {
const { directory } = await createStore()
const filePath = join(directory, 'nested', 'application-settings.json')
const store = createApplicationSettingsStore(filePath)
await store.update({
checkUpdatesOnStartup: false,
magicNotesEnabled: false
})
await expect(
createApplicationSettingsStore(filePath).get()
).resolves.toEqual({
...defaultApplicationSettings,
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultApplicationSettings.localToolEnvironment,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false,
magicNotesEnabled: false,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'immediate',
magicNoteCommentFormat: 'combined'
})
})
it.each([1, 2])(
'loads version %s settings missing the field with Magic Notes enabled',
async (version) => {
const { filePath, store } = await createStore()
await writeFile(
filePath,
JSON.stringify({
version,
checkUpdatesOnStartup: false
}),
'utf8'
)
await expect(store.get()).resolves.toEqual({
...defaultApplicationSettings,
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultApplicationSettings.localToolEnvironment,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false,
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'immediate',
magicNoteCommentFormat: 'combined'
})
}
)
it('migrates version 2 Magic Notes settings with the immediate comment mode', async () => {
const { filePath, store } = await createStore()
await writeFile(
filePath,
JSON.stringify({
version: 2,
checkUpdatesOnStartup: false,
magicNotesEnabled: true
}),
'utf8'
)
await expect(store.get()).resolves.toEqual({
...defaultApplicationSettings,
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultApplicationSettings.localToolEnvironment,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false,
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'immediate',
magicNoteCommentFormat: 'combined'
})
})
it('migrates version 3 settings with the combined comment format', async () => {
const { filePath, store } = await createStore()
await writeFile(
filePath,
JSON.stringify({
version: 3,
checkUpdatesOnStartup: false,
magicNotesEnabled: true,
magicNoteCommentMode: 'after-save-manual'
}),
'utf8'
)
await expect(store.get()).resolves.toEqual({
...defaultApplicationSettings,
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultApplicationSettings.localToolEnvironment,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false,
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'after-save-manual',
magicNoteCommentFormat: 'combined'
})
})
it('migrates version 4 settings with no release notes acknowledged', async () => {
const { filePath, store } = await createStore()
await writeFile(
filePath,
JSON.stringify({
version: 4,
checkUpdatesOnStartup: false,
magicNotesEnabled: true,
magicNoteCommentMode: 'after-save-manual',
magicNoteCommentFormat: 'narrative'
}),
'utf8'
)
await expect(store.getLastSeenReleaseNotesVersion()).resolves.toBeNull()
await store.setLastSeenReleaseNotesVersion('0.8.18')
await expect(
createApplicationSettingsStore(filePath).getLastSeenReleaseNotesVersion()
).resolves.toBe('0.8.18')
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
...defaultApplicationSettings,
version: 12,
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultApplicationSettings.localToolEnvironment,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false,
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'after-save-manual',
magicNoteCommentFormat: 'narrative',
lastSeenReleaseNotesVersion: '0.8.18'
})
})
it('migrates version 5 settings to the default GitHub update source', async () => {
const { filePath, store } = await createStore()
await writeFile(
filePath,
JSON.stringify({
version: 5,
checkUpdatesOnStartup: false,
magicNotesEnabled: true,
magicNoteCommentMode: 'after-save-auto',
magicNoteCommentFormat: 'structured',
lastSeenReleaseNotesVersion: '0.8.18'
}),
'utf8'
)
await expect(store.get()).resolves.toEqual({
...defaultApplicationSettings,
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultApplicationSettings.localToolEnvironment,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false,
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'after-save-auto',
magicNoteCommentFormat: 'structured'
})
await expect(store.getLastSeenReleaseNotesVersion()).resolves.toBe('0.8.18')
})
it('migrates version 6 to the default ModelScope source', async () => {
const { filePath, store } = await createStore()
const versionSix = {
version: 6,
checkUpdatesOnStartup: false,
updateSource: 'mirror',
magicNotesEnabled: true,
magicNoteCommentMode: 'after-save-auto',
magicNoteCommentFormat: 'structured',
lastSeenReleaseNotesVersion: '0.8.18'
}
await writeFile(filePath, JSON.stringify(versionSix), 'utf8')
await expect(store.get()).resolves.toEqual({
...defaultApplicationSettings,
checkUpdatesOnStartup: false,
updateSource: 'mirror',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultApplicationSettings.localToolEnvironment,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false,
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'after-save-auto',
magicNoteCommentFormat: 'structured'
})
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
...defaultApplicationSettings,
...versionSix,
version: 12,
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultApplicationSettings.localToolEnvironment,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false,
magicNotesShowIncompleteTodoCount: true
})
await store.update({ modelDownloadSource: 'hugging-face' })
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
...defaultApplicationSettings,
...versionSix,
version: 12,
modelDownloadSource: 'hugging-face',
localToolEnvironment: defaultApplicationSettings.localToolEnvironment,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false,
magicNotesShowIncompleteTodoCount: true
})
})
it('migrates version 7 to show incomplete todo counts', async () => {
const { filePath, store } = await createStore()
const versionSeven = {
version: 7,
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
magicNotesEnabled: true,
magicNoteCommentMode: 'immediate',
magicNoteCommentFormat: 'combined',
lastSeenReleaseNotesVersion: null
}
await writeFile(filePath, JSON.stringify(versionSeven), 'utf8')
await expect(store.get()).resolves.toEqual({
...defaultApplicationSettings,
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultApplicationSettings.localToolEnvironment,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false,
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'immediate',
magicNoteCommentFormat: 'combined'
})
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
...defaultApplicationSettings,
...versionSeven,
version: 12,
localToolEnvironment: defaultApplicationSettings.localToolEnvironment,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false,
magicNotesShowIncompleteTodoCount: true
})
})
it('migrates version 8 with Remote Projects disabled', async () => {
const { filePath, store } = await createStore()
const versionEight = {
version: 8,
checkUpdatesOnStartup: false,
updateSource: 'mirror',
modelDownloadSource: 'hugging-face',
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: false,
magicNoteCommentMode: 'after-save-manual',
magicNoteCommentFormat: 'narrative',
warnings: [{ code: 'application-settings-recovered' }],
lastSeenReleaseNotesVersion: '0.8.18'
}
await writeFile(filePath, JSON.stringify(versionEight), 'utf8')
const migratedSettings = await store.get()
expect(migratedSettings).toEqual({
...defaultApplicationSettings,
checkUpdatesOnStartup: false,
updateSource: 'mirror',
modelDownloadSource: 'hugging-face',
localToolEnvironment: defaultApplicationSettings.localToolEnvironment,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false,
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: false,
magicNoteCommentMode: 'after-save-manual',
magicNoteCommentFormat: 'narrative'
})
await expect(store.getLastSeenReleaseNotesVersion()).resolves.toBe('0.8.18')
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
...defaultApplicationSettings,
...versionEight,
version: 12,
localToolEnvironment: defaultApplicationSettings.localToolEnvironment,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false
})
await store.update({ checkUpdatesOnStartup: true })
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
...defaultApplicationSettings,
...versionEight,
version: 12,
checkUpdatesOnStartup: true,
localToolEnvironment: defaultApplicationSettings.localToolEnvironment,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false
})
})
it('persists independently resolved version 9 runtime selections exactly once', async () => {
const { filePath } = await createStore()
const versionNine = {
version: 9,
checkUpdatesOnStartup: false,
updateSource: 'mirror',
modelDownloadSource: 'hugging-face',
remoteProjectsEnabled: true,
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: false,
magicNoteCommentMode: 'after-save-manual',
magicNoteCommentFormat: 'narrative',
lastSeenReleaseNotesVersion: '0.8.18'
}
await writeFile(filePath, JSON.stringify(versionNine), 'utf8')
let resolverCalls = 0
const store = new ApplicationSettingsStore(filePath, async () => {
resolverCalls += 1
return {
nodeExecutablePath: 'C:\\Tools\\node.exe',
pythonExecutablePath: 'relative/python'
}
})
await expect(store.get()).resolves.toEqual({
...defaultApplicationSettings,
checkUpdatesOnStartup: false,
updateSource: 'mirror',
modelDownloadSource: 'hugging-face',
remoteProjectsEnabled: true,
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: false,
magicNoteCommentMode: 'after-save-manual',
magicNoteCommentFormat: 'narrative',
conversationHtmlRenderingEnabled: true,
localToolEnvironment: {
node: {
source: 'custom',
executablePath: 'C:\\Tools\\node.exe'
},
python: { source: 'managed' },
artifactDownloadSource: 'native'
}
})
expect(resolverCalls).toBe(1)
const persisted = JSON.parse(await readFile(filePath, 'utf8'))
expect(persisted).toEqual({
...defaultApplicationSettings,
...versionNine,
version: 12,
conversationHtmlRenderingEnabled: true,
localToolEnvironment: {
node: {
source: 'custom',
executablePath: 'C:\\Tools\\node.exe'
},
python: { source: 'managed' },
artifactDownloadSource: 'native'
}
})
const reloaded = new ApplicationSettingsStore(filePath, async () => {
throw new Error('A persisted migration must not resolve PATH again')
})
await expect(reloaded.get()).resolves.toEqual(await store.get())
})
it('strictly rejects incomplete full settings', () => {
expect(
applicationSettingsSchema.safeParse(defaultApplicationSettings).success
).toBe(true)
for (const input of [
{},
{ checkUpdatesOnStartup: 'true' },
{
checkUpdatesOnStartup: true,
magicNotesEnabled: true,
anotherSetting: true
},
{ checkUpdatesOnStartup: true },
null
]) {
expect(applicationSettingsSchema.safeParse(input).success).toBe(false)
}
})
it('strictly rejects empty, unknown, and mistyped updates', async () => {
const { directory, store } = await createStore()
for (const input of [
{},
{ checkUpdatesOnStartup: 'true' },
{ modelDownloadSource: 'automatic' },
{ remoteProjectsEnabled: 'false' },
{ anotherSetting: true },
null
]) {
expect(applicationSettingsUpdateSchema.safeParse(input).success).toBe(
false
)
await expect(store.update(input)).rejects.toThrow()
}
await expect(readdir(directory)).resolves.toEqual([])
})
it('merges partial updates without overwriting other settings', async () => {
const { store } = await createStore()
await store.update({ magicNotesEnabled: true })
await expect(
store.update({ checkUpdatesOnStartup: false })
).resolves.toEqual({
...defaultApplicationSettings,
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultApplicationSettings.localToolEnvironment,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false,
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'immediate',
magicNoteCommentFormat: 'combined'
})
})
it('persists the selected mirror update source', async () => {
const { filePath, store } = await createStore()
await expect(store.update({ updateSource: 'mirror' })).resolves.toEqual({
...defaultApplicationSettings,
updateSource: 'mirror'
})
await expect(
createApplicationSettingsStore(filePath).get()
).resolves.toEqual({
...defaultApplicationSettings,
updateSource: 'mirror'
})
})
it('persists disabling conversation HTML rendering', async () => {
const { filePath, store } = await createStore()
await expect(
store.update({ conversationHtmlRenderingEnabled: false })
).resolves.toEqual({
...defaultApplicationSettings,
conversationHtmlRenderingEnabled: false
})
await expect(
createApplicationSettingsStore(filePath).get()
).resolves.toEqual({
...defaultApplicationSettings,
conversationHtmlRenderingEnabled: false
})
})
it('enables conversation HTML rendering when migrating version 10 settings', async () => {
const { filePath, store } = await createStore()
const versionTenSettings: Record<string, unknown> = {
...defaultApplicationSettings,
version: 10,
lastSeenReleaseNotesVersion: '0.8.18'
}
delete versionTenSettings.conversationHtmlRenderingEnabled
await writeFile(filePath, JSON.stringify(versionTenSettings), 'utf8')
await expect(store.get()).resolves.toEqual(defaultApplicationSettings)
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
...versionTenSettings,
version: 12,
conversationHtmlRenderingEnabled: true
})
})
it.each([
'{not-json',
JSON.stringify({
version: 3,
checkUpdatesOnStartup: false,
magicNotesEnabled: 'false'
}),
JSON.stringify({
version: 2,
checkUpdatesOnStartup: false,
magicNotesEnabled: true,
injected: true
}),
JSON.stringify({
version: 2,
checkUpdatesOnStartup: 'false',
magicNotesEnabled: true
})
])('isolates corrupt persisted data and restores defaults', async (data) => {
const { directory, filePath, store } = await createStore()
await writeFile(filePath, data, 'utf8')
await expect(store.get()).resolves.toEqual({
...defaultApplicationSettings,
warnings: [{ code: 'application-settings-recovered' }]
})
const entries = await readdir(directory)
expect(entries).toHaveLength(1)
expect(entries[0]).toMatch(
/^application-settings\.json\.corrupt-\d+-[a-f0-9]{12}$/u
)
expect(await readFile(join(directory, entries[0] ?? ''), 'utf8')).toBe(data)
})
it('preserves settings created by a newer unsupported version', async () => {
const { directory, filePath, store } = await createStore()
const futureSettings = JSON.stringify({
version: 99,
futureField: 'keep-me'
})
await writeFile(filePath, futureSettings, 'utf8')
await expect(store.get()).rejects.toThrow('不支持应用设置版本 99')
expect(await readFile(filePath, 'utf8')).toBe(futureSettings)
expect(
(await readdir(directory)).some((name) =>
name.startsWith('application-settings.json.corrupt-')
)
).toBe(false)
})
it('does not classify an I/O failure as corrupt settings', async () => {
const { directory } = await createStore()
const filePath = join(directory, 'settings-directory')
const store = createApplicationSettingsStore(filePath)
await writeFile(join(directory, 'sentinel'), 'unchanged', 'utf8')
const directoryStore = createApplicationSettingsStore(directory)
await expect(directoryStore.get()).rejects.toThrow(
'Application settings could not be read'
)
expect(await readdir(directory)).toEqual(['sentinel'])
await expect(store.get()).resolves.toEqual(defaultApplicationSettings)
})
it('serializes concurrent updates and leaves complete JSON', async () => {
const { filePath, store } = await createStore()
await Promise.all([
store.update({
checkUpdatesOnStartup: false,
magicNotesEnabled: true
}),
store.update({
checkUpdatesOnStartup: true,
magicNotesEnabled: false
}),
store.update({
checkUpdatesOnStartup: false,
magicNotesEnabled: false
})
])
await expect(store.get()).resolves.toEqual({
...defaultApplicationSettings,
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultApplicationSettings.localToolEnvironment,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false,
magicNotesEnabled: false,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'immediate',
magicNoteCommentFormat: 'combined'
})
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
...defaultApplicationSettings,
version: 12,
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultApplicationSettings.localToolEnvironment,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false,
magicNotesEnabled: false,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'immediate',
magicNoteCommentFormat: 'combined',
lastSeenReleaseNotesVersion: null
})
})
it('continues accepting updates after a validation failure', async () => {
const { store } = await createStore()
await expect(
store.update({
checkUpdatesOnStartup: 'invalid',
magicNotesEnabled: true
})
).rejects.toThrow()
await expect(
store.update({
checkUpdatesOnStartup: false,
magicNotesEnabled: true
})
).resolves.toEqual({
...defaultApplicationSettings,
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultApplicationSettings.localToolEnvironment,
conversationHtmlRenderingEnabled: true,
remoteProjectsEnabled: false,
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'immediate',
magicNoteCommentFormat: 'combined'
})
})
})