137 lines
6.3 KiB
TypeScript
137 lines
6.3 KiB
TypeScript
import type { ExtensionContext, Memento, Uri } from 'vscode'
|
|
import type { AskRunner } from './claudeAsk'
|
|
import { mkdtempSync, rmSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import path from 'node:path'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { listClaudeProfiles } from '../../cc/profiles'
|
|
import { getSettings, saveSettings } from '../../settings/store'
|
|
import { CodeQa } from './codeQa'
|
|
|
|
vi.mock('vscode', async () => {
|
|
const { EventEmitter } = await import('node:events')
|
|
return {
|
|
EventEmitter: class {
|
|
private readonly emitter = new EventEmitter()
|
|
event = (listener: (value: unknown) => void) => {
|
|
this.emitter.on('event', listener)
|
|
return { dispose: () => this.emitter.off('event', listener) }
|
|
}
|
|
|
|
fire(value: unknown) { this.emitter.emit('event', value) }
|
|
dispose() { this.emitter.removeAllListeners() }
|
|
},
|
|
Disposable: { from: () => ({ dispose: () => {} }) },
|
|
Uri: {},
|
|
window: { showErrorMessage: vi.fn() },
|
|
}
|
|
})
|
|
|
|
vi.mock('../folderStore', () => ({
|
|
FolderStore: class {
|
|
get() { return {} }
|
|
dispose() {}
|
|
},
|
|
locate: () => ({ folder: { uri: { fsPath: '/workspace', toString: () => 'file:///workspace' } }, key: 'a.ts' }),
|
|
}))
|
|
vi.mock('../editor/pathPreview', () => ({
|
|
pathQaLocation: () => ({ folder: { uri: { fsPath: '/workspace', toString: () => 'file:///workspace' } }, key: 'a.ts' }),
|
|
}))
|
|
vi.mock('./pathAskDescribe', () => ({ describePath: async () => ({ kind: 'file', relativePath: 'a.ts', head: { unavailable: '测试' } }) }))
|
|
vi.mock('../../cc/profiles', () => ({
|
|
getDefaultProfilePath: () => '/p/offical.json',
|
|
getProfilesDir: () => '/p',
|
|
listClaudeProfiles: vi.fn(),
|
|
}))
|
|
|
|
const AVAILABLE = ['offical', 'sonnet', 'dsflash'].map(name => ({ name, path: `/p/${name}.json` }))
|
|
const uri = { fsPath: '/workspace/a.ts', toString: () => 'file:///workspace/a.ts' } as Uri
|
|
const dirs: string[] = []
|
|
const services: CodeQa[] = []
|
|
|
|
function context(): ExtensionContext {
|
|
const dir = mkdtempSync(path.join(tmpdir(), 'file-qa-settings-'))
|
|
dirs.push(dir)
|
|
return {
|
|
extensionPath: path.resolve(__dirname, '../../..'),
|
|
globalStorageUri: { fsPath: dir },
|
|
globalState: { get: (key: string) => key === 'superpowers.codeReader.askProfile' ? 'sonnet' : undefined },
|
|
workspaceState: { get: () => undefined, update: vi.fn() },
|
|
} as unknown as ExtensionContext
|
|
}
|
|
|
|
function service(ctx: ExtensionContext) {
|
|
const runner = vi.fn<AskRunner>(run => new Promise((_resolve, reject) => {
|
|
run.signal.addEventListener('abort', () => reject(new Error('停止')))
|
|
}))
|
|
const qa = new CodeQa(ctx, ctx.workspaceState as Memento, runner)
|
|
services.push(qa)
|
|
return { qa, runner }
|
|
}
|
|
|
|
beforeEach(() => vi.mocked(listClaudeProfiles).mockResolvedValue(AVAILABLE))
|
|
afterEach(() => {
|
|
services.splice(0).forEach(qa => qa.dispose())
|
|
dirs.splice(0).forEach(dir => rmSync(dir, { recursive: true, force: true }))
|
|
})
|
|
|
|
describe('问这个文件实际使用设置的默认模型', () => {
|
|
it('新增官方文件和保存无关设置不改变显式空选择,真实默认变更才重置', async () => {
|
|
const ctx = context()
|
|
await saveSettings(ctx, { defaultAskProfilePath: '/p/sonnet.json' })
|
|
vi.mocked(listClaudeProfiles).mockResolvedValue(AVAILABLE.filter(item => item.name !== 'offical'))
|
|
const { qa, runner } = service(ctx)
|
|
await qa.profiles.select('')
|
|
vi.mocked(listClaudeProfiles).mockResolvedValue(AVAILABLE)
|
|
const shown = vi.fn()
|
|
qa.profiles.subscribe(shown)
|
|
await saveSettings(ctx, { webhookPort: 9000 })
|
|
await vi.waitFor(() => expect(shown).toHaveBeenLastCalledWith(expect.objectContaining({ selected: '' })))
|
|
await qa.askPath(uri, 'file', { askId: '1', question: '这是什么', images: [] })
|
|
expect(runner.mock.calls[0]?.[0]).not.toHaveProperty('profilePath')
|
|
expect(getSettings(ctx).defaultAskProfilePath).toBe('/p/sonnet.json')
|
|
qa.stop('1')
|
|
await saveSettings(ctx, { defaultAskProfilePath: '/p/dsflash.json' })
|
|
await vi.waitFor(() => expect(shown).toHaveBeenLastCalledWith(expect.objectContaining({ selected: 'dsflash' })))
|
|
await qa.askPath(uri, 'file', { askId: '2', question: '这是什么', images: [] })
|
|
expect(runner.mock.calls[1]?.[0].profilePath).toBe('/p/dsflash.json')
|
|
})
|
|
|
|
it('忽略旧选择,临时切换不写默认;重新创建后恢复配置', async () => {
|
|
const ctx = context()
|
|
await saveSettings(ctx, { defaultAskProfilePath: '/p/dsflash.json' })
|
|
const { qa, runner } = service(ctx)
|
|
await qa.askPath(uri, 'file', { askId: '1', question: '这是什么', images: [] })
|
|
expect(runner.mock.calls[0]?.[0].profilePath).toBe('/p/dsflash.json')
|
|
qa.stop('1')
|
|
await qa.profiles.select('sonnet')
|
|
await qa.askPath(uri, 'file', { askId: '2', question: '这是什么', images: [] })
|
|
expect(runner.mock.calls[1]?.[0].profilePath).toBe('/p/sonnet.json')
|
|
expect(getSettings(ctx).defaultAskProfilePath).toBe('/p/dsflash.json')
|
|
const restarted = service(ctx)
|
|
await restarted.qa.askPath(uri, 'file', { askId: '3', question: '这是什么', images: [] })
|
|
expect(restarted.runner.mock.calls[0]?.[0].profilePath).toBe('/p/dsflash.json')
|
|
})
|
|
|
|
it('保存新默认后所有现有预览收到新选择,下一个问题和界面使用同一个模型', async () => {
|
|
const ctx = context()
|
|
const { qa, runner } = service(ctx)
|
|
await qa.profiles.select('sonnet')
|
|
const shown = vi.fn()
|
|
qa.profiles.subscribe(shown)
|
|
await saveSettings(ctx, { defaultAskProfilePath: '/p/dsflash.json' })
|
|
await vi.waitFor(() => expect(shown).toHaveBeenLastCalledWith(expect.objectContaining({ selected: 'dsflash' })))
|
|
await qa.askPath(uri, 'file', { askId: '1', question: '这是什么', images: [] })
|
|
expect(runner.mock.calls[0]?.[0].profilePath).toBe('/p/dsflash.json')
|
|
})
|
|
|
|
it('已删除配置以现有问答错误返回,不能启动模型;代码块问答同样阻止', async () => {
|
|
const ctx = context()
|
|
await saveSettings(ctx, { defaultAskProfilePath: '/p/deleted.json' })
|
|
const { qa, runner } = service(ctx)
|
|
expect(await qa.askPath(uri, 'file', { askId: '1', question: '这是什么', images: [] })).toContain('文件问答模型不可用')
|
|
expect(await qa.ask(uri, { askId: '2', anchor: 'a', question: '问题', prompt: '问题', followUp: '继续', images: [] })).toContain('文件问答模型不可用')
|
|
expect(runner).not.toHaveBeenCalled()
|
|
})
|
|
})
|