Files
windmill/chat-sdk/test/stream.test.ts
T
Ruben FiszelandClaude Fable 5.1 e8c02c04cd feat: windmill-chat sdk for chat-mode flows in external frontends and raw apps (#11117)
* feat: windmill-chat sdk for chat-mode flows in external frontends and raw apps

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018aQiZNAU8g17kWkyTryS5J

* fix: keep streamed answers until persisted, finish turns after history fallback

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018aQiZNAU8g17kWkyTryS5J

* feat: ai sdk transport and assistant-ui runtime for windmill-chat

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: finish a turn from the flow result until its answer row lands, hash chat ids without crypto.subtle

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: judge a turn answered by a persisted assistant row, wherever it was fetched

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: attribute a turn's answer to its own jobs, keep a local turn when switching conversations

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: mirror local history on every change, attribute failure-handler answers to the turn

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: new chat per token string in the React hook, idle after destroy, no reorder on view

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: recreate the hook's chat on any credential change, namespace local history per user

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: send the latest inputs from the React hook

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 22:32:45 +02:00

62 lines
2.5 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import { readServerSentEvents } from '../src/api'
import { createStreamEventParser, parseStreamEvents } from '../src/stream'
import { ndjson } from './support'
describe('parseStreamEvents', () => {
test('keeps agent events and skips other lines', () => {
const events = parseStreamEvents(
ndjson(
{ type: 'token_delta', content: 'Hi' },
{ type: 'reasoning_token_delta', content: 'thinking' },
{ type: 'something_else', content: 'x' },
{ type: 'tool_result', call_id: 'c1', function_name: 'lookup', result: '42', success: true }
) + 'not json\n'
)
expect(events.map((e) => e.type)).toEqual(['token_delta', 'reasoning_token_delta', 'tool_result'])
})
})
describe('createStreamEventParser', () => {
test('holds an incomplete line until the rest arrives', () => {
const parser = createStreamEventParser()
const line = JSON.stringify({ type: 'token_delta', content: 'Hello' })
expect(parser.push(line.slice(0, 10))).toEqual([])
expect(parser.push(line.slice(10) + '\n' + '{"type":"token_delta",')).toEqual([
{ type: 'token_delta', content: 'Hello' }
])
expect(parser.push('"content":"!"}')).toEqual([])
expect(parser.flush()).toEqual([{ type: 'token_delta', content: '!' }])
})
})
describe('readServerSentEvents', () => {
test('splits frames that straddle chunks and normalizes CRLF', async () => {
const chunks = ['data: {"a":1}\r\n\r\ndata: {"b"', ':2}\n\ndata: first\ndata: second\n\n', 'data: {"c":3}']
const encoder = new TextEncoder()
const body = new ReadableStream<Uint8Array>({
start(controller) {
for (const c of chunks) controller.enqueue(encoder.encode(c))
controller.close()
}
})
const frames: string[] = []
for await (const data of readServerSentEvents(body)) frames.push(data)
expect(frames).toEqual(['{"a":1}', '{"b":2}', 'first\nsecond', '{"c":3}'])
})
test('keeps a CRLF split across chunks from ending the event', async () => {
const chunks = ['data: first\r', '\ndata: second\r\n\r\ndata: last\r']
const encoder = new TextEncoder()
const body = new ReadableStream<Uint8Array>({
start(controller) {
for (const c of chunks) controller.enqueue(encoder.encode(c))
controller.close()
}
})
const frames: string[] = []
for await (const data of readServerSentEvents(body)) frames.push(data)
expect(frames).toEqual(['first\nsecond', 'last'])
})
})