assistant-stream
Version:
Streaming utilities for AI assistants
326 lines (311 loc) • 7.67 kB
text/typescript
import { describe, it, expect } from "vitest";
import {
parsePartialJsonObject,
getPartialJsonObjectFieldState,
getPartialJsonObjectMeta,
markPartialJsonObjectComplete,
} from "./parse-partial-json-object";
type PartialJsonTest = {
input: string;
query: (string | number)[];
result: "partial" | "complete" | "undefined";
};
const tests: PartialJsonTest[] = [
// empty query
{
input: ``,
query: [],
result: "partial",
},
{
input: `{`,
query: [],
result: "partial",
},
{
input: `{}`,
query: [],
result: "complete",
},
// field query (missing)
{
input: ``,
query: ["test"],
result: "partial",
},
{
input: `{`,
query: ["test"],
result: "partial",
},
{
input: `{}`,
query: ["test"],
result: "complete",
},
// field query (partial)
{
input: `{"foo": `,
query: ["foo"],
result: "partial",
},
{
input: `{"foo": "b`,
query: ["foo"],
result: "partial",
},
{
input: `{"foo": 123`,
query: ["foo"],
result: "partial",
},
{
input: `{"foo": {`,
query: ["foo"],
result: "partial",
},
{
input: `{"foo": [`,
query: ["foo"],
result: "partial",
},
// field query (complete)
{
input: `{"foo": 123,`,
query: ["foo"],
result: "complete",
},
{
input: `{"foo": "b"`,
query: ["foo"],
result: "complete",
},
{
input: `{"foo": nu`,
query: ["foo"],
result: "complete",
},
{
input: `{"foo": fa`,
query: ["foo"],
result: "complete",
},
{
input: `{"foo": tr`,
query: ["foo"],
result: "complete",
},
{
input: `{"foo": {}`,
query: ["foo"],
result: "complete",
},
{
input: `{"foo": []`,
query: ["foo"],
result: "complete",
},
// field query (nested)
{
input: `{"foo": [{ "bar": "abc`,
query: ["foo", "0", "bar"],
result: "partial",
},
{
input: `{"foo": [{ "bar": "abc"`,
query: ["foo", "0", "bar"],
result: "complete",
},
{
input: `{"foo": [{ "bar": 123`,
query: ["foo", "0", "bar"],
result: "partial",
},
{
input: `{"foo": [{ "bar": nu`,
query: ["foo", 0, "bar"],
result: "complete",
},
// field non-existent
{
input: `{"bar": "hello"`,
query: ["foo"],
result: "partial",
},
{
input: `{"bar": "hello"}`,
query: ["foo"],
result: "complete",
},
// mismatch type
{
input: `{"foo": 123`,
query: ["foo", "bar", "baz"],
result: "partial",
},
{
input: `{"foo": fa`,
query: ["foo", "bar", "baz"],
result: "complete",
},
// numeric keys
{
input: `{"1": "value","0":"`,
query: ["0"],
result: "partial",
},
{
input: `{"1": "value","0":"`,
query: ["1"],
result: "complete",
},
{
input: `{"foo": "value", "0": "`,
query: ["0"],
result: "partial",
},
{
input: `{"foo": "value", "0": "`,
query: ["foo"],
result: "complete",
},
// duplicate keys
{
input: `{"foo": "foo","bar":"bar","foo": "`,
query: ["foo"],
result: "partial",
},
{
input: `{"foo": "foo","bar":"bar","foo": "`,
query: ["bar"],
result: "complete",
},
{
input: `{"foo": [1,"a",{"b":1},[],{},[1,[[2]]],{"1":1,"t":1`,
query: ["foo", 6, 1],
result: "complete",
},
{
input: `{"foo": [1,"",{"b":1},[],{},[1,[[2]]],{"1":1,"t":1`,
query: ["foo", 6, "t"],
result: "partial",
},
{
input: `{"\\"": "t`,
query: ['"'],
result: "partial",
},
{
input: `{"\\"": "t"`,
query: ['"'],
result: "complete",
},
{
input: `{"\\u25CF": "t`,
query: ["\u25CF"],
result: "partial",
},
// negative number whose digits have not arrived yet
{
input: `{"foo": [-`,
query: ["foo"],
result: "partial",
},
{
input: `{"foo": [-`,
query: ["foo", 0],
result: "partial",
},
{
input: `{"foo": [[-`,
query: ["foo", 0, 0],
result: "partial",
},
// positive exponent whose digits have not arrived yet
{
input: `{"foo": 1e+`,
query: ["foo"],
result: "partial",
},
{
input: `{"foo": 1e+2`,
query: ["foo"],
result: "partial",
},
];
describe("parsePartialJsonObject and getPartialJsonObjectFieldState", () => {
// Test each case in the tests array
tests.forEach((testCase, index) => {
it(`Test case #${index + 1}: ${testCase.input || `""`} with query ${JSON.stringify(testCase.query)} should return "${testCase.result}"`, () => {
const args = parsePartialJsonObject(testCase.input);
if (args === undefined) throw new Error("unable to parse args");
const fieldState = getPartialJsonObjectFieldState(
args as Record<string, unknown>,
testCase.query,
);
expect(fieldState).toBe(testCase.result);
});
});
});
describe("parsePartialJsonObject inside a unicode escape", () => {
const full = `{"a":"x\\uD83D\\uDE00y"}`;
it("returns an object for every prefix of a string containing a surrogate-pair escape", () => {
for (let cut = 0; cut <= full.length; cut++) {
expect(
parsePartialJsonObject(full.slice(0, cut)),
`cut=${cut}`,
).toBeDefined();
}
});
it("omits the unfinished escape from the partial value", () => {
expect(parsePartialJsonObject(`{"a":"x\\u`)).toMatchObject({ a: "x" });
expect(parsePartialJsonObject(`{"a":"x\\uD8`)).toMatchObject({ a: "x" });
expect(parsePartialJsonObject(`{"a":"x\\uD83D\\uDE0`)).toMatchObject({
a: "x\uD83D",
});
expect(parsePartialJsonObject(`{"a":"x\\uD83D\\uDE00y`)).toMatchObject({
a: "x😀y",
});
});
it("keeps the partial path through an escape cut inside a value", () => {
const args = parsePartialJsonObject(`{"a":"x\\uD8`)!;
expect(getPartialJsonObjectFieldState(args, ["a"])).toBe("partial");
});
it("handles an escape cut inside an object key", () => {
expect(Object.keys(parsePartialJsonObject(`{"\\u00e9`)!)).toEqual([]);
expect(parsePartialJsonObject(`{"\\u00e9":1`)).toMatchObject({ é: 1 });
expect(parsePartialJsonObject(`{"\\u00e9":"v`)).toMatchObject({ é: "v" });
});
it("keeps malformed escapes unparseable", () => {
expect(parsePartialJsonObject(`{"a":"\\uZZ`)).toBeUndefined();
expect(parsePartialJsonObject(`{"a":"\\u"}`)).toBeUndefined();
expect(parsePartialJsonObject(`{"a":"\\u12"}`)).toBeUndefined();
});
it("leaves single-char escapes unchanged", () => {
expect(parsePartialJsonObject(`{"a":"x\\n`)).toMatchObject({ a: "x\n" });
expect(parsePartialJsonObject(`{"a":"x\\`)).toMatchObject({ a: "x" });
expect(parsePartialJsonObject(`{"a":"x\\"`)).toMatchObject({ a: 'x"' });
});
});
describe("markPartialJsonObjectComplete", () => {
it.each([
'{"__proto__":{"polluted":true}}',
'{"constructor":{"prototype":{"polluted":true}}}',
])(
"keeps prototype-named fields as own data without relaxing safe parsing: %s",
(json) => {
const input = JSON.parse(json);
expect(parsePartialJsonObject(json)).toBeUndefined();
const complete = markPartialJsonObjectComplete(input);
expect(complete).not.toBe(input);
expect(Object.entries(complete)).toEqual(Object.entries(input));
expect(Object.getPrototypeOf(complete)).toBe(Object.prototype);
expect(Object.prototype).not.toHaveProperty("polluted");
expect(getPartialJsonObjectMeta(complete)?.state).toBe("complete");
expect(
getPartialJsonObjectFieldState(complete, [Object.keys(input)[0]!]),
).toBe("complete");
expect(getPartialJsonObjectMeta(input)).toBeUndefined();
},
);
});