86 lines
2.6 KiB
Dart
86 lines
2.6 KiB
Dart
|
|
import 'dart:convert';
|
||
|
|
|
||
|
|
import 'package:halkyon_llm/src/sse.dart';
|
||
|
|
import 'package:test/test.dart';
|
||
|
|
|
||
|
|
Future<List<(String, String)>> _events(List<String> chunks) async => [
|
||
|
|
await for (final event in parseSse(
|
||
|
|
Stream.fromIterable(chunks.map(utf8.encode)),
|
||
|
|
))
|
||
|
|
(event.type, event.data),
|
||
|
|
];
|
||
|
|
|
||
|
|
void main() {
|
||
|
|
group('parseSse, as the HTML Standard reads an event stream', () {
|
||
|
|
test('an event, typed, its data after one space', () async {
|
||
|
|
expect(await _events(['event: ping\ndata: {"a":1}\n\n']), [
|
||
|
|
('ping', '{"a":1}'),
|
||
|
|
]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('untyped events are messages', () async {
|
||
|
|
expect(await _events(['data: x\n\n']), [('message', 'x')]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('data lines are joined by line feeds', () async {
|
||
|
|
expect(await _events(['data: a\ndata: b\ndata:c\n\n']), [
|
||
|
|
('message', 'a\nb\nc'),
|
||
|
|
]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('only one space after the colon is taken off', () async {
|
||
|
|
expect(await _events(['data: two\n\n']), [('message', ' two')]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test(
|
||
|
|
'lines end in CR LF, LF or CR, also when a chunk cuts CR LF',
|
||
|
|
() async {
|
||
|
|
expect(
|
||
|
|
await _events(['data: a\r', '\n\r\ndata: b\r\rdata: c\n\n']),
|
||
|
|
[('message', 'a'), ('message', 'b'), ('message', 'c')],
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
|
||
|
|
test('comments, id and retry are skipped', () async {
|
||
|
|
expect(
|
||
|
|
await _events([': keep-alive\nid: 7\nretry: 100\ndata: x\n\n']),
|
||
|
|
[('message', 'x')],
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('a field without a colon has an empty value', () async {
|
||
|
|
expect(await _events(['data\n\n']), [('message', '')]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('an event without data is not dispatched', () async {
|
||
|
|
expect(await _events(['event: ping\n\ndata: x\n\n']), [('message', 'x')]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('an event the stream ends in the middle of is dropped', () async {
|
||
|
|
expect(await _events(['data: a\n\ndata: b\n']), [('message', 'a')]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('a byte order mark is skipped', () async {
|
||
|
|
expect(await _events(['\uFEFFdata: x\n\n']), [('message', 'x')]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('characters cut in two by chunks are put together', () async {
|
||
|
|
final bytes = utf8.encode('data: 犬 (いぬ)\n\n');
|
||
|
|
final events = await parseSse(
|
||
|
|
Stream.fromIterable([
|
||
|
|
for (final byte in bytes) [byte],
|
||
|
|
]),
|
||
|
|
).toList();
|
||
|
|
expect(events.single.data, '犬 (いぬ)');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('bytes that are not UTF-8 fail as a FormatException', () {
|
||
|
|
expect(
|
||
|
|
parseSse(Stream.value([0x64, 0x61, 0x74, 0x61, 0x3A, 0xFF, 0x0A, 0x0A]))
|
||
|
|
.toList(),
|
||
|
|
throwsFormatException,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|