✨ 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,114 @@
|
||||
import type { AskEnd, AskStart, RunningAsk } from './asks'
|
||||
import type { Answer, AskRun } from './claudeAsk'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { ClaudeCancelledError, ClaudeError } from '../../cc/spawnClaude'
|
||||
import { QaAsks } from './asks'
|
||||
|
||||
/** 假的回答:每次开问记下来,由测试决定什么时候答完 / 出错;叫停时像真的一样抛 ClaudeCancelledError */
|
||||
function fakeRunner(): { runner: (run: AskRun) => Promise<Answer>, runs: (AskRun & { answer: (answer: Answer) => void, fail: (err: Error) => void })[] } {
|
||||
const runs: (AskRun & { answer: (answer: Answer) => void, fail: (err: Error) => void })[] = []
|
||||
return {
|
||||
runs,
|
||||
runner: run => new Promise<Answer>((resolve, reject) => {
|
||||
run.signal.addEventListener('abort', () => reject(new ClaudeCancelledError('')))
|
||||
runs.push({ ...run, answer: resolve, fail: reject })
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
function start(askId: string, anchor = askId, owner = 'file:///a.py'): AskStart {
|
||||
return { owner, askId, anchor, question: `问 ${askId}`, run: { prompt: `完整的 ${askId}`, cwd: '/work' } }
|
||||
}
|
||||
|
||||
function listener(): { updates: RunningAsk[], ends: [RunningAsk, AskEnd][], ended: Promise<void>, update: (ask: RunningAsk) => void, end: (ask: RunningAsk, end: AskEnd) => void } {
|
||||
let done: () => void = () => {}
|
||||
const record = {
|
||||
updates: [] as RunningAsk[],
|
||||
ends: [] as [RunningAsk, AskEnd][],
|
||||
ended: new Promise<void>((resolve) => {
|
||||
done = resolve
|
||||
}),
|
||||
update: (ask: RunningAsk) => record.updates.push(ask),
|
||||
end: (ask: RunningAsk, end: AskEnd) => {
|
||||
record.ends.push([ask, end])
|
||||
done()
|
||||
},
|
||||
}
|
||||
return record
|
||||
}
|
||||
|
||||
describe('正在答的问题', () => {
|
||||
it('回答长出来、换了在做的事都报给界面;答完报 done,不再算在答', async () => {
|
||||
const { runner, runs } = fakeRunner()
|
||||
const asks = new QaAsks(runner)
|
||||
const events = listener()
|
||||
expect(asks.start(start('1'), events)).toBeUndefined()
|
||||
expect(runs[0]).toMatchObject({ prompt: '完整的 1', cwd: '/work' })
|
||||
runs[0].onEvent({ kind: 'session', sessionId: 's' })
|
||||
runs[0].onEvent({ kind: 'activity', label: '读取 m.py' })
|
||||
runs[0].onEvent({ kind: 'text', text: '答' })
|
||||
expect(events.updates).toEqual([
|
||||
{ askId: '1', anchor: '1', question: '问 1', text: '', activity: '读取 m.py' },
|
||||
{ askId: '1', anchor: '1', question: '问 1', text: '答' },
|
||||
])
|
||||
expect(asks.runningFor('file:///a.py')).toEqual([{ askId: '1', anchor: '1', question: '问 1', text: '答' }])
|
||||
runs[0].answer({ text: '答案', sessionId: 's' })
|
||||
await events.ended
|
||||
expect(events.ends).toEqual([[{ askId: '1', anchor: '1', question: '问 1', text: '答' }, { status: 'done', answer: { text: '答案', sessionId: 's' } }]])
|
||||
expect(asks.runningFor('file:///a.py')).toEqual([])
|
||||
})
|
||||
|
||||
it('叫停:结束 claude,报 stopped,这块可以再问', async () => {
|
||||
const { runner, runs } = fakeRunner()
|
||||
const asks = new QaAsks(runner)
|
||||
const events = listener()
|
||||
asks.start(start('1'), events)
|
||||
asks.stop('1')
|
||||
await events.ended
|
||||
expect(runs[0].signal.aborted).toBe(true)
|
||||
expect(events.ends[0][1]).toEqual({ status: 'stopped' })
|
||||
expect(asks.start(start('2', '1'), listener())).toBeUndefined()
|
||||
})
|
||||
|
||||
it('出错报 failed,带给人看的原因', async () => {
|
||||
const { runner, runs } = fakeRunner()
|
||||
const asks = new QaAsks(runner)
|
||||
const events = listener()
|
||||
asks.start(start('1'), events)
|
||||
runs[0].fail(new ClaudeError('Claude 退出码非零 (1): 网络断了', ''))
|
||||
await events.ended
|
||||
expect(events.ends[0][1]).toEqual({ status: 'failed', error: 'Claude Code 没答成:Claude 退出码非零 (1): 网络断了' })
|
||||
})
|
||||
|
||||
it('同一块同时只答一个;别的文件里同样记号的块不算同一块', () => {
|
||||
const { runner } = fakeRunner()
|
||||
const asks = new QaAsks(runner)
|
||||
asks.start(start('1', 'function f'), listener())
|
||||
expect(asks.start(start('2', 'function f'), listener())).toBe('这块已经在问了,等它答完或者先停下再问。')
|
||||
expect(asks.start(start('3', 'function f', 'file:///b.py'), listener())).toBeUndefined()
|
||||
expect(asks.start(start('1', 'function g'), listener())).toBe('这个问题已经在问了。')
|
||||
})
|
||||
|
||||
it('不同块可以同时答,最多几个;答完一个又能问', async () => {
|
||||
const { runner, runs } = fakeRunner()
|
||||
const asks = new QaAsks(runner, 2)
|
||||
const first = listener()
|
||||
asks.start(start('1'), first)
|
||||
asks.start(start('2'), listener())
|
||||
expect(asks.start(start('3'), listener())).toBe('同时最多问 2 个问题,等前面的答完再问。')
|
||||
runs[0].answer({ text: '答', sessionId: 's' })
|
||||
await first.ended
|
||||
expect(asks.start(start('3'), listener())).toBeUndefined()
|
||||
})
|
||||
|
||||
it('全部叫停', async () => {
|
||||
const { runner, runs } = fakeRunner()
|
||||
const asks = new QaAsks(runner)
|
||||
const [a, b] = [listener(), listener()]
|
||||
asks.start(start('1'), a)
|
||||
asks.start(start('2'), b)
|
||||
asks.stopAll()
|
||||
await Promise.all([a.ended, b.ended])
|
||||
expect(runs.every(run => run.signal.aborted)).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user