142 lines
4.2 KiB
TypeScript
142 lines
4.2 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const { createSseHeartbeatTransform, HEARTBEAT_SHAPES, shapeForClientFormat } =
|
|
await import("../../open-sse/utils/sseHeartbeat.ts");
|
|
|
|
const STREAM_TS_STRIP_RE = /^event:\s*keepalive\b/i;
|
|
|
|
function decodeChunk(value) {
|
|
return typeof value === "string" ? value : new TextDecoder().decode(value);
|
|
}
|
|
|
|
async function readWithTimeout(reader, timeoutMs = 250) {
|
|
let timeout;
|
|
try {
|
|
return await Promise.race([
|
|
reader.read(),
|
|
new Promise((_, reject) => {
|
|
timeout = setTimeout(
|
|
() => reject(new Error("Timed out waiting for SSE heartbeat")),
|
|
timeoutMs
|
|
);
|
|
}),
|
|
]);
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
}
|
|
}
|
|
|
|
test("integration: anthropic-ping heartbeat reaches downstream and does NOT trigger stream.ts strip", async () => {
|
|
// Build a fake upstream that emits one chunk then idles indefinitely
|
|
let cancelled = false;
|
|
const upstream = new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(new TextEncoder().encode("event: message_start\ndata: {}\n\n"));
|
|
// never close — let heartbeat fire
|
|
},
|
|
cancel() {
|
|
cancelled = true;
|
|
},
|
|
});
|
|
|
|
const transform = createSseHeartbeatTransform({
|
|
intervalMs: 20,
|
|
shape: HEARTBEAT_SHAPES.ANTHROPIC_PING,
|
|
});
|
|
|
|
const piped = upstream.pipeThrough(transform);
|
|
const reader = piped.getReader();
|
|
|
|
// Read first (real) chunk
|
|
const { value: first } = await reader.read();
|
|
assert.match(decodeChunk(first), /event: message_start/);
|
|
|
|
// Wait for at least one heartbeat (interval = 20ms, give it 60ms slack)
|
|
const startedAt = Date.now();
|
|
let sawPing = false;
|
|
while (Date.now() - startedAt < 200) {
|
|
const { value, done } = await readWithTimeout(reader);
|
|
if (done) break;
|
|
const chunk = decodeChunk(value);
|
|
if (/^event: ping\b/m.test(chunk)) {
|
|
sawPing = true;
|
|
// Verify it does NOT match the strip regex
|
|
for (const line of chunk.split("\n")) {
|
|
assert.ok(
|
|
!STREAM_TS_STRIP_RE.test(line.trim()),
|
|
`heartbeat chunk produced a stream.ts-strippable line: ${line}`
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
assert.equal(sawPing, true);
|
|
|
|
await reader.cancel();
|
|
});
|
|
|
|
test("integration: openai-chunk heartbeat is valid JSON parseable by SDKs", async () => {
|
|
const upstream = new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(
|
|
new TextEncoder().encode(
|
|
`data: {"id":"chatcmpl-1","object":"chat.completion.chunk","choices":[]}\n\n`
|
|
)
|
|
);
|
|
},
|
|
});
|
|
|
|
const transform = createSseHeartbeatTransform({
|
|
intervalMs: 20,
|
|
shape: HEARTBEAT_SHAPES.OPENAI_CHUNK,
|
|
});
|
|
|
|
const piped = upstream.pipeThrough(transform);
|
|
const reader = piped.getReader();
|
|
|
|
await reader.read(); // skip first real chunk
|
|
|
|
const startedAt = Date.now();
|
|
let sawValidChunk = false;
|
|
while (Date.now() - startedAt < 200) {
|
|
const { value, done } = await readWithTimeout(reader);
|
|
if (done) break;
|
|
const chunk = decodeChunk(value);
|
|
if (chunk.startsWith("data: ") && chunk.includes("omniroute-keepalive")) {
|
|
const jsonStr = chunk.slice(6, chunk.indexOf("\n\n"));
|
|
const parsed = JSON.parse(jsonStr); // must not throw
|
|
assert.equal(parsed.object, "chat.completion.chunk");
|
|
assert.equal(parsed.choices[0].finish_reason, null);
|
|
sawValidChunk = true;
|
|
break;
|
|
}
|
|
}
|
|
assert.equal(sawValidChunk, true);
|
|
|
|
await reader.cancel();
|
|
});
|
|
|
|
test("integration: shapeForClientFormat + createSseHeartbeatTransform pipeline (claude path)", async () => {
|
|
// Simulates what chatCore.ts does at line 4276
|
|
const shape = shapeForClientFormat("claude");
|
|
assert.equal(shape, HEARTBEAT_SHAPES.ANTHROPIC_PING);
|
|
|
|
const transform = createSseHeartbeatTransform({
|
|
intervalMs: 20,
|
|
shape,
|
|
});
|
|
|
|
const upstream = new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(new TextEncoder().encode("event: hello\ndata: {}\n\n"));
|
|
},
|
|
});
|
|
|
|
const reader = upstream.pipeThrough(transform).getReader();
|
|
await reader.read(); // first real
|
|
const { value } = await readWithTimeout(reader);
|
|
assert.match(decodeChunk(value), /^event: ping\ndata: \{\}\n\n$/);
|
|
await reader.cancel();
|
|
});
|