Files
diegosouzapw--omniroute/tests/unit/responses-textless-terminal-3948.test.ts
2026-07-13 13:39:12 +08:00

101 lines
4.7 KiB
TypeScript

/**
* #3948 — Combo via n8n (non-streaming) returns empty content.
*
* A Responses-API target (codex/`cx`) streams from upstream even on
* `stream:false`. Its terminal `response.completed` snapshot can carry a
* non-empty `output` that LACKS the assistant message item (e.g. only a
* `reasoning` item) even though the streamed `response.output_text.delta`
* events reconstructed a full assistant message.
*
* `parseSSEToResponsesOutput` preferred the terminal `output` wholesale when it
* was non-empty, discarding the reconstructed delta text → empty content on
* `stream:false`. n8n defaults to `stream:false`, so the combo response came back
* HTTP 200 with empty content (regression vs 3.8.10).
*
* The aggregator must fall back to the reconstructed delta output when the
* terminal output has no message item but the reconstruction does — while still
* letting the terminal snapshot win whenever it already carries the message.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { parseSSEToResponsesOutput } from "../../open-sse/handlers/sseParser.ts";
type AnyRecord = Record<string, unknown>;
function messageText(output: AnyRecord[]): string {
const msg = output.find((o) => (o as AnyRecord).type === "message") as AnyRecord | undefined;
if (!msg) return "";
const content = Array.isArray(msg.content) ? (msg.content as AnyRecord[]) : [];
return content.map((p) => String((p as AnyRecord).text ?? "")).join("");
}
// Terminal `response.completed.output` carries ONLY the reasoning item; the
// assistant message text arrived purely via output_text deltas.
const SSE_TEXTLESS_TERMINAL = [
`event: response.created`,
`data: {"type":"response.created","response":{"id":"resp_1","object":"response","model":"gpt-5.2-codex","status":"in_progress"}}`,
``,
`event: response.output_item.added`,
`data: {"type":"response.output_item.added","output_index":0,"item":{"type":"reasoning","summary":[]}}`,
``,
`event: response.output_item.added`,
`data: {"type":"response.output_item.added","output_index":1,"item":{"type":"message","role":"assistant","content":[]}}`,
``,
`event: response.output_text.delta`,
`data: {"type":"response.output_text.delta","output_index":1,"delta":"Hello "}`,
``,
`event: response.output_text.delta`,
`data: {"type":"response.output_text.delta","output_index":1,"delta":"from codex"}`,
``,
`event: response.output_text.done`,
`data: {"type":"response.output_text.done","output_index":1,"text":"Hello from codex"}`,
``,
`event: response.completed`,
`data: {"type":"response.completed","response":{"id":"resp_1","object":"response","model":"gpt-5.2-codex","status":"completed","output":[{"type":"reasoning","summary":[{"type":"summary_text","text":"thinking..."}]}],"usage":{"input_tokens":10,"output_tokens":5}}}`,
``,
`data: [DONE]`,
``,
].join("\n");
// Control: terminal output DOES carry the assistant message — terminal must win.
const SSE_TERMINAL_WITH_MESSAGE = [
`event: response.created`,
`data: {"type":"response.created","response":{"id":"resp_2","object":"response","model":"gpt-5.2-codex","status":"in_progress"}}`,
``,
`event: response.output_item.added`,
`data: {"type":"response.output_item.added","output_index":0,"item":{"type":"message","role":"assistant","content":[]}}`,
``,
`event: response.output_text.delta`,
`data: {"type":"response.output_text.delta","output_index":0,"delta":"partial"}`,
``,
`event: response.completed`,
`data: {"type":"response.completed","response":{"id":"resp_2","object":"response","model":"gpt-5.2-codex","status":"completed","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","annotations":[],"text":"Hi there"}]}],"usage":{"input_tokens":3,"output_tokens":2}}}`,
``,
`data: [DONE]`,
``,
].join("\n");
test("#3948 aggregator recovers reconstructed message when terminal output is textless", () => {
const result = parseSSEToResponsesOutput(SSE_TEXTLESS_TERMINAL, "gpt-5.2-codex") as AnyRecord;
const output = result.output as AnyRecord[];
const hasMessage = output.some((o) => o.type === "message");
assert.ok(hasMessage, "output must contain the assistant message item, not just reasoning");
assert.equal(
messageText(output),
"Hello from codex",
"the reconstructed delta text must survive a textless terminal snapshot"
);
});
test("#3948 terminal output still wins when it carries the assistant message", () => {
const result = parseSSEToResponsesOutput(SSE_TERMINAL_WITH_MESSAGE, "gpt-5.2-codex") as AnyRecord;
const output = result.output as AnyRecord[];
assert.equal(
messageText(output),
"Hi there",
"the terminal snapshot message must be preserved (not overwritten by reconstruction)"
);
});