115 lines
5.0 KiB
TypeScript
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)
|
|
})
|
|
})
|