268 lines
8.4 KiB
TypeScript
268 lines
8.4 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { createSseTextTransform } from "../../src/lib/sseTextTransform.ts";
|
|
import type { FieldCategory } from "../../src/lib/sseTextTransform.ts";
|
|
|
|
async function testTransform(transform: TransformStream, inputChunks: string[]): Promise<string> {
|
|
const writer = transform.writable.getWriter();
|
|
const reader = transform.readable.getReader();
|
|
|
|
const writePromise = (async () => {
|
|
for (const chunk of inputChunks) {
|
|
await writer.write(new TextEncoder().encode(chunk));
|
|
}
|
|
await writer.close();
|
|
})();
|
|
|
|
const outputChunks: string[] = [];
|
|
let res = await reader.read();
|
|
while (!res.done) {
|
|
outputChunks.push(new TextDecoder().decode(res.value));
|
|
res = await reader.read();
|
|
}
|
|
|
|
await writePromise;
|
|
return outputChunks.join("");
|
|
}
|
|
|
|
test("processor receives delta.content from OpenAI CC format", async () => {
|
|
const received: string[] = [];
|
|
const transform = createSseTextTransform((text, field) => {
|
|
received.push(text);
|
|
return text.toUpperCase();
|
|
});
|
|
|
|
const output = await testTransform(transform, [
|
|
`data: {"choices":[{"delta":{"content":"hello"}}]}\n\n`,
|
|
]);
|
|
|
|
assert.equal(received.length, 1);
|
|
assert.equal(received[0], "hello");
|
|
assert.ok(output.includes("HELLO"), "output should contain uppercased text");
|
|
});
|
|
|
|
test("processor receives 'content' field category for delta.content", async () => {
|
|
const fields: FieldCategory[] = [];
|
|
const transform = createSseTextTransform((text, field) => {
|
|
fields.push(field);
|
|
return text;
|
|
});
|
|
|
|
await testTransform(transform, [`data: {"choices":[{"delta":{"content":"hi"}}]}\n\n`]);
|
|
|
|
assert.equal(fields[0], "content");
|
|
});
|
|
|
|
test("processor does not mutate tool_calls function.arguments", async () => {
|
|
const received: string[] = [];
|
|
const transform = createSseTextTransform((text, field) => {
|
|
received.push(`${field}:${text}`);
|
|
return "MUTATED";
|
|
});
|
|
const payload = {
|
|
choices: [
|
|
{
|
|
delta: {
|
|
tool_calls: [
|
|
{
|
|
function: {
|
|
arguments: JSON.stringify({ command: "find /tmp -name test.txt" }),
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const output = await testTransform(transform, [`data: ${JSON.stringify(payload)}\n\n`]);
|
|
|
|
assert.equal(received.length, 0, "tool call arguments must bypass text processors");
|
|
assert.ok(output.includes("find /tmp -name test.txt"), "arguments should pass through unchanged");
|
|
assert.ok(!output.includes("MUTATED"), "processor output must not replace tool JSON");
|
|
});
|
|
|
|
test("processor receives delta.reasoning_content with 'reasoning' category", async () => {
|
|
const fields: FieldCategory[] = [];
|
|
const transform = createSseTextTransform((text, field) => {
|
|
fields.push(field);
|
|
return text;
|
|
});
|
|
|
|
await testTransform(transform, [
|
|
`data: {"choices":[{"delta":{"reasoning_content":"thinking..."}}]}\n\n`,
|
|
]);
|
|
|
|
assert.ok(fields.includes("reasoning"));
|
|
});
|
|
|
|
test("[DONE] sentinel passes through unchanged", async () => {
|
|
const received: string[] = [];
|
|
const transform = createSseTextTransform((text) => {
|
|
received.push(text);
|
|
return text;
|
|
});
|
|
|
|
const output = await testTransform(transform, [`data: [DONE]\n\n`]);
|
|
|
|
assert.equal(received.length, 0, "processor should NOT be called for [DONE]");
|
|
assert.ok(output.includes("[DONE]"), "output should contain [DONE]");
|
|
});
|
|
|
|
test("SSE comments (: prefix) pass through unchanged", async () => {
|
|
const received: string[] = [];
|
|
const transform = createSseTextTransform((text) => {
|
|
received.push(text);
|
|
return text;
|
|
});
|
|
|
|
await testTransform(transform, [`: this is a comment\n\n`]);
|
|
|
|
assert.equal(received.length, 0, "processor should NOT be called for comments");
|
|
});
|
|
|
|
test("handles data: line split across two chunks", async () => {
|
|
const received: string[] = [];
|
|
const transform = createSseTextTransform((text) => {
|
|
received.push(text);
|
|
return text;
|
|
});
|
|
|
|
await testTransform(transform, [`data: {"choices":[{"del`, `ta":{"content":"split"}}]}\n\n`]);
|
|
|
|
assert.equal(received.length, 1);
|
|
assert.equal(received[0], "split");
|
|
});
|
|
|
|
test("processor receives Claude delta.text with 'content' category", async () => {
|
|
const received: Array<{ text: string; field: FieldCategory }> = [];
|
|
const transform = createSseTextTransform((text, field) => {
|
|
received.push({ text, field });
|
|
return text;
|
|
});
|
|
|
|
await testTransform(transform, [
|
|
`data: {"type":"content_block_delta","delta":{"type":"text_delta","text":"hello claude"}}\n\n`,
|
|
]);
|
|
|
|
assert.ok(received.some((r) => r.text === "hello claude" && r.field === "content"));
|
|
});
|
|
|
|
test("onFlush callback invoked at stream close", async () => {
|
|
let flushCalled = false;
|
|
const transform = createSseTextTransform(
|
|
(text) => text,
|
|
() => {
|
|
flushCalled = true;
|
|
return "";
|
|
}
|
|
);
|
|
|
|
await testTransform(transform, [`data: {"choices":[{"delta":{"content":"x"}}]}\n\n`]);
|
|
|
|
assert.ok(flushCalled, "onFlush should be called when stream closes");
|
|
});
|
|
|
|
test("malformed JSON data line doesn't crash", async () => {
|
|
const transform = createSseTextTransform((text) => text);
|
|
|
|
const output = await testTransform(transform, [`data: {not valid json\n\n`]);
|
|
|
|
assert.ok(output, "should emit something (passthrough on parse failure)");
|
|
});
|
|
|
|
test("unknown JSON format passes through without processing (no processDeep)", async () => {
|
|
const received: string[] = [];
|
|
const transform = createSseTextTransform((text) => {
|
|
received.push(text);
|
|
return text.toUpperCase();
|
|
});
|
|
|
|
const output = await testTransform(transform, [
|
|
`data: {"model":"gpt-4","id":"chatcmpl-123","object":"chat.completion.chunk"}\n\n`,
|
|
]);
|
|
|
|
assert.equal(received.length, 0, "processor should NOT be called for unrecognized format");
|
|
assert.ok(output.includes('"model":"gpt-4"'), "original data should pass through unchanged");
|
|
});
|
|
|
|
test("onFlush called exactly once when [DONE] is present", async () => {
|
|
let flushCount = 0;
|
|
const transform = createSseTextTransform(
|
|
(text) => text,
|
|
() => {
|
|
flushCount++;
|
|
return null;
|
|
}
|
|
);
|
|
|
|
await testTransform(transform, [
|
|
`data: {"choices":[{"delta":{"content":"hi"}}]}\n\n`,
|
|
`data: [DONE]\n\n`,
|
|
]);
|
|
|
|
assert.equal(flushCount, 1, "onFlush should be called exactly once");
|
|
});
|
|
|
|
test("Gemini candidates[0].content.parts[0].text is processed", async () => {
|
|
const received: Array<{ text: string; field: FieldCategory }> = [];
|
|
const transform = createSseTextTransform((text, field) => {
|
|
received.push({ text, field });
|
|
return text.toUpperCase();
|
|
});
|
|
|
|
const output = await testTransform(transform, [
|
|
`data: {"candidates":[{"content":{"parts":[{"text":"gemini response"}]}}]}\n\n`,
|
|
]);
|
|
|
|
assert.ok(received.some((r) => r.text === "gemini response" && r.field === "content"));
|
|
assert.ok(output.includes("GEMINI RESPONSE"));
|
|
});
|
|
|
|
test("Responses API string delta is processed", async () => {
|
|
const received: string[] = [];
|
|
const transform = createSseTextTransform((text) => {
|
|
received.push(text);
|
|
return text;
|
|
});
|
|
|
|
await testTransform(transform, [
|
|
`data: {"type":"response.output_text.delta","delta":"hello responses"}\n\n`,
|
|
]);
|
|
|
|
assert.ok(received.includes("hello responses"));
|
|
});
|
|
|
|
test("recursive scanning sanitizes multiple nested format fields (no format bypass)", async () => {
|
|
let callCount = 0;
|
|
const transform = createSseTextTransform((text) => {
|
|
callCount++;
|
|
return text;
|
|
});
|
|
|
|
// This JSON has both `choices` (OpenAI) AND top-level `content` (Generic)
|
|
await testTransform(transform, [
|
|
`data: {"choices":[{"delta":{"content":"hi"}}],"content":"generic"}\n\n`,
|
|
]);
|
|
|
|
// Should sanitize both fields recursively to prevent format-based bypasses
|
|
assert.equal(callCount, 2, "processor should be called for each string property recursively");
|
|
});
|
|
|
|
test("Responses API snapshot text is identified as snapshot and bypasses delta buffering", async () => {
|
|
let isSnapshotReceived = false;
|
|
const transform = createSseTextTransform((text, field, isStopSignal, index, isSnapshot) => {
|
|
if (isSnapshot) {
|
|
isSnapshotReceived = true;
|
|
}
|
|
return text.toUpperCase();
|
|
});
|
|
|
|
const output = await testTransform(transform, [
|
|
`data: {"type":"response.output_text.done","text":"hello snapshot"}\n\n`,
|
|
]);
|
|
|
|
assert.ok(isSnapshotReceived, "should identify done event as snapshot");
|
|
assert.ok(output.includes("HELLO SNAPSHOT"), "output should contain sanitized snapshot text");
|
|
});
|