cc-haha/adapters/common/__tests__/message-buffer.test.ts
程序员阿江(Relakkes) 09d2c4f4db Fix IM stream completion when card text already flushed
DingTalk AI cards can receive all intermediate text before the final
message_complete event. In that case the message buffer is empty, but
the platform card still needs an explicit terminal update to leave the
inputing state.

Constraint: DingTalk AI cards expose completion separately from buffered text delivery
Rejected: Rely on MessageBuffer.complete only | an empty buffer cannot finalize an active card
Confidence: high
Scope-risk: narrow
Directive: Keep IM terminal-state updates independent from whether a text buffer has pending content
Tested: bun test adapters/common/__tests__/message-buffer.test.ts adapters/dingtalk/__tests__/stream-state.test.ts adapters/dingtalk/__tests__/ai-card.test.ts
Tested: bunx tsc -p adapters/tsconfig.json --noEmit
Tested: bun run check:adapters
Tested: cd desktop && bun run build
Tested: bun run check:native
Not-tested: bun run check:coverage is blocked by existing desktop pages.test.tsx sidebar expectation unrelated to IM
2026-05-15 20:04:23 +08:00

118 lines
3.1 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'bun:test'
import { MessageBuffer } from '../message-buffer.js'
describe('MessageBuffer', () => {
it('accumulates text and flushes on complete', async () => {
const flushed: Array<{ text: string; isComplete: boolean }> = []
const buf = new MessageBuffer(
(text, isComplete) => { flushed.push({ text, isComplete }) },
500, // 500ms interval
1000, // 1000 char threshold
)
buf.append('Hello ')
buf.append('World')
await buf.complete()
expect(flushed.length).toBeGreaterThanOrEqual(1)
const allText = flushed.map((f) => f.text).join('')
expect(allText).toBe('Hello World')
// Last flush should be marked complete
expect(flushed[flushed.length - 1]!.isComplete).toBe(true)
})
it('flushes when character threshold is reached', async () => {
const flushed: string[] = []
const buf = new MessageBuffer(
(text) => { flushed.push(text) },
10000, // very long interval (won't trigger)
10, // 10 char threshold
)
buf.append('12345678901') // 11 chars > threshold
// Wait for microtask
await new Promise((r) => setTimeout(r, 10))
expect(flushed.length).toBeGreaterThanOrEqual(1)
buf.reset()
})
it('flushes on timer interval', async () => {
const flushed: string[] = []
const buf = new MessageBuffer(
(text) => { flushed.push(text) },
50, // 50ms interval
1000,
)
buf.append('hi')
// Wait for timer
await new Promise((r) => setTimeout(r, 80))
expect(flushed).toContain('hi')
buf.reset()
})
it('does not flush empty buffer on complete', async () => {
const flushed: string[] = []
const buf = new MessageBuffer(
(text) => { flushed.push(text) },
)
await buf.complete()
expect(flushed.length).toBe(0)
})
it('waits for an in-flight flush before complete resolves', async () => {
let releaseFlush!: () => void
let flushStarted = false
const flushed: Array<{ text: string; isComplete: boolean }> = []
const buf = new MessageBuffer(
async (text, isComplete) => {
flushStarted = true
flushed.push({ text, isComplete })
await new Promise<void>((resolve) => {
releaseFlush = resolve
})
},
10000,
3,
)
buf.append('abcd')
await new Promise((resolve) => setTimeout(resolve, 0))
expect(flushStarted).toBe(true)
let completeResolved = false
const completing = buf.complete().then(() => {
completeResolved = true
})
await new Promise((resolve) => setTimeout(resolve, 0))
expect(completeResolved).toBe(false)
releaseFlush()
await completing
expect(completeResolved).toBe(true)
expect(flushed).toEqual([{ text: 'abcd', isComplete: false }])
})
it('resets properly between messages', async () => {
const flushed: string[] = []
const buf = new MessageBuffer(
(text) => { flushed.push(text) },
500,
1000,
)
buf.append('first')
buf.reset()
buf.append('second')
await buf.complete()
const allText = flushed.map((f) => f).join('')
expect(allText).toBe('second')
})
})