test(adapters): cover reconnect failures in IM commands

This commit is contained in:
yuehua-meng
2026-09-25 22:09:09 +08:00
parent cd982986a1
commit b7a91f2734
2 changed files with 102 additions and 0 deletions
@@ -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 })