import type { ExtensionContext, Memento, Uri } from 'vscode' import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs' import * as os from 'node:os' import * as path from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import { DEFAULT_COMMIT_PROFILE_NAME } from '../cc/commitProfile' import { getEffectiveCommitProfilePath, getSettings, persistSeededProfiles, saveSettings, SETTINGS_KEY } from './store' const tempDirs: string[] = [] afterEach(() => { vi.restoreAllMocks() while (tempDirs.length > 0) { const dir = tempDirs.pop() if (dir) rmSync(dir, { recursive: true, force: true }) } }) function tempDir(): string { const dir = mkdtempSync(path.join(os.tmpdir(), 'settings-store-')) tempDirs.push(dir) return dir } function fakeCtx(dir: string, memento: Record = {}): ExtensionContext { return { extensionPath: path.resolve(__dirname, '../..'), globalStorageUri: { fsPath: dir } as Uri, globalState: { get: (key: string): T | undefined => memento[key] as T | undefined, update: async (key: string, value: unknown) => { memento[key] = value }, keys: () => Object.keys(memento), } as unknown as Memento, } as ExtensionContext } describe('saveSettings 落盘', () => { it('目录配置落盘后才切换候选目录,随后通知问答刷新', async () => { const ctx = fakeCtx(tempDir()) const profileModule = await import('../cc/profiles.js') const setDirectory = vi.spyOn(profileModule, 'setProfilesDirOverride').mockImplementation(() => { expect(getSettings(ctx).profilesDirectory).toBe('/new/profiles') }) const { onDidChangeSettings } = await import('./store.js') const subscription = onDidChangeSettings(ctx, () => { expect(setDirectory).toHaveBeenCalledWith('/new/profiles') }) try { await saveSettings(ctx, { profilesDirectory: '/new/profiles' }) expect(setDirectory).toHaveBeenCalledOnce() } finally { subscription.dispose() setDirectory.mockRestore() } }) it('文件问答默认模型独立落盘,重启读取且不受旧的上次选择影响', async () => { const dir = tempDir() const ctx = fakeCtx(dir, { 'superpowers.codeReader.askProfile': 'sonnet' }) expect(getSettings(ctx).defaultAskProfilePath).toBe('') await saveSettings(ctx, { defaultAskProfilePath: '/p/dsflash.json' }) expect(JSON.parse(readFileSync(path.join(dir, 'settings.json'), 'utf8')).defaultAskProfilePath).toBe('/p/dsflash.json') expect(getSettings(fakeCtx(dir)).defaultAskProfilePath).toBe('/p/dsflash.json') await saveSettings(ctx, { webhookPort: 9000 }) expect(getSettings(ctx).defaultAskProfilePath).toBe('/p/dsflash.json') await saveSettings(ctx, { defaultAskProfilePath: '' }) expect(getSettings(fakeCtx(dir)).defaultAskProfilePath).toBe('') }) it('默认配置保存后通知订阅者,取消订阅后不再通知', async () => { const ctx = fakeCtx(tempDir()) const { onDidChangeSettings } = await import('./store.js') const listener = vi.fn(() => getSettings(ctx).defaultAskProfilePath) const subscription = onDidChangeSettings(ctx, listener) await saveSettings(ctx, { defaultAskProfilePath: '/p/sonnet.json' }) expect(listener).toHaveLastReturnedWith('/p/sonnet.json') subscription.dispose() await saveSettings(ctx, { defaultAskProfilePath: '' }) expect(listener).toHaveBeenCalledTimes(1) }) it('写入 globalStorage 的 settings.json,清空 memento 后仍能读到 commitProfilePath', async () => { const dir = tempDir() const ctx = fakeCtx(dir) await saveSettings(ctx, { commitProfilePath: '/p/deepseek-v4-pro.json' }) const file = path.join(dir, 'settings.json') expect(existsSync(file)).toBe(true) expect(JSON.parse(readFileSync(file, 'utf8')).commitProfilePath).toBe('/p/deepseek-v4-pro.json') const reloaded = fakeCtx(dir, {}) expect(getSettings(reloaded).commitProfilePath).toBe('/p/deepseek-v4-pro.json') expect(await getEffectiveCommitProfilePath(reloaded)).toBe('/p/deepseek-v4-pro.json') }) }) describe('persistSeededProfiles', () => { it('memento 里有旧设置、磁盘无文件时,把旧设置迁到 settings.json', async () => { const dir = tempDir() const ctx = fakeCtx(dir, { [SETTINGS_KEY]: { commitProfilePath: '/old/from-memento.json', webhookPort: 9000 }, }) await persistSeededProfiles(ctx) const reloaded = fakeCtx(dir, {}) expect(getSettings(reloaded).commitProfilePath).toBe('/old/from-memento.json') expect(getSettings(reloaded).webhookPort).toBe(9000) }) it('key 缺失且本机有 deepseek-v4-pro 时,把 seed 写进 settings.json', async () => { const dir = tempDir() const ctx = fakeCtx(dir) vi.spyOn(await import('../cc/profiles.js'), 'listClaudeProfiles').mockResolvedValue([ { name: DEFAULT_COMMIT_PROFILE_NAME, path: '/p/deepseek-v4-pro.json' }, ]) await persistSeededProfiles(ctx) const reloaded = fakeCtx(dir, {}) expect(getSettings(reloaded).commitProfilePath).toBe('/p/deepseek-v4-pro.json') }) it('用户主动存了空串,不再被 seed 覆盖', async () => { const dir = tempDir() const ctx = fakeCtx(dir) await saveSettings(ctx, { commitProfilePath: '' }) vi.spyOn(await import('../cc/profiles.js'), 'listClaudeProfiles').mockResolvedValue([ { name: DEFAULT_COMMIT_PROFILE_NAME, path: '/p/deepseek-v4-pro.json' }, ]) await persistSeededProfiles(ctx) expect(getSettings(ctx).commitProfilePath).toBe('') }) })