Files
superwork/vscode/src/codeReader/qa/asks.test.ts
T

115 lines
5.0 KiB
TypeScript

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}`, images: [], 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)
})
})