























@@ -1,3 +1,4 @@
1+import { Stream } from "openai/streaming";
12import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2334const warn = vi.hoisted(() => vi.fn());
@@ -30,6 +31,10 @@ describe("buildTimeoutAbortSignal", () => {
3031await vi.advanceTimersByTimeAsync(25);
31323233expect(signal?.aborted).toBe(true);
34+expect(signal?.reason).toMatchObject({
35+name: "TimeoutError",
36+message: "request timed out",
37+});
3338expect(warn).toHaveBeenCalledTimes(1);
3439expect(warn).toHaveBeenCalledWith(
3540"fetch timeout reached; aborting operation",
@@ -45,6 +50,45 @@ describe("buildTimeoutAbortSignal", () => {
4550cleanup();
4651});
475253+it("keeps timeout aborts visible to OpenAI SSE streams instead of cleanly ending", async () => {
54+const { signal, cleanup } = buildTimeoutAbortSignal({
55+timeoutMs: 25,
56+operation: "unit-test",
57+});
58+const encoder = new TextEncoder();
59+const response = new Response(
60+new ReadableStream({
61+start(controller) {
62+controller.enqueue(encoder.encode('data: {"ok": true}\n\n'));
63+signal?.addEventListener(
64+"abort",
65+() => controller.error(signal.reason ?? new Error("request timed out")),
66+{ once: true },
67+);
68+},
69+}),
70+{ headers: { "content-type": "text/event-stream" } },
71+);
72+73+const iterator = Stream.fromSSEResponse(response, new AbortController())[
74+Symbol.asyncIterator
75+]();
76+77+await expect(iterator.next()).resolves.toMatchObject({
78+done: false,
79+value: { ok: true },
80+});
81+const pending = iterator.next().catch((error: unknown) => error);
82+await vi.advanceTimersByTimeAsync(25);
83+84+await expect(pending).resolves.toMatchObject({
85+name: "TimeoutError",
86+message: "request timed out",
87+});
88+89+cleanup();
90+});
91+4892it("annotates timeout logs when the timer fires late", async () => {
4993vi.setSystemTime(0);
5094const { cleanup } = buildTimeoutAbortSignal({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。