Files
9router/tests/unit/commandcode-executor.test.js
luulam e438a03f96 feat(open-sse): early-peek stream error detection + fix UTF-8 loss in CommandCode peek
- new open-sse/utils/streamErrorPeek.js: bounded peek of the first bytes of a
  200 stream; configured pattern match → 502 so account/combo fallback can run
  before any byte reaches the client (streaming included). Re-emits RAW bytes
  so split multi-byte UTF-8 sequences survive the peek (never re-encode
  decoded text — TextDecoder flush corrupts a lone leading byte to U+FFFD).
- chatCore: run the peek after executor.execute when the provider has
  streamErrorPatterns configured; chat.js passes the settings through.
- commandcode executor: same raw-bytes fix in peekForUpstreamError + regression
  test that fails against the old flush-based re-encode.
2026-08-04 23:33:12 +07:00

124 lines
4.2 KiB
JavaScript

/**
* Unit tests for the CommandCode executor early-error peek.
*
* The upstream emits AI SDK v5 NDJSON over an HTTP 200 stream, so a terminal
* `{"type":"error"}` event is invisible to the normal `response.ok` success
* check. `peekForUpstreamError` reads the first events before committing the
* response: an error event → non-ok Response (fallback can kick in); otherwise
* the buffered bytes are re-emitted and streaming proceeds as before.
*/
import { describe, it, expect } from "vitest";
import { peekForUpstreamError } from "../../open-sse/executors/commandcode.js";
const encoder = new TextEncoder();
function ndjsonResponse(lines) {
const body = new ReadableStream({
start(controller) {
for (const line of lines) controller.enqueue(encoder.encode(line + "\n"));
controller.close();
},
});
return new Response(body, {
status: 200,
headers: { "content-type": "text/event-stream" },
});
}
describe("commandcode executor — early-error peek", () => {
it("returns 502 when the first meaningful event is an error", async () => {
const res = await peekForUpstreamError(
ndjsonResponse([
'{"type":"error","error":{"type":"server_error","message":"Network connection lost."}}',
]),
"model",
);
expect(res.status).toBe(502);
const body = await res.json();
expect(body.error.message).toBe("Network connection lost.");
expect(body.error.type).toBe("server_error");
});
it("detects an error event even when metadata events arrive first", async () => {
const res = await peekForUpstreamError(
ndjsonResponse([
'{"type":"start"}',
'{"type":"start-step"}',
'{"type":"error","error":{"type":"server_error","message":"Network connection lost."}}',
]),
"model",
);
expect(res.status).toBe(502);
const body = await res.json();
expect(body.error.message).toContain("Network connection lost");
});
it("commits and streams normally when the first meaningful event is content", async () => {
const res = await peekForUpstreamError(
ndjsonResponse([
'{"type":"start"}',
'{"type":"text-delta","text":"hi there"}',
'{"type":"finish"}',
]),
"model",
);
expect(res.status).toBe(200);
const text = await res.text();
expect(text).toContain('"content":"hi there"');
expect(text).not.toContain("[CommandCode error:");
});
it("commits when the stream ends without any event", async () => {
const res = await peekForUpstreamError(ndjsonResponse([]), "model");
expect(res.status).toBe(200);
await res.body.cancel();
});
it("commits (does not hang) when no event arrives before the peek timeout", async () => {
const stalled = new Response(new ReadableStream({ start() {} }), {
status: 200,
headers: { "content-type": "text/event-stream" },
});
const res = await peekForUpstreamError(stalled, "model", { timeoutMs: 50 });
expect(res.status).toBe(200);
await res.body.cancel();
});
it("does not hang when the request signal aborts during the peek", async () => {
const controller = new AbortController();
const stalled = new Response(new ReadableStream({ start() {} }), {
status: 200,
headers: { "content-type": "text/event-stream" },
});
setTimeout(() => controller.abort(new Error("client gone")), 10);
const res = await peekForUpstreamError(stalled, "model", {
signal: controller.signal,
timeoutMs: 2000,
});
expect(res.status).toBe(200);
await res.body.cancel();
});
it("re-emits raw bytes so a multi-byte char split across the peek boundary survives", async () => {
// chunk1 ends mid-é (0xC3); the peek commits on the first complete line
// (text-delta "hi") while the decoder still holds 0xC3. Re-emission must
// use RAW bytes — re-encoding decoded text would replace 0xC3 with U+FFFD.
const first = Buffer.from(
'{"type":"text-delta","text":"hi"}\n{"type":"text-delta","text":"caf',
);
const chunk1 = new Uint8Array([...first, 0xc3]);
const rest = new Uint8Array([0xa9, 0x22, 0x7d, 0x0a]); // é"}\n
const body = new ReadableStream({
start(c) {
c.enqueue(chunk1);
c.enqueue(rest);
c.close();
},
});
const res = await peekForUpstreamError(new Response(body, { status: 200 }), "m");
const text = await res.text();
expect(text).toContain("café");
expect(text).not.toContain("\uFFFD");
});
});