chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* Tests for open-sse/utils/diagnostics.ts
|
||||
*
|
||||
* Covers:
|
||||
* (a) synthOpenAIErrorChunk — shape validation
|
||||
* (b) synthResponsesFailure — matches a response.failed event
|
||||
* (c) detectMalformedNonStream — empty/malformed input classified correctly
|
||||
* (d) no stack trace leakage in error chunk message
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
reportMalformed200,
|
||||
synthOpenAIErrorChunk,
|
||||
synthResponsesFailure,
|
||||
detectMalformedNonStream,
|
||||
} from "../../open-sse/utils/diagnostics.ts";
|
||||
|
||||
// ── (a) synthOpenAIErrorChunk shape ──────────────────────────────────────────
|
||||
|
||||
test("synthOpenAIErrorChunk returns a valid SSE data line", () => {
|
||||
const line = synthOpenAIErrorChunk({
|
||||
provider: "testprovider",
|
||||
model: "gpt-test",
|
||||
reason: "empty_stream",
|
||||
});
|
||||
assert.match(line, /^data: /);
|
||||
assert.ok(line.endsWith("\n\n"), "must end with double newline");
|
||||
});
|
||||
|
||||
test("synthOpenAIErrorChunk payload has expected OpenAI chunk shape", () => {
|
||||
const line = synthOpenAIErrorChunk({ provider: "myprovider", model: "mymodel", reason: "empty" });
|
||||
const payload = JSON.parse(line.slice("data: ".length).trimEnd());
|
||||
assert.equal(payload.object, "chat.completion.chunk");
|
||||
assert.ok(Array.isArray(payload.choices), "must have choices array");
|
||||
assert.equal(payload.choices.length, 1);
|
||||
assert.ok(payload.error, "must have error field");
|
||||
assert.equal(payload.error.type, "upstream_empty_response");
|
||||
assert.ok(typeof payload.error.message === "string" && payload.error.message.length > 0);
|
||||
});
|
||||
|
||||
test("synthOpenAIErrorChunk references provider in message", () => {
|
||||
const line = synthOpenAIErrorChunk({
|
||||
provider: "mymysteriosprovider",
|
||||
model: "m",
|
||||
reason: "stall",
|
||||
});
|
||||
const payload = JSON.parse(line.slice("data: ".length).trimEnd());
|
||||
assert.ok(
|
||||
payload.error.message.includes("mymysteriosprovider"),
|
||||
`message should reference provider, got: ${payload.error.message}`
|
||||
);
|
||||
});
|
||||
|
||||
// ── (b) synthResponsesFailure matches a response.failed event ────────────────
|
||||
|
||||
test("synthResponsesFailure produces a response.failed SSE event", () => {
|
||||
const sseText = synthResponsesFailure("empty_stream");
|
||||
assert.match(sseText, /event: response\.failed/);
|
||||
assert.match(sseText, /data: /);
|
||||
});
|
||||
|
||||
test("synthResponsesFailure includes a reason in the data payload", () => {
|
||||
const sseText = synthResponsesFailure("no_terminal");
|
||||
// Extract JSON after "data: " line
|
||||
const dataLine = sseText.split("\n").find((l) => l.startsWith("data: "));
|
||||
assert.ok(dataLine, "must have a data line");
|
||||
const parsed = JSON.parse(dataLine.slice("data: ".length));
|
||||
assert.ok(
|
||||
parsed?.response?.error?.message?.length > 0,
|
||||
`response.error.message should be non-empty, got: ${JSON.stringify(parsed?.response?.error)}`
|
||||
);
|
||||
});
|
||||
|
||||
// ── (c) detectMalformedNonStream ─────────────────────────────────────────────
|
||||
|
||||
test("detectMalformedNonStream returns 'empty_choices' for null input", () => {
|
||||
assert.equal(detectMalformedNonStream(null), "empty_choices");
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns 'empty_choices' for empty object", () => {
|
||||
assert.equal(detectMalformedNonStream({}), "empty_choices");
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns 'empty_choices' for choices:[]", () => {
|
||||
assert.equal(detectMalformedNonStream({ choices: [] }), "empty_choices");
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns 'empty_choices' when choice message has no content", () => {
|
||||
const body = {
|
||||
choices: [{ message: { content: "", tool_calls: null }, finish_reason: "stop" }],
|
||||
};
|
||||
assert.equal(detectMalformedNonStream(body), "empty_choices");
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns null for valid chat completion", () => {
|
||||
const body = {
|
||||
choices: [{ message: { content: "Hello!", tool_calls: null }, finish_reason: "stop" }],
|
||||
};
|
||||
assert.equal(detectMalformedNonStream(body), null);
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns null for OpenAI choices whose content is a text-block array (#5559)", () => {
|
||||
// Cline (OAuth) returns choices[].message.content as an array of Anthropic-style
|
||||
// blocks inside an OpenAI envelope; this must count as real output, not empty_choices.
|
||||
const body = {
|
||||
id: "chatcmpl-kimi",
|
||||
object: "chat.completion",
|
||||
model: "moonshotai/kimi-k2.6",
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: { role: "assistant", content: [{ type: "text", text: "Here is my analysis." }] },
|
||||
finish_reason: "stop",
|
||||
},
|
||||
],
|
||||
};
|
||||
assert.equal(detectMalformedNonStream(body), null);
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns 'empty_choices' for an OpenAI choice with an empty text-block array (#5559 guard)", () => {
|
||||
const body = {
|
||||
choices: [{ message: { role: "assistant", content: [{ type: "text", text: "" }] }, finish_reason: "stop" }],
|
||||
};
|
||||
assert.equal(detectMalformedNonStream(body), "empty_choices");
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns null for a valid Claude-native message (#4942 regression)", () => {
|
||||
const body = {
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "Preserved" }],
|
||||
stop_reason: "end_turn",
|
||||
};
|
||||
assert.equal(detectMalformedNonStream(body), null);
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns 'empty_choices' for a Claude-native message with no text", () => {
|
||||
const body = { type: "message", role: "assistant", content: [{ type: "text", text: "" }] };
|
||||
assert.equal(detectMalformedNonStream(body), "empty_choices");
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns null for a Claude-native message carrying a tool_use block", () => {
|
||||
const body = {
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "tu_1", name: "search", input: {} }],
|
||||
};
|
||||
assert.equal(detectMalformedNonStream(body), null);
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream tolerates a null block in a Claude-native content array", () => {
|
||||
// A malformed/partial provider response could carry a null entry in `content`.
|
||||
// The detector must not throw (TypeError on null.type) — it skips the null
|
||||
// block and classifies by the remaining valid blocks.
|
||||
const body = {
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [null, { type: "text", text: "still here" }],
|
||||
};
|
||||
assert.equal(detectMalformedNonStream(body), null);
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns 'empty_choices' for a Claude-native message of only null blocks", () => {
|
||||
const body = { type: "message", role: "assistant", content: [null] };
|
||||
assert.equal(detectMalformedNonStream(body), "empty_choices");
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns null when tool_calls present", () => {
|
||||
const body = {
|
||||
choices: [
|
||||
{
|
||||
message: { content: null, tool_calls: [{ id: "call_1", type: "function" }] },
|
||||
finish_reason: "tool_calls",
|
||||
},
|
||||
],
|
||||
};
|
||||
assert.equal(detectMalformedNonStream(body), null);
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns null when reasoning_content present (reasoning-only)", () => {
|
||||
const body = {
|
||||
choices: [
|
||||
{
|
||||
message: { content: "", reasoning_content: "some reasoning text", tool_calls: null },
|
||||
finish_reason: "stop",
|
||||
},
|
||||
],
|
||||
};
|
||||
assert.equal(detectMalformedNonStream(body), null);
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns 'empty_choices' for Responses API with empty output", () => {
|
||||
const body = { object: "response", output: [], status: "completed" };
|
||||
assert.equal(detectMalformedNonStream(body), "empty_choices");
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns null for Responses API with text output", () => {
|
||||
const body = {
|
||||
object: "response",
|
||||
status: "completed",
|
||||
output: [
|
||||
{
|
||||
type: "message",
|
||||
content: [{ type: "output_text", text: "Hello there!" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
assert.equal(detectMalformedNonStream(body), null);
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns 'no_terminal' for Responses API with failed status", () => {
|
||||
const body = {
|
||||
object: "response",
|
||||
status: "failed",
|
||||
output: [
|
||||
{
|
||||
type: "message",
|
||||
content: [{ type: "output_text", text: "Some text" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
assert.equal(detectMalformedNonStream(body), "no_terminal");
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream allows Responses API function_call items as valid output", () => {
|
||||
const body = {
|
||||
object: "response",
|
||||
status: "completed",
|
||||
output: [{ type: "function_call", name: "search", arguments: "{}" }],
|
||||
};
|
||||
assert.equal(detectMalformedNonStream(body), null);
|
||||
});
|
||||
|
||||
// ── (c2) detectMalformedNonStream — Claude Messages shape ────────────────────
|
||||
|
||||
test("detectMalformedNonStream returns null for Claude message with text content", () => {
|
||||
const body = {
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "Hi!" }],
|
||||
stop_reason: "end_turn",
|
||||
};
|
||||
assert.equal(detectMalformedNonStream(body), null);
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns null for Claude message with tool_use block", () => {
|
||||
const body = {
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [{ type: "tool_use", id: "toolu_1", name: "search", input: {} }],
|
||||
stop_reason: "tool_use",
|
||||
};
|
||||
assert.equal(detectMalformedNonStream(body), null);
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns null for Claude message with thinking block", () => {
|
||||
const body = {
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [{ type: "thinking", thinking: "let me think" }],
|
||||
stop_reason: "end_turn",
|
||||
};
|
||||
assert.equal(detectMalformedNonStream(body), null);
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns 'empty_choices' for Claude message with empty content", () => {
|
||||
const body = { type: "message", role: "assistant", content: [], stop_reason: "end_turn" };
|
||||
assert.equal(detectMalformedNonStream(body), "empty_choices");
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns 'empty_choices' for Claude message with empty-text block", () => {
|
||||
const body = {
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "" }],
|
||||
stop_reason: "end_turn",
|
||||
};
|
||||
assert.equal(detectMalformedNonStream(body), "empty_choices");
|
||||
});
|
||||
|
||||
test("detectMalformedNonStream returns 'empty_choices' for Claude message with (empty response) placeholder", () => {
|
||||
// convertOpenAINonStreamingToClaude substitutes this placeholder for empty
|
||||
// upstream content; it must still be treated as malformed.
|
||||
const body = {
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "(empty response)" }],
|
||||
stop_reason: "end_turn",
|
||||
};
|
||||
assert.equal(detectMalformedNonStream(body), "empty_choices");
|
||||
});
|
||||
|
||||
// ── (d) no stack trace leakage ───────────────────────────────────────────────
|
||||
|
||||
test("synthOpenAIErrorChunk message does NOT contain stack trace path", () => {
|
||||
const line = synthOpenAIErrorChunk({ provider: "p", model: "m", reason: "empty_stream" });
|
||||
const payload = JSON.parse(line.slice("data: ".length).trimEnd());
|
||||
const msg = payload.error.message as string;
|
||||
assert.ok(
|
||||
!msg.includes("at /"),
|
||||
`error.message must not contain stack trace patterns, got: ${msg}`
|
||||
);
|
||||
});
|
||||
|
||||
test("reportMalformed200 runs without throwing", () => {
|
||||
// smoke: it only logs, should not throw
|
||||
assert.doesNotThrow(() =>
|
||||
reportMalformed200({
|
||||
mode: "nonstream",
|
||||
provider: "testprov",
|
||||
model: "testmodel",
|
||||
connectionId: "conn-123",
|
||||
reason: "empty_choices",
|
||||
recvBytes: 42,
|
||||
recvLines: -1,
|
||||
emitted: -1,
|
||||
events: { "response.completed": 1 },
|
||||
ttftMs: 100,
|
||||
elapsedMs: 200,
|
||||
})
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user