✨ feat(vscode): 代码阅读加「问 AI」,块下面问 Claude Code,问答存进 .spx 挂在块下
This commit is contained in:
1 parent
badc722640
commit
fb53bb727c
34 files changed
+2726
-43
No files matched your search
@@ -0,0 +1,116 @@
|
||||
import type { runClaudeLines } from '../../cc/spawnClaude'
|
||||
import type { AskDeps, AskRun } from './claudeAsk'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { CLAUDE_MISSING_MSG, ClaudeCancelledError, ClaudeError, ClaudeTimeoutError } from '../../cc/spawnClaude'
|
||||
import { askArgs, askClaude, askFailure } from './claudeAsk'
|
||||
|
||||
type RunLinesOptions = Parameters<typeof runClaudeLines>[0]
|
||||
|
||||
const line = (value: unknown): string => JSON.stringify(value)
|
||||
const init = (session: string): string => line({ type: 'system', subtype: 'init', session_id: session })
|
||||
const delta = (text: string): string => line({ type: 'stream_event', event: { type: 'content_block_delta', delta: { type: 'text_delta', text } }, parent_tool_use_id: null })
|
||||
const result = (text: string, session: string): string => line({ type: 'result', subtype: 'success', is_error: false, result: text, session_id: session })
|
||||
|
||||
/** 假的 claude:记下每次的参数,按次序吐出事先给的行;给了 error 就在吐完后抛它 */
|
||||
function fakeClaude(runs: { lines: string[], error?: Error }[]): { deps: AskDeps, calls: RunLinesOptions[] } {
|
||||
const calls: RunLinesOptions[] = []
|
||||
return {
|
||||
calls,
|
||||
deps: {
|
||||
profilePath: () => '/profiles/offical.json',
|
||||
runLines: async (options) => {
|
||||
const run = runs[calls.length]
|
||||
calls.push(options)
|
||||
run.lines.forEach(options.onLine)
|
||||
if (run.error)
|
||||
throw run.error
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function request(overrides: Partial<AskRun> = {}): AskRun & { events: unknown[] } {
|
||||
const events: unknown[] = []
|
||||
return { prompt: '完整的问题', cwd: '/work', signal: new AbortController().signal, onEvent: event => events.push(event), events, ...overrides }
|
||||
}
|
||||
|
||||
describe('命令行参数', () => {
|
||||
it('只读:只给读、搜的工具,别的一律拒、不等人批,不加载 MCP;不带跳过权限;输出逐字的 stream-json', () => {
|
||||
const args = askArgs('问题', { profilePath: '/p/offical.json' })
|
||||
expect(args).toEqual([
|
||||
'--settings',
|
||||
'/p/offical.json',
|
||||
'--tools',
|
||||
'Read,Grep,Glob',
|
||||
'--allowedTools',
|
||||
'Read,Grep,Glob',
|
||||
'--permission-mode',
|
||||
'dontAsk',
|
||||
'--strict-mcp-config',
|
||||
'-p',
|
||||
'问题',
|
||||
'--output-format',
|
||||
'stream-json',
|
||||
'--verbose',
|
||||
'--include-partial-messages',
|
||||
])
|
||||
expect(args.join(' ')).not.toMatch(/dangerously|bypassPermissions|Edit|Write|Bash/)
|
||||
})
|
||||
|
||||
it('追问带 --resume,放在问题前面;没有 profile 就不带 --settings', () => {
|
||||
const args = askArgs('追问', { resume: 'session-1' })
|
||||
expect(args).not.toContain('--settings')
|
||||
expect(args.slice(args.indexOf('--resume'), args.indexOf('--resume') + 4)).toEqual(['--resume', 'session-1', '-p', '追问'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('问一次', () => {
|
||||
it('在工作区文件夹里跑、带超时和叫停;过程交给 onEvent,答完返回最终回答和会话 id', async () => {
|
||||
const { deps, calls } = fakeClaude([{ lines: [init('s1'), delta('答'), delta('案'), result('答案', 's1')] }])
|
||||
const run = request()
|
||||
expect(await askClaude(run, deps)).toEqual({ text: '答案', sessionId: 's1' })
|
||||
expect(run.events).toEqual([{ kind: 'session', sessionId: 's1' }, { kind: 'text', text: '答' }, { kind: 'text', text: '答案' }])
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(calls[0]).toMatchObject({ cwd: '/work', signal: run.signal, timeoutMs: 300_000 })
|
||||
expect(calls[0].args).toContain('/profiles/offical.json')
|
||||
expect(calls[0].args[calls[0].args.indexOf('-p') + 1]).toBe('完整的问题')
|
||||
})
|
||||
|
||||
it('追问接着上次的会话问短问题', async () => {
|
||||
const { deps, calls } = fakeClaude([{ lines: [result('接着答', 's1')] }])
|
||||
expect(await askClaude(request({ followUp: { sessionId: 's1', prompt: '短问题' } }), deps)).toEqual({ text: '接着答', sessionId: 's1' })
|
||||
expect(calls[0].args.slice(calls[0].args.indexOf('--resume'), calls[0].args.indexOf('--resume') + 4)).toEqual(['--resume', 's1', '-p', '短问题'])
|
||||
})
|
||||
|
||||
it('上次的会话找不到了:按完整问题重新开一次', async () => {
|
||||
const { deps, calls } = fakeClaude([
|
||||
{ lines: [], error: new ClaudeError('Claude 退出码非零 (1): No conversation found with session ID: s0', 'No conversation found with session ID: s0') },
|
||||
{ lines: [init('s2'), result('重新答', 's2')] },
|
||||
])
|
||||
expect(await askClaude(request({ followUp: { sessionId: 's0', prompt: '短问题' } }), deps)).toEqual({ text: '重新答', sessionId: 's2' })
|
||||
expect(calls).toHaveLength(2)
|
||||
expect(calls[1].args).not.toContain('--resume')
|
||||
expect(calls[1].args[calls[1].args.indexOf('-p') + 1]).toBe('完整的问题')
|
||||
})
|
||||
|
||||
it('别的失败不重试,照样抛出来;Claude 报的失败、没给回答也算失败', async () => {
|
||||
const timeout = new ClaudeTimeoutError('超时', '')
|
||||
const failing = fakeClaude([{ lines: [], error: timeout }, { lines: [result('不该到这', 's')] }])
|
||||
await expect(askClaude(request({ followUp: { sessionId: 's0', prompt: '短问题' } }), failing.deps)).rejects.toBe(timeout)
|
||||
expect(failing.calls).toHaveLength(1)
|
||||
await expect(askClaude(request(), fakeClaude([{ lines: [line({ type: 'result', is_error: true, result: 'Invalid API key · Please run /login' })] }]).deps))
|
||||
.rejects
|
||||
.toThrow('Invalid API key')
|
||||
await expect(askClaude(request(), fakeClaude([{ lines: [init('s')] }]).deps)).rejects.toThrow('没给出回答')
|
||||
})
|
||||
})
|
||||
|
||||
describe('失败了给人看的话', () => {
|
||||
it('超时、没装、没登录、别的', () => {
|
||||
expect(askFailure(new ClaudeTimeoutError('超时', ''))).toBe('等了 5 分钟还没答完,已经停掉。可以把问题问得具体一点再试。')
|
||||
expect(askFailure(new ClaudeError(CLAUDE_MISSING_MSG, ''))).toContain('没找到 Claude Code')
|
||||
expect(askFailure(new ClaudeError('Claude 退出码非零 (1): Invalid API key · Please run /login', ''))).toMatch(/^Claude Code 没登录或登录过期了:在终端里运行 claude 登录一下再问。/)
|
||||
expect(askFailure(new ClaudeError('Claude 退出码非零 (1): 网络断了', ''))).toBe('Claude Code 没答成:Claude 退出码非零 (1): 网络断了')
|
||||
expect(askFailure(new ClaudeCancelledError(''))).toBe('Claude Code 没答成:Claude 调用已停止')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user