mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
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
118 lines
3.1 KiB
TypeScript
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')
|
|
})
|
|
})
|