mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 08:02:38 +00:00
* 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>
62 lines
2.5 KiB
TypeScript
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'])
|
|
})
|
|
})
|