import type { StreamFn } from "@mariozechner/pi-agent-core"; import { describe, expect, it } from "vitest"; import { createHtmlEntityToolCallArgumentDecodingWrapper, decodeHtmlEntitiesInObject, } from "./provider-stream-shared.js"; type FakeWrappedStream = { result: () => Promise; [Symbol.asyncIterator]: () => AsyncIterator; }; function createFakeStream(params: { events: unknown[]; resultMessage: unknown; }): FakeWrappedStream { return { async result() { return params.resultMessage; }, [Symbol.asyncIterator]() { return (async function* () { for (const event of params.events) { yield event; } })(); }, }; } describe("decodeHtmlEntitiesInObject", () => { it("recursively decodes string values", () => { expect( decodeHtmlEntitiesInObject({ command: "cd ~/dev && echo "ok"", args: ["<input>", "'quoted'"], }), ).toEqual({ command: 'cd ~/dev && echo "ok"', args: ["", "'quoted'"], }); }); }); describe("createHtmlEntityToolCallArgumentDecodingWrapper", () => { it("decodes tool call arguments in final and streaming messages", async () => { const resultMessage = { content: [ { type: "toolCall", arguments: { command: "echo "result" && true" }, }, ], }; const streamEvent = { partial: { content: [ { type: "toolCall", arguments: { path: "<stream>", nested: { quote: "'x'" } }, }, ], }, }; const baseStreamFn: StreamFn = () => createFakeStream({ events: [streamEvent], resultMessage }) as never; const stream = createHtmlEntityToolCallArgumentDecodingWrapper(baseStreamFn)( {} as never, {} as never, {}, ) as FakeWrappedStream; await expect(stream.result()).resolves.toEqual({ content: [ { type: "toolCall", arguments: { command: 'echo "result" && true' }, }, ], }); const iterator = stream[Symbol.asyncIterator](); await expect(iterator.next()).resolves.toEqual({ done: false, value: { partial: { content: [ { type: "toolCall", arguments: { path: "", nested: { quote: "'x'" } }, }, ], }, }, }); }); });