mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 03:43:11 +08:00
test(adapters): cover reconnect failures in IM commands
This commit is contained in:
@@ -187,6 +187,62 @@ async function send(platform: Platform, text: string, options: { unauthorized?:
|
||||
|
||||
for (const platform of platforms) {
|
||||
describe(`${platform} actual module session selection`, () => {
|
||||
it.each(['/status', '/stop', '/clear'])('preserves the binding when %s cannot reconnect and retries the command', async (command) => {
|
||||
const adapter = adapterFor(platform)
|
||||
const chatId = chatFor(platform)
|
||||
const originalProject = path.join(temporaryRoot, `${platform}-command-project`)
|
||||
fs.mkdirSync(originalProject, { recursive: true })
|
||||
adapter.bridge.resetSession(chatId)
|
||||
adapter.clearTransientChatState(chatId)
|
||||
adapter.sessionStore.set(chatId, 'old-cmd', originalProject)
|
||||
const originalBinding = adapter.sessionStore.get(chatId)
|
||||
const creationsBefore = newSessionCount
|
||||
const noticesBefore = notices.length
|
||||
const failedOpen = spyOn(adapter.bridge, 'waitForOpen').mockImplementationOnce(async () => {
|
||||
adapter.bridge.resetSession(chatId)
|
||||
return false
|
||||
})
|
||||
const sendSpy = spyOn(adapter.bridge, 'sendUserMessage')
|
||||
const stopSpy = spyOn(adapter.bridge, 'sendStopGeneration')
|
||||
try {
|
||||
await send(platform, command)
|
||||
|
||||
expect(adapter.sessionStore.get(chatId)).toEqual(originalBinding)
|
||||
expect(newSessionCount).toBe(creationsBefore)
|
||||
expect(sendSpy).not.toHaveBeenCalled()
|
||||
expect(stopSpy).not.toHaveBeenCalled()
|
||||
expect(notices).toHaveLength(noticesBefore + 1)
|
||||
expect(notices.at(-1)).toContain('已保留会话和工作目录')
|
||||
expect(notices.at(-1)).toContain('重试')
|
||||
expect(notices.at(-1)).not.toContain('当前没有活动会话')
|
||||
|
||||
await send(platform, command)
|
||||
|
||||
expect(adapter.bridge.getSessionId(chatId)).toBe('old-cmd')
|
||||
expect(adapter.sessionStore.get(chatId)).toEqual(originalBinding)
|
||||
expect(newSessionCount).toBe(creationsBefore)
|
||||
if (command === '/clear') {
|
||||
expect(sendSpy).toHaveBeenCalledTimes(1)
|
||||
expect(sendSpy).toHaveBeenCalledWith(chatId, '/clear')
|
||||
expect(stopSpy).not.toHaveBeenCalled()
|
||||
} else if (command === '/stop') {
|
||||
expect(stopSpy).toHaveBeenCalledTimes(1)
|
||||
expect(stopSpy).toHaveBeenCalledWith(chatId)
|
||||
expect(sendSpy).not.toHaveBeenCalled()
|
||||
} else {
|
||||
expect(notices.at(-1)).toContain('old-cmd')
|
||||
expect(sendSpy).not.toHaveBeenCalled()
|
||||
expect(stopSpy).not.toHaveBeenCalled()
|
||||
}
|
||||
} finally {
|
||||
failedOpen.mockRestore()
|
||||
sendSpy.mockRestore()
|
||||
stopSpy.mockRestore()
|
||||
adapter.bridge.resetSession(chatId)
|
||||
adapter.clearTransientChatState(chatId)
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the original session and project after reconnect timeout and resumes it on retry', async () => {
|
||||
const adapter = adapterFor(platform)
|
||||
const chatId = chatFor(platform)
|
||||
|
||||
@@ -177,6 +177,52 @@ describe('Telegram entrypoint session routing', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['status', 'stop', 'clear'] as const)('preserves the session and project when /%s cannot reconnect, then retries the original session', async (command) => {
|
||||
const chatId = nextId++
|
||||
const sessionId = `retry-${command}`
|
||||
sessionPaths.set(sessionId, worktree)
|
||||
store.set(String(chatId), sessionId, worktree)
|
||||
const originalBinding = store.get(String(chatId))
|
||||
const creationsBefore = requests.filter((request) => request === 'POST /api/sessions').length
|
||||
const sendMessage = spyOn(WsBridge.prototype, 'sendUserMessage')
|
||||
const sendStop = spyOn(WsBridge.prototype, 'sendStopGeneration')
|
||||
const failedOpen = spyOn(WsBridge.prototype, 'waitForOpen').mockImplementationOnce(async function (this: WsBridge, id) {
|
||||
this.resetSession(id)
|
||||
return false
|
||||
})
|
||||
try {
|
||||
await text(chatId, `/${command}`)
|
||||
// /stop and /clear dispatch asynchronously, so wait for their failure reply.
|
||||
await eventually(() => expect(texts(chatId).at(-1)).toContain('已保留会话和工作目录'))
|
||||
expect(store.get(String(chatId))).toEqual(originalBinding)
|
||||
expect(requests.filter((request) => request === 'POST /api/sessions').length).toBe(creationsBefore)
|
||||
expect(sendMessage).not.toHaveBeenCalled()
|
||||
expect(sendStop).not.toHaveBeenCalled()
|
||||
expect(messages.filter((item) => item.sessionId === sessionId)).toEqual([])
|
||||
expect(texts(chatId).at(-1)).not.toContain('/new')
|
||||
|
||||
await text(chatId, `/${command}`)
|
||||
await eventually(() => {
|
||||
if (command === 'status') {
|
||||
expect(texts(chatId).at(-1)).toContain(sessionId)
|
||||
} else if (command === 'stop') {
|
||||
expect(messages.some((item) => item.sessionId === sessionId && item.message.type === 'stop_generation')).toBe(true)
|
||||
expect(texts(chatId).at(-1)).toContain('已发送停止信号')
|
||||
} else {
|
||||
expect(messages.some((item) => item.sessionId === sessionId && item.message.content === '/clear')).toBe(true)
|
||||
expect(texts(chatId).some((value) => value.includes('已清空当前会话上下文'))).toBe(true)
|
||||
}
|
||||
})
|
||||
expect(store.get(String(chatId))).toEqual(originalBinding)
|
||||
expect(requests.filter((request) => request === 'POST /api/sessions').length).toBe(creationsBefore)
|
||||
expect(failedOpen).toHaveBeenCalledTimes(2)
|
||||
} finally {
|
||||
failedOpen.mockRestore()
|
||||
sendMessage.mockRestore()
|
||||
sendStop.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
it('runs registered history commands after authorization and deduplication', async () => {
|
||||
const before = requests.length
|
||||
await text(701, '/sessions', { userId: 99 })
|
||||
|
||||
Reference in New Issue
Block a user