fix: preserve responses SSE lifecycle during replay
This commit is contained in:
@@ -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`,
|
||||
|
||||
Reference in New Issue
Block a user