fix: preserve responses SSE lifecycle during replay

This commit is contained in:
2026-07-11 02:36:00 +08:00
parent 92d3845eca
commit 7d0a40e1ea
3 changed files with 89 additions and 197 deletions
+72 -34
View File
@@ -562,6 +562,30 @@ async function readSseUntilClose(url, requestBody) {
firstChunkDelayMs,
};
}
function parseSseEvents(text) {
return `${text || ""}`
.split(/\r?\n\r?\n/)
.filter((block) => block.trim())
.map((block) => {
const lines = block.split(/\r?\n/);
const eventName = lines
.filter((line) => line.startsWith("event:"))
.map((line) => line.replace(/^event:\s?/, "").trim())
.find(Boolean) || "";
const payloadText = lines
.filter((line) => line.startsWith("data:"))
.map((line) => line.replace(/^data:\s?/, ""))
.join("\n");
let payload = null;
try {
payload = JSON.parse(payloadText);
} catch {
// [DONE] and malformed test payloads intentionally remain raw.
}
return { eventName, payloadText, payload };
});
}
async function run() {
assert(
@@ -1611,43 +1635,57 @@ async function run() {
}
}
let previewResolved = false;
const previewFetchPromise = fetch(`http://127.0.0.1:${gatewayPort}/responses`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
const capturedLifecycleStream = await readSseUntilClose(
`http://127.0.0.1:${gatewayPort}/responses`,
{
stream: true,
thread_id: "thread-preview",
thread_id: "thread-captured-lifecycle",
test_reasoning_tokens: 128,
test_stream_include_lifecycle: true,
test_stream_lifecycle_marker: "preview-marker",
test_stream_delta_chunks: 4,
test_stream_chunk_delay_ms: 320,
}),
}).then((response) => {
previewResolved = true;
return response;
});
await new Promise((resolve) => setTimeout(resolve, 1350));
assert(previewResolved, "responses thread 重打预览未在首个上游 chunk 后尽早返回头部");
const previewResponse = await previewFetchPromise;
assert(previewResponse.status === 200, `responses thread 重打预览状态异常: ${previewResponse.status}`);
const previewReader = previewResponse.body.getReader();
const previewDecoder = new TextDecoder();
const firstPreviewRead = await previewReader.read();
const firstPreviewText = previewDecoder.decode(firstPreviewRead.value || new Uint8Array(), { stream: true });
assert(firstPreviewText.includes('"type":"response.created"'), "responses thread 重打预览未先发 response.created");
let previewText = firstPreviewText;
while (true) {
const { done, value } = await previewReader.read();
if (done) {
break;
}
previewText += previewDecoder.decode(value, { stream: true });
}
previewText += previewDecoder.decode();
assert(previewText.includes('"delta":"hello-1"'), "responses thread 重打预览未回放最终 delta");
assert(!previewText.includes("preview-marker"), "responses thread 重打预览不应透传巨大的 lifecycle 原始 payload");
test_stream_lifecycle_marker: "captured-lifecycle-marker",
test_stream_delta_chunks: 64,
test_stream_delta_text: "captured-chunk",
test_stream_chunk_delay_ms: 2,
},
);
assert(capturedLifecycleStream.status === 200, `/responses 捕获回放状态异常: ${capturedLifecycleStream.status}`);
assert(capturedLifecycleStream.readCount > 1, "/responses 捕获回放不应退化为单块响应");
const capturedEvents = parseSseEvents(capturedLifecycleStream.text);
const firstReplayEvent = capturedEvents.find((event) => event.payload);
assert(
firstReplayEvent?.payload?.type === "response.created" &&
firstReplayEvent.payload.response?.id === "resp_stream",
"/responses 捕获回放的首个生命周期事件必须带真实 response ID",
);
const lifecycleEvents = capturedEvents.filter((event) => [
"response.created",
"response.in_progress",
"response.completed",
].includes(event.payload?.type));
assert(lifecycleEvents.length === 3, "/responses 捕获回放不应注入额外生命周期事件");
assert(
lifecycleEvents.every((event) => event.payload?.response?.id === "resp_stream"),
"/responses 捕获回放不应发送缺少真实 response ID 的生命周期事件",
);
assert(
lifecycleEvents.map((event) => event.payload.type).join(",") ===
"response.created,response.in_progress,response.completed",
"/responses 捕获回放未保留生命周期顺序",
);
const capturedDeltas = capturedEvents
.filter((event) => event.payload?.type === "response.output_text.delta")
.map((event) => event.payload.delta);
assert(capturedDeltas.length === 64, "/responses 捕获回放未保留全部长流 delta");
assert(
capturedDeltas.every((delta, index) => delta === `captured-chunk-${index + 1}`),
"/responses 捕获回放的长流 delta 顺序不完整",
);
const completedIndex = capturedEvents.findIndex((event) => event.payload?.type === "response.completed");
const lastDeltaIndex = capturedEvents
.map((event) => event.payload?.type)
.lastIndexOf("response.output_text.delta");
assert(completedIndex > lastDeltaIndex, "/responses 捕获回放未在所有 delta 后终止");
assert(!capturedLifecycleStream.text.includes("captured-lifecycle-marker"), "/responses lifecycle 归一化仍透传了巨大的生命周期原始 payload");
const normalizedLifecycleStream = await readSseUntilClose(
`http://127.0.0.1:${gatewayPort}/responses`,