assistant-stream
Version:
Streaming utilities for AI assistants
134 lines (124 loc) • 4.26 kB
text/typescript
import { afterEach, describe, expect, it, vi } from "vitest";
import { LineDecoderStream } from "../../utils/stream/LineDecoderStream";
import {
DataStreamChunkDecoder,
DataStreamChunkEncoder,
} from "./serialization";
import { type DataStreamChunk, DataStreamStreamChunkType } from "./chunk-types";
async function collectChunks<T>(stream: ReadableStream<T>): Promise<T[]> {
const reader = stream.getReader();
const chunks: T[] = [];
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
} finally {
reader.releaseLock();
}
return chunks;
}
function createLineStream(lines: string[]): ReadableStream<string> {
return new ReadableStream<string>({
start(controller) {
for (const line of lines) controller.enqueue(line);
controller.close();
},
});
}
describe("DataStreamChunkDecoder", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("decodes text-delta chunks", async () => {
const chunks = await collectChunks(
createLineStream(['0:"hello"', '0:" world"']).pipeThrough(
new DataStreamChunkDecoder(),
),
);
expect(chunks).toEqual([
{ type: DataStreamStreamChunkType.TextDelta, value: "hello" },
{ type: DataStreamStreamChunkType.TextDelta, value: " world" },
]);
});
it("round-trips through DataStreamChunkEncoder", async () => {
const values: DataStreamChunk[] = [
{ type: DataStreamStreamChunkType.TextDelta, value: "hello" },
{ type: DataStreamStreamChunkType.Error, value: { error: "boom" } },
];
const source = new ReadableStream<DataStreamChunk>({
start(controller) {
for (const v of values) controller.enqueue(v);
controller.close();
},
});
const decoded = await collectChunks(
source
.pipeThrough(new DataStreamChunkEncoder())
.pipeThrough(new LineDecoderStream())
.pipeThrough(new DataStreamChunkDecoder()),
);
expect(decoded).toEqual(values);
});
it("drops chunks carrying prototype-pollution keys", async () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const chunks = await collectChunks(
createLineStream([
'0:{"__proto__":{"polluted":true}}',
'0:{"constructor":{"prototype":{"polluted":true}}}',
'0:"ok"',
]).pipeThrough(new DataStreamChunkDecoder()),
);
expect(warn).toHaveBeenCalledTimes(2);
expect(chunks).toEqual([
{ type: DataStreamStreamChunkType.TextDelta, value: "ok" },
]);
});
it("drops chunks that are not valid JSON", async () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const invalid = ["0:{not json", "0:not json"];
const chunks = await collectChunks(
createLineStream([...invalid, '0:"ok"']).pipeThrough(
new DataStreamChunkDecoder(),
),
);
expect(warn.mock.calls.map((c) => c[0])).toEqual(
invalid.map((l) => `Dropped invalid data-stream chunk: ${l}`),
);
expect(chunks).toEqual([
{ type: DataStreamStreamChunkType.TextDelta, value: "ok" },
]);
});
it("skips blank and whitespace-only lines silently", async () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const chunks = await collectChunks(
createLineStream([
"",
'0:"hello"',
" ",
"\t",
'0:" world"',
]).pipeThrough(new DataStreamChunkDecoder()),
);
expect(warn).not.toHaveBeenCalled();
expect(chunks).toEqual([
{ type: DataStreamStreamChunkType.TextDelta, value: "hello" },
{ type: DataStreamStreamChunkType.TextDelta, value: " world" },
]);
});
it("drops a chunk without a colon separator with a warning", async () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const chunks = await collectChunks(
createLineStream(["garbage", '0:"ok"']).pipeThrough(
new DataStreamChunkDecoder(),
),
);
expect(warn).toHaveBeenCalledWith(
"Dropped invalid data-stream chunk: garbage",
);
expect(chunks).toEqual([
{ type: DataStreamStreamChunkType.TextDelta, value: "ok" },
]);
});
});