assistant-stream
Version:
Streaming utilities for AI assistants
208 lines (195 loc) • 8.11 kB
text/typescript
import { afterEach, expect, it, vi } from "vitest";
import {
registerJsonParserBackend,
type JsonParserBackend,
type JsonParserBackendContext,
} from "./json-parser-backend";
import { IncrementalJsonObjectParser } from "./incremental-json-object-parser";
import {
getPartialJsonObjectMeta,
parsePartialJsonObject,
} from "./parse-partial-json-object";
const cleanups: (() => void)[] = [];
afterEach(() => {
for (const cleanup of cleanups.splice(0).reverse()) cleanup();
});
const referenceBackend = (
context: JsonParserBackendContext,
): JsonParserBackend => {
const { text, fallback, createSnapshot } = context;
const parsed = text.trim() ? parsePartialJsonObject(text) : undefined;
const meta = parsed && getPartialJsonObjectMeta(parsed);
const args =
parsed && meta
? createSnapshot(parsed, meta.state, meta.partialPath)
: fallback;
return {
currentArgs: args,
append: (delta) =>
referenceBackend({ ...context, text: text + delta, fallback: args }),
};
};
it("registers through the public entry without a provider-specific import", async () => {
const { registerJsonParserBackend: register } = await import("../../index");
const factory = vi.fn(referenceBackend);
cleanups.push(register(factory));
const parser = IncrementalJsonObjectParser.from('{"value":"hel');
expect(factory).toHaveBeenCalledOnce();
expect(parser.append('lo"}').currentArgs).toMatchObject({ value: "hello" });
expect(parser.currentArgs).toMatchObject({ value: "hel" });
});
it("keeps existing parsers and independently unregisters overlapping registrations", () => {
const before = IncrementalJsonObjectParser.from('{"value":"');
const first = vi.fn(referenceBackend);
const second = vi.fn(referenceBackend);
const removeFirst = registerJsonParserBackend(first);
cleanups.push(removeFirst);
const retained = IncrementalJsonObjectParser.from('{"value":"hel');
const removeSecond = registerJsonParserBackend(second);
cleanups.push(removeSecond);
removeFirst();
removeFirst();
const latest = IncrementalJsonObjectParser.from('{"value":"new');
expect(first).toHaveBeenCalledOnce();
expect(second).toHaveBeenCalledOnce();
removeSecond();
expect(retained.append('lo"}').currentArgs).toMatchObject({ value: "hello" });
expect(latest.append('est"}').currentArgs).toMatchObject({ value: "newest" });
expect(before.append('native"}').currentArgs).toMatchObject({
value: "native",
});
expect(
IncrementalJsonObjectParser.from('{"value":').append("1}").currentArgs,
).toMatchObject({ value: 1 });
expect(first).toHaveBeenCalledOnce();
expect(second).toHaveBeenCalledOnce();
});
it("restores an earlier registration and distinguishes repeated factory registrations", () => {
const first = vi.fn(referenceBackend);
const second = vi.fn(referenceBackend);
const removeFirst = registerJsonParserBackend(first);
const removeDuplicate = registerJsonParserBackend(first);
const removeSecond = registerJsonParserBackend(second);
cleanups.push(removeFirst, removeDuplicate, removeSecond);
removeFirst();
IncrementalJsonObjectParser.from('{"value":');
expect(second).toHaveBeenCalledOnce();
removeSecond();
IncrementalJsonObjectParser.from('{"value":');
expect(first).toHaveBeenCalledOnce();
removeDuplicate();
IncrementalJsonObjectParser.from('{"value":');
expect(first).toHaveBeenCalledOnce();
});
it("shares registration across separately loaded package copies", async () => {
vi.resetModules();
const duplicate = await import("./json-parser-backend");
const factory = vi.fn(referenceBackend);
cleanups.push(duplicate.registerJsonParserBackend(factory));
const parser = IncrementalJsonObjectParser.from('{"value":"hel');
expect(factory).toHaveBeenCalledOnce();
expect(getPartialJsonObjectMeta(parser.currentArgs)).toEqual({
state: "partial",
partialPath: ["value"],
});
expect(parser.append('lo"}').currentArgs).toMatchObject({ value: "hello" });
const other = await import("./incremental-json-object-parser");
const otherMetadata = await import("./parse-partial-json-object");
const otherParser = other.IncrementalJsonObjectParser.from('{"value":"other');
expect(
otherMetadata.getPartialJsonObjectMeta(otherParser.currentArgs),
).toEqual({ state: "partial", partialPath: ["value"] });
expect(otherParser.append(' copy"}').currentArgs).toMatchObject({
value: "other copy",
});
});
it.each(["decline", "throw"])(
"falls back when backend creation fails (%s)",
(failure) => {
cleanups.push(
registerJsonParserBackend(() => {
if (failure === "throw") throw new Error("backend unavailable");
return undefined;
}),
);
const parser = IncrementalJsonObjectParser.from('{"nested":{"value":"hel');
expect(parser.append('lo"}}').currentArgs).toMatchObject({
nested: { value: "hello" },
});
expect(parser.currentArgs).toMatchObject({ nested: { value: "hel" } });
},
);
it.each(["decline", "throw"])(
"replays the accepted prefix when append fails (%s)",
(failure) => {
cleanups.push(
registerJsonParserBackend((context) => ({
...referenceBackend(context),
append: (delta) => ({
...referenceBackend({ ...context, text: context.text + delta }),
append: () => {
if (failure === "throw") throw new Error("unsupported delta");
return undefined;
},
}),
})),
);
const initial = IncrementalJsonObjectParser.from('{"value":"hel');
const parser = initial.append("lo");
const left = parser.append(' world"}');
const right = parser.append(' there"}');
expect(left.currentArgs).toMatchObject({ value: "hello world" });
expect(right.currentArgs).toMatchObject({ value: "hello there" });
expect(initial.currentArgs).toMatchObject({ value: "hel" });
expect(parser.currentArgs).toMatchObject({ value: "hello" });
expect(parser.currentTextLength).toBe('{"value":"hello'.length);
expect(getPartialJsonObjectMeta(left.currentArgs)?.state).toBe("complete");
},
);
it("detaches backend snapshots and attaches metadata without mutating backend values", () => {
const root = { nested: { value: "hel" } };
cleanups.push(
registerJsonParserBackend((context) => ({
currentArgs: context.createSnapshot(root, "partial", ["nested", "value"]),
append: () => undefined,
})),
);
const parser = IncrementalJsonObjectParser.from('{"nested":{"value":"hel');
root.nested.value = "changed";
expect(parser.currentArgs).toMatchObject({ nested: { value: "hel" } });
expect(getPartialJsonObjectMeta(parser.currentArgs)).toEqual({
state: "partial",
partialPath: ["nested", "value"],
});
(parser.currentArgs.nested as { value: string }).value = "caller";
expect(root.nested.value).toBe("changed");
});
it("preserves the complete-input fast path", () => {
const factory = vi.fn(referenceBackend);
cleanups.push(registerJsonParserBackend(factory));
expect(
IncrementalJsonObjectParser.from('{"value":"a }"}').currentArgs,
).toMatchObject({ value: "a }" });
expect(factory).not.toHaveBeenCalled();
const partial = IncrementalJsonObjectParser.from('{"value":"a }');
expect(factory).toHaveBeenCalledOnce();
expect(partial.append(' b"}').currentArgs).toMatchObject({ value: "a } b" });
});
it.each([
'{"__proto__":{"polluted":true}}',
'{"nested":[{"__proto__":{"polluted":true}}]}',
'{"nested":{"constructor":{"prototype":{"polluted":true}}}}',
])("rejects unsafe backend snapshot keys and replays natively: %s", (input) => {
const root = JSON.parse(input);
cleanups.push(
registerJsonParserBackend((context) => ({
currentArgs: context.createSnapshot(root, "partial", []),
append: () => undefined,
})),
);
const fallback = { value: "safe" };
const parser = IncrementalJsonObjectParser.from(input, fallback);
expect(parser.currentArgs).toBe(fallback);
expect(Object.getPrototypeOf(parser.currentArgs)).toBe(Object.prototype);
expect(parser.append(" ").currentArgs).toBe(fallback);
});