Files
2026-07-13 13:39:12 +08:00

277 lines
10 KiB
TypeScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { STREAM_RECOVERY } from "../../open-sse/config/constants.ts";
import {
HoldbackBuffer,
TruncatedStreamError,
isRetryableStreamError,
hasTerminalMarker,
createRecoverableStream,
} from "../../open-sse/services/streamRecovery.ts";
const enc = (s: string) => new TextEncoder().encode(s);
/** Build a mock upstream stream that emits `chunks` then ends via close or error. */
function makeStream(chunks: string[], end: "close" | Error): ReadableStream<Uint8Array> {
let i = 0;
return new ReadableStream<Uint8Array>({
pull(controller) {
if (i < chunks.length) {
controller.enqueue(enc(chunks[i++]));
return;
}
if (end instanceof Error) controller.error(end);
else controller.close();
},
});
}
async function readAll(
rs: ReadableStream<Uint8Array>
): Promise<{ text: string; errored: Error | null }> {
const reader = rs.getReader();
const dec = new TextDecoder();
let text = "";
try {
for (;;) {
const { done, value } = await reader.read();
if (done) break;
if (value) text += dec.decode(value, { stream: true });
}
return { text, errored: null };
} catch (e) {
return { text, errored: e as Error };
}
}
const econnreset = () => Object.assign(new Error("socket reset"), { code: "ECONNRESET" });
test("STREAM_RECOVERY constants mirror the free-claude-code values", () => {
assert.equal(STREAM_RECOVERY.HOLDBACK_MS, 750);
assert.equal(STREAM_RECOVERY.BUFFER_MAX_BYTES, 65536);
assert.equal(STREAM_RECOVERY.EARLY_RETRY_MAX, 4);
});
test("HoldbackBuffer holds chunks until flushed, then commits", () => {
// Frozen clock so neither the time nor the byte threshold trips.
const hb = new HoldbackBuffer({ now: () => 1000 });
assert.deepEqual(hb.push(enc("data: a\n\n")), []);
assert.deepEqual(hb.push(enc("data: b\n\n")), []);
assert.equal(hb.committed, false);
assert.equal(hb.hasBuffered, true);
const flushed = hb.flush();
assert.equal(Buffer.concat(flushed).toString("utf8"), "data: a\n\ndata: b\n\n");
assert.equal(hb.committed, true);
assert.equal(hb.hasBuffered, false);
});
test("HoldbackBuffer auto-commits once buffered bytes exceed BUFFER_MAX_BYTES", () => {
const hb = new HoldbackBuffer({ now: () => 0 });
const emitted = hb.push(enc("x".repeat(STREAM_RECOVERY.BUFFER_MAX_BYTES + 1)));
assert.equal(hb.committed, true, "should commit once buffer exceeds max bytes");
assert.equal(emitted.length, 1, "the over-cap chunk flushes immediately");
});
test("HoldbackBuffer auto-commits once the holdback window elapses", () => {
let clock = 0;
const hb = new HoldbackBuffer({ now: () => clock });
assert.deepEqual(hb.push(enc("data: first\n\n")), [], "first chunk still held");
clock = STREAM_RECOVERY.HOLDBACK_MS; // window elapsed
const emitted = hb.push(enc("data: second\n\n"));
assert.equal(hb.committed, true, "should commit once the holdback window elapses");
assert.equal(
Buffer.concat(emitted).toString("utf8"),
"data: first\n\ndata: second\n\n",
"flush releases everything buffered so far"
);
});
test("HoldbackBuffer passes chunks straight through once committed", () => {
const hb = new HoldbackBuffer({ now: () => 0 });
hb.flush(); // commit with nothing buffered
const out = hb.push(enc("data: post\n\n"));
assert.equal(Buffer.concat(out).toString("utf8"), "data: post\n\n");
});
test("HoldbackBuffer.discard drops buffered chunks without committing", () => {
const hb = new HoldbackBuffer({ now: () => 0 });
hb.push(enc("data: a\n\n"));
hb.discard();
assert.equal(hb.committed, false, "discard must not commit — a retry is still possible");
assert.equal(hb.hasBuffered, false);
});
test("TruncatedStreamError is a named Error", () => {
const err = new TruncatedStreamError("stream ended without terminal marker");
assert.ok(err instanceof Error);
assert.equal(err.name, "TruncatedStreamError");
assert.match(err.message, /terminal marker/);
});
test("isRetryableStreamError: truncation and transient transport errors are retryable", () => {
assert.equal(isRetryableStreamError(new TruncatedStreamError("x")), true);
for (const code of ["ECONNRESET", "ECONNREFUSED", "ETIMEDOUT", "EPIPE", "UND_ERR_SOCKET"]) {
const err = Object.assign(new Error("socket"), { code });
assert.equal(isRetryableStreamError(err), true, `${code} should be retryable`);
}
const timeout = Object.assign(new Error("body timeout"), { name: "BodyTimeoutError" });
assert.equal(isRetryableStreamError(timeout), true);
const terminated = new Error("terminated");
assert.equal(isRetryableStreamError(terminated), true);
});
test("isRetryableStreamError: client aborts and unknown errors are NOT retryable", () => {
const abort = Object.assign(new Error("aborted"), { name: "AbortError" });
assert.equal(isRetryableStreamError(abort), false, "client cancellation must not be retried");
assert.equal(isRetryableStreamError(new Error("some unrelated failure")), false);
assert.equal(isRetryableStreamError(null), false);
assert.equal(isRetryableStreamError("nope"), false);
});
test("hasTerminalMarker detects OpenAI and Anthropic stream terminators", () => {
assert.equal(hasTerminalMarker(enc("data: {...}\n\ndata: [DONE]\n\n")), true);
assert.equal(hasTerminalMarker(enc("event: message_stop\ndata: {}\n\n")), true);
assert.equal(hasTerminalMarker(enc("data: {\"choices\":[]}\n\n")), false);
assert.equal(hasTerminalMarker(enc("")), false);
});
test("createRecoverableStream retries an early truncation transparently", async () => {
let finalizeCount = 0;
let reopened = 0;
// Attempt 1 emits 2 held chunks then the socket resets before any commit.
const attempt1 = makeStream(["data: a\n\n", "data: b\n\n"], econnreset());
const attempt2 = makeStream(["data: x\n\n", "data: [DONE]\n\n"], "close");
const rs = createRecoverableStream(
attempt1,
async () => {
reopened++;
return attempt2;
},
{ finalize: () => finalizeCount++, now: () => 0 } // frozen clock: no time-based commit
);
const { text, errored } = await readAll(rs);
assert.equal(errored, null);
assert.equal(reopened, 1, "should re-open exactly once");
assert.equal(text, "data: x\n\ndata: [DONE]\n\n", "client sees ONLY the recovered attempt");
assert.equal(finalizeCount, 1, "finalize runs exactly once");
});
test("createRecoverableStream emits the held partial and closes when reopen fails", async () => {
let finalizeCount = 0;
// Graceful end with NO terminal marker = silent truncation; reopen can't recover.
const attempt1 = makeStream(["data: partial\n\n"], "close");
const rs = createRecoverableStream(attempt1, async () => null, {
finalize: () => finalizeCount++,
now: () => 0,
});
const { text, errored } = await readAll(rs);
assert.equal(errored, null);
assert.equal(text, "data: partial\n\n", "best-effort: held bytes are still delivered");
assert.equal(finalizeCount, 1);
});
test("createRecoverableStream propagates errors once committed (never replays)", async () => {
let reopened = 0;
let finalizeCount = 0;
const big = "x".repeat(STREAM_RECOVERY.BUFFER_MAX_BYTES + 10); // forces immediate commit
const attempt1 = makeStream([big, "data: more\n\n"], econnreset());
const rs = createRecoverableStream(
attempt1,
async () => {
reopened++;
return makeStream(["data: nope\n\n"], "close");
},
{ finalize: () => finalizeCount++, now: () => 0 }
);
const { errored } = await readAll(rs);
assert.ok(errored, "a post-commit failure must surface to the client");
assert.equal(reopened, 0, "must NOT re-open after the client has already seen bytes");
assert.equal(finalizeCount, 1);
});
test("createRecoverableStream passes a clean short stream straight through", async () => {
let reopened = 0;
let finalizeCount = 0;
const attempt1 = makeStream(["data: hi\n\n", "data: [DONE]\n\n"], "close");
const rs = createRecoverableStream(
attempt1,
async () => {
reopened++;
return null;
},
{ finalize: () => finalizeCount++, now: () => 0 }
);
const { text, errored } = await readAll(rs);
assert.equal(errored, null);
assert.equal(reopened, 0, "a clean stream must not trigger recovery");
assert.equal(text, "data: hi\n\ndata: [DONE]\n\n");
assert.equal(finalizeCount, 1);
});
test("createRecoverableStream stops after maxEarlyRetries", async () => {
let reopened = 0;
let finalizeCount = 0;
const attempt1 = makeStream(["data: t\n\n"], "close"); // truncated (no terminal marker)
const rs = createRecoverableStream(
attempt1,
async () => {
reopened++;
return makeStream(["data: t\n\n"], "close"); // every retry truncates too
},
{ finalize: () => finalizeCount++, now: () => 0, maxEarlyRetries: 2 }
);
const { errored } = await readAll(rs);
assert.equal(errored, null);
assert.equal(reopened, 2, "should re-open exactly maxEarlyRetries times, then give up");
assert.equal(finalizeCount, 1);
});
test("createRecoverableStream finalizes on client cancel", async () => {
let finalizeCount = 0;
const attempt1 = makeStream(["data: a\n\n"], "close");
const rs = createRecoverableStream(attempt1, async () => null, {
finalize: () => finalizeCount++,
now: () => 0,
});
const reader = rs.getReader();
await reader.cancel("client gone");
assert.equal(finalizeCount, 1);
});
test("createRecoverableStream does not spend an upstream re-open after a client cancel", async () => {
let reopened = 0;
let finalizeCount = 0;
const attempt1 = makeStream(["data: a\n\n"], "close");
const rs = createRecoverableStream(
attempt1,
async () => {
reopened++;
return makeStream(["data: t\n\n"], "close");
},
{ finalize: () => finalizeCount++, now: () => 0 }
);
const reader = rs.getReader();
await reader.cancel("client gone");
await new Promise((r) => setTimeout(r, 10)); // let any in-flight pull settle
assert.equal(reopened, 0, "a client cancel must never trigger a transparent re-open");
assert.equal(finalizeCount, 1);
});