Skip to content

Commit 98c5db5

Browse files
committed
test(api): strengthen requesty abort assertions and share settle guard helper
1 parent d298d4a commit 98c5db5

4 files changed

Lines changed: 47 additions & 58 deletions

File tree

‎src/api/providers/__tests__/requesty.spec.ts‎

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,33 +17,7 @@ import { ApiHandlerCreateMessageMetadata } from "../../index"
1717
import { makeApiHandlerOptions } from "../../../test-utils/api"
1818
import { asyncStreamFrom, collectStream } from "../../../test-utils/stream"
1919
import { clearAllMocks } from "../../../test-utils/reset"
20-
21-
/**
22-
* Stryker guard: fails fast if `promise` does not settle within `ms`.
23-
*
24-
* Stryker's per-mutant cutoff (timeoutMS 5s x timeoutFactor 1.5 ~= 7.5s) is shorter
25-
* than vitest's testTimeout (20s). A mutant that removes a settle call (or an abort
26-
* listener) leaves an awaited promise pending forever; without this guard the test
27-
* would outlive the cutoff and the mutant would be reported as Timeout (inconclusive).
28-
* Settling the guard at 500ms turns those mutants into fast failures (KILLED).
29-
*/
30-
function withSettleGuard<T>(promise: Promise<T>, ms = 500): Promise<T> {
31-
return new Promise<T>((resolve, reject) => {
32-
const timer = setTimeout(() => {
33-
reject(new Error(`settle guard timed out after ${ms}ms`))
34-
}, ms)
35-
void promise.then(
36-
(value) => {
37-
clearTimeout(timer)
38-
resolve(value)
39-
},
40-
(error) => {
41-
clearTimeout(timer)
42-
reject(error)
43-
},
44-
)
45-
})
46-
}
20+
import { withSettleGuard } from "../../../test-utils/settle-guard"
4721

4822
const mockCreate = vitest.fn()
4923

@@ -770,6 +744,7 @@ describe("RequestyHandler", () => {
770744
expect.objectContaining({ model: expect.any(String) }),
771745
expect.objectContaining({
772746
timeout: 5000,
747+
signal: expect.any(AbortSignal),
773748
}),
774749
)
775750
})
@@ -795,6 +770,7 @@ describe("RequestyHandler", () => {
795770
name: "AbortError",
796771
message: "This operation was aborted",
797772
})
773+
expect(mockCreate).not.toHaveBeenCalled()
798774
})
799775

800776
it("rejects with AbortError when the signal aborts during model lookup", async () => {

‎src/api/providers/requesty.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,14 +263,14 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan
263263
// Aborted requests are user-initiated: surface them as AbortError (this also covers
264264
// timeouts, which abort the same signal) instead of a completion error.
265265
if (requestAbortSignal?.aborted) {
266-
throw createAbortError("Requesty")
266+
throw createAbortError(this.providerName)
267267
}
268268
throw handleOpenAIError(error, this.providerName)
269269
}
270270

271271
if (requestAbortSignal?.aborted) {
272272
// The response resolved after the request was aborted: do not return the late result.
273-
throw createAbortError("Requesty")
273+
throw createAbortError(this.providerName)
274274
}
275275
return response.choices[0]?.message.content || ""
276276
}

‎src/api/providers/utils/__tests__/abort-signal.spec.ts‎

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,7 @@ import {
66
rejectOnAbort,
77
throwIfAborted,
88
} from "../abort-signal"
9-
10-
/**
11-
* Stryker guard: fails fast if `promise` does not settle within `ms`.
12-
*
13-
* Stryker's per-mutant cutoff (timeoutMS 5s x timeoutFactor 1.5 ~= 7.5s) is shorter
14-
* than vitest's testTimeout (20s). A mutant that removes a settle call (or an abort
15-
* listener) leaves an awaited promise pending forever; without this guard the test
16-
* would outlive the cutoff and the mutant would be reported as Timeout (inconclusive).
17-
* Settling the guard at 500ms turns those mutants into fast failures (KILLED).
18-
*/
19-
function withSettleGuard<T>(promise: Promise<T>, ms = 500): Promise<T> {
20-
return new Promise<T>((resolve, reject) => {
21-
const timer = setTimeout(() => {
22-
reject(new Error(`settle guard timed out after ${ms}ms`))
23-
}, ms)
24-
void promise.then(
25-
(value) => {
26-
clearTimeout(timer)
27-
resolve(value)
28-
},
29-
(error) => {
30-
clearTimeout(timer)
31-
reject(error)
32-
},
33-
)
34-
})
35-
}
9+
import { withSettleGuard } from "../../../../test-utils/settle-guard"
3610

3711
describe("rejectOnAbort", () => {
3812
it("resolves with the pending value when it settles before the signal aborts", async () => {
@@ -64,6 +38,7 @@ describe("rejectOnAbort", () => {
6438

6539
await expect(withSettleGuard(rejectOnAbort(pending, controller.signal, "TestProvider"))).rejects.toMatchObject({
6640
name: "AbortError",
41+
message: "The TestProvider request was aborted",
6742
})
6843
})
6944

@@ -78,26 +53,38 @@ describe("rejectOnAbort", () => {
7853

7954
it("detaches the abort listener once the pending settles", async () => {
8055
const controller = new AbortController()
56+
const addSpy = vi.spyOn(controller.signal, "addEventListener")
8157
const removeSpy = vi.spyOn(controller.signal, "removeEventListener")
8258

8359
await expect(
8460
withSettleGuard(rejectOnAbort(Promise.resolve("done"), controller.signal, "TestProvider")),
8561
).resolves.toBe("done")
8662

87-
expect(removeSpy).toHaveBeenCalledWith("abort", expect.any(Function))
63+
// The settle path must remove the exact listener that was registered, not just any
64+
// function: removing a different reference would leave the original abort listener
65+
// attached to the signal.
66+
const registeredListener = addSpy.mock.calls[0]?.[1] as EventListener | undefined
67+
expect(removeSpy).toHaveBeenCalledWith("abort", registeredListener)
68+
addSpy.mockRestore()
8869
removeSpy.mockRestore()
8970
})
9071

9172
it("detaches the abort listener when the pending rejects", async () => {
9273
const controller = new AbortController()
74+
const addSpy = vi.spyOn(controller.signal, "addEventListener")
9375
const removeSpy = vi.spyOn(controller.signal, "removeEventListener")
9476
const lookupError = new Error("lookup failed")
9577

9678
await expect(
9779
withSettleGuard(rejectOnAbort(Promise.reject(lookupError), controller.signal, "TestProvider")),
9880
).rejects.toBe(lookupError)
9981

100-
expect(removeSpy).toHaveBeenCalledWith("abort", expect.any(Function))
82+
// The settle path must remove the exact listener that was registered, not just any
83+
// function: removing a different reference would leave the original abort listener
84+
// attached to the signal.
85+
const registeredListener = addSpy.mock.calls[0]?.[1] as EventListener | undefined
86+
expect(removeSpy).toHaveBeenCalledWith("abort", registeredListener)
87+
addSpy.mockRestore()
10188
removeSpy.mockRestore()
10289
})
10390
})

‎src/test-utils/settle-guard.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Stryker guard: fails fast if `promise` does not settle within `ms`.
3+
*
4+
* Stryker's per-mutant cutoff (timeoutMS 5s x timeoutFactor 1.5 ~= 7.5s) is shorter
5+
* than vitest's testTimeout (20s). A mutant that removes a settle call (or an abort
6+
* listener) leaves an awaited promise pending forever; without this guard the test
7+
* would outlive the cutoff and the mutant would be reported as Timeout (inconclusive).
8+
* Settling the guard at 500ms turns those mutants into fast failures (KILLED).
9+
*/
10+
export function withSettleGuard<T>(promise: Promise<T>, ms = 500): Promise<T> {
11+
return new Promise<T>((resolve, reject) => {
12+
const timer = setTimeout(() => {
13+
reject(new Error(`settle guard timed out after ${ms}ms`))
14+
}, ms)
15+
void promise.then(
16+
(value) => {
17+
clearTimeout(timer)
18+
resolve(value)
19+
},
20+
(error) => {
21+
clearTimeout(timer)
22+
reject(error)
23+
},
24+
)
25+
})
26+
}

0 commit comments

Comments
 (0)