UNPKG

assistant-stream

Version:

Streaming utilities for AI assistants

334 lines (292 loc) • 11.7 kB
import { inspect } from "node:util"; import { describe, expect, it } from "vitest"; import { getPartialJsonObjectMeta, parsePartialJsonObject, } from "./parse-partial-json-object"; import { IncrementalJsonObjectParser } from "./incremental-json-object-parser"; import { registerJsonParserBackend } from "./json-parser-backend"; import type { ReadonlyJSONObject, ReadonlyJSONValue } from "./json-value"; const inputs = [ '{"n":2.5e+3,"m":0.25,"k":1E2}', '{"text":"brace } quote \\" slash \\\\ emoji 😀","nested":{"values":[1,-2.5e3,true,false,null]}}', '{"escaped":"line\\nfeed","unicode":"\\uD83D\\uDE00"}', '{"duplicate":"first","duplicate":"second","tail":0}', '{"constructor":1,"tail":"ok"}', '{"2":2,"1":1,"nested":{"10":"ten","0":"zero"}}', '{\n "a" : [ 1 , { "b" : true } ] ,\n "c" : "d"\n}\n', '{"tag-name":"well-known","n":-2.5e-3}', ]; const expectPrefixParity = (input: string) => { let prefix = ""; let expected = parsePartialJsonObject("")!; let parser = IncrementalJsonObjectParser.from(""); expect(parser.currentTextLength).toBe(0); expect(parser.currentArgs).toEqual(expected); for (const delta of input) { prefix += delta; parser = parser.append(delta); expected = parsePartialJsonObject(prefix) ?? expected; expect(parser.currentTextLength).toBe(prefix.length); expect(parser.currentArgs, `prefix=${prefix}`).toEqual(expected); expect(getPartialJsonObjectMeta(parser.currentArgs)).toEqual( getPartialJsonObjectMeta(expected), ); } }; describe("IncrementalJsonObjectParser", () => { it("falls back when the backend args getter throws during creation", () => { const unregister = registerJsonParserBackend(() => ({ get currentArgs(): ReadonlyJSONObject { throw new Error("Backend snapshot unavailable"); }, append: () => undefined, })); try { let text = '{"nested":{"value":"hel'; let parser = IncrementalJsonObjectParser.from(text); expect(() => parser.currentArgs).not.toThrow(); expect(parser.currentArgs).toEqual(parsePartialJsonObject(text)); const args = parser.currentArgs; expect(parser.currentArgs).toBe(args); for (const delta of ["lo", '"},"ready":', "true}"]) { text += delta; expect(() => { parser = parser.append(delta); }).not.toThrow(); expect(parser.currentArgs).toEqual(parsePartialJsonObject(text)); } expect(args).toMatchObject({ nested: { value: "hel" } }); } finally { unregister(); } }); it("falls back when the backend args getter throws after a successful append", () => { let text = '{"nested":{"value":"hel'; const args = parsePartialJsonObject(text)!; const unregister = registerJsonParserBackend(() => ({ currentArgs: args, append: () => ({ get currentArgs(): ReadonlyJSONObject { throw new Error("Backend snapshot unavailable"); }, append: () => undefined, }), })); try { const initial = IncrementalJsonObjectParser.from(text); expect(initial.currentArgs).toBe(args); let parser = initial; for (const delta of ["lo", '"},"ready":', "true}"]) { text += delta; expect(() => { parser = parser.append(delta); }).not.toThrow(); expect(() => parser.currentArgs).not.toThrow(); expect(parser.currentArgs).toEqual(parsePartialJsonObject(text)); const snapshot = parser.currentArgs; expect(parser.currentArgs).toBe(snapshot); } expect(initial.currentArgs).toBe(args); expect(args).toMatchObject({ nested: { value: "hel" } }); } finally { unregister(); } }); it.each([false, true])( "preserves caller-owned fallback objects before parsing starts (frozen=%s)", (frozen) => { const nested = { value: 1 }; const fallback = { nested }; if (frozen) { Object.freeze(nested); Object.freeze(fallback); } for (const text of ["", " "]) { const parser = IncrementalJsonObjectParser.from(text, fallback); expect(parser.currentArgs).toBe(fallback); expect(fallback.nested).toBe(nested); const next = parser.append(" \n"); expect(next.currentArgs).toBe(fallback); expect(fallback.nested).toBe(nested); expect(next.append('{"ready":true}').currentArgs).toMatchObject({ ready: true, }); expect(fallback.nested).toBe(nested); } }, ); it.each(inputs)( "matches parsePartialJsonObject for every prefix", (input) => { expectPrefixParity(input); }, ); it.each(inputs.slice(0, 2))( "matches when initialized from an existing prefix", (input) => { let expected = parsePartialJsonObject("")!; for (let cut = 0; cut <= input.length; cut++) { const prefix = input.slice(0, cut); expected = parsePartialJsonObject(prefix) ?? expected; const parser = IncrementalJsonObjectParser.from(prefix); expect(parser.currentArgs, `prefix=${prefix}`).toEqual(expected); expect(getPartialJsonObjectMeta(parser.currentArgs)).toEqual( getPartialJsonObjectMeta(expected), ); } }, ); it.each([ '{"value":01,"tail":1}', '{"value":1,"tail":\f2}', '\f{"value":1}', '{"value":"\\uZZ","tail":1}', '{"__proto__":{"polluted":true},"tail":1}', '{"constructor":{"prototype":{"polluted":true}},"tail":1}', ])( "retains fallback and metadata for malformed or unsafe prefixes", (input) => { expectPrefixParity(input); }, ); it("keeps parser branches independent", () => { const base = IncrementalJsonObjectParser.from('{"value":"'); const left = base.append('left"}'); const right = base.append('right"}'); const nestedBase = IncrementalJsonObjectParser.from('{"values":[1'); const nestedLeft = nestedBase.append(",2]}"); const nestedRight = nestedBase.append(",3]}"); expect(base.currentArgs).toMatchObject({ value: "" }); expect(left.currentArgs).toMatchObject({ value: "left" }); expect(right.currentArgs).toMatchObject({ value: "right" }); expect(nestedBase.currentArgs).toMatchObject({ values: [1] }); expect(nestedLeft.currentArgs).toMatchObject({ values: [1, 2] }); expect(nestedRight.currentArgs).toMatchObject({ values: [1, 3] }); }); it("keeps dense arrays immutable across deltas", () => { let parser = IncrementalJsonObjectParser.from('{"values":[0'); const early = parser; for (let index = 1; index < 2_000; index++) { parser = parser.append(`,${index}`); } const middle = parser; for (let index = 2_000; index < 4_000; index++) { parser = parser.append(`,${index}`); } parser = parser.append("]}"); expect(early.currentArgs.values).toEqual([0]); expect(middle.currentArgs.values).toHaveLength(2_000); expect((middle.currentArgs.values as readonly number[]).at(-1)).toBe(1_999); expect(parser.currentArgs.values).toHaveLength(4_000); expect((parser.currentArgs.values as readonly number[]).at(-1)).toBe(3_999); expect(JSON.stringify(parser.currentArgs)).toBe( JSON.stringify({ values: Array.from({ length: 4_000 }, (_, index) => index), }), ); }); it("keeps dense objects immutable across deltas", () => { let parser = IncrementalJsonObjectParser.from('{"values":{"key0":0'); const early = parser; for (let index = 1; index < 1_000; index++) { parser = parser.append(`,"key${index}":${index}`); } const middle = parser; for (let index = 1_000; index < 2_000; index++) { parser = parser.append(`,"key${index}":${index}`); } parser = parser.append("}}"); expect(early.currentArgs.values).toEqual({ key0: 0 }); expect(Object.keys(middle.currentArgs.values as object)).toHaveLength( 1_000, ); expect( (middle.currentArgs.values as Readonly<Record<string, number>>).key999, ).toBe(999); expect(Object.keys(parser.currentArgs.values as object)).toHaveLength( 2_000, ); expect( (parser.currentArgs.values as Readonly<Record<string, number>>).key1999, ).toBe(1_999); }); it("preserves ordinary object and array behavior", () => { const parser = IncrementalJsonObjectParser.from( '{"values":[1,{"nested":true}]}', ); const values = parser.currentArgs.values as readonly ReadonlyJSONValue[]; const copy = { ...parser.currentArgs }; const mutableArgs = parser.currentArgs as Record<string, unknown>; expect(Array.isArray(values)).toBe(true); expect(Object.keys(parser.currentArgs)).toEqual(["values"]); expect(copy.values).toEqual([1, { nested: true }]); expect(getPartialJsonObjectMeta(copy)).toEqual( getPartialJsonObjectMeta(parser.currentArgs), ); expect(JSON.stringify(parser.currentArgs)).toBe( '{"values":[1,{"nested":true}]}', ); expect(structuredClone(parser.currentArgs)).toEqual({ values: [1, { nested: true }], }); expect(inspect(parser.currentArgs)).toContain("nested: true"); mutableArgs.extra = "visible"; expect(mutableArgs.extra).toBe("visible"); }); it("isolates complete initial arguments from caller mutations", () => { const parser = IncrementalJsonObjectParser.from( ' {"values":[1,{"nested":true}],"label":"original"} \n', ); const args = parser.currentArgs as Record<string, unknown>; args.label = "changed"; (args.values as unknown[]).push(2); expect(args.values).toEqual([1, { nested: true }, 2]); expect(parser.append(" ").currentArgs).toMatchObject({ values: [1, { nested: true }], label: "original", }); }); it("prevents nested snapshot mutations from changing later snapshots", () => { const parser = IncrementalJsonObjectParser.from( '{"values":[1,{"nested":true}', ); const values = parser.currentArgs.values as ReadonlyJSONValue[]; const nested = values[1] as Record<string, ReadonlyJSONValue>; values.push(2); nested.nested = false; expect(values).toEqual([1, { nested: false }, 2]); const complete = parser.append("]}"); expect(complete.currentArgs).toMatchObject({ values: [1, { nested: true }], }); }); it("keeps nested references stable when a fallback publishes before its parent", () => { const parent = IncrementalJsonObjectParser.from('{"values":[1],"text":"hi'); const child = parent.append("\\uZZ"); const args = child.currentArgs; const values = args.values as number[]; values.push(2); expect(parent.currentArgs).toBe(args); expect(parent.currentArgs.values).toBe(values); expect(child.currentArgs.values).toBe(values); }); it("preserves integer-like object keys and their ordinary key order", () => { const parser = IncrementalJsonObjectParser.from( '{"2":2,"1":1,"nested":{"10":"ten","0":"zero"}}', ); const nested = parser.currentArgs.nested as ReadonlyJSONObject; expect(parser.currentArgs["1"]).toBe(1); expect(parser.currentArgs["2"]).toBe(2); expect(Object.keys(parser.currentArgs)).toEqual(["1", "2", "nested"]); expect(Object.keys(nested)).toEqual(["0", "10"]); expect(JSON.stringify(parser.currentArgs)).toBe( '{"1":1,"2":2,"nested":{"0":"zero","10":"ten"}}', ); }); it("parses a large array delivered in one delta", () => { const values = Array.from({ length: 10_000 }, (_, index) => index + 0.5); const parser = IncrementalJsonObjectParser.from(JSON.stringify({ values })); expect(parser.currentArgs.values).toEqual(values); }); });