halkyon/packages/halkyon_llm/test/sse_test.dart
Tizian 「ティツィアーン」 038aca5439
feat(llm): halkyon_llm, language models' APIs streamed over plain HTTP (ADR 0021, step 2)
A pure Dart package, no SDKs, one interface (LlmClient) over Anthropic's
Messages API, OpenAI's chat completions, in each known host's dialect and
the local servers' (D45), and OpenAI's Responses API, stateless (D46). A
model's turn keeps what its provider wants back with the tools' results:
Anthropic's thinking blocks, the Responses API's reasoning items and
message phase, Gemini's thought signatures. Failures come by kind, the
provider's words cut and without keys.

Tested on replies written from the providers' documentation, replayed in
pieces of 1, 7 and 4,096 bytes; tool/record_llm.dart records real ones
with a key of one's own, which the tests then run on too (D36).
FakeLlmClient for the code that uses it. BSD-3-Clause, as halkyon_oidc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-26 07:17:06 +01:00

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,
);
});
});
}