diff --git a/packages/tools/src/shared/forget-memory.test.ts b/packages/tools/src/shared/forget-memory.test.ts new file mode 100644 index 000000000..a97df2f76 --- /dev/null +++ b/packages/tools/src/shared/forget-memory.test.ts @@ -0,0 +1,59 @@ +import { afterEach, describe, expect, it, vi } from "vitest" +import { forgetMemoryRequest } from "./forget-memory" + +const originalFetch = globalThis.fetch + +afterEach(() => { + globalThis.fetch = originalFetch + vi.restoreAllMocks() +}) + +describe("forgetMemoryRequest signal composition", () => { + it("aborts via the caller's signal even though the 30s timeout is also armed", async () => { + // Simulate a server that never answers, honoring the signal like real + // fetch: rejection on abort proves the composed signal is wired + // through to the request. + const fetchMock = vi.fn().mockImplementation( + (_url: unknown, init?: RequestInit) => + new Promise((_resolve, reject) => { + const signal = init?.signal as AbortSignal | undefined + signal?.addEventListener("abort", () => { + reject(new Error("aborted")) + }) + }), + ) + globalThis.fetch = fetchMock as unknown as typeof fetch + + const controller = new AbortController() + const promise = forgetMemoryRequest( + "sm_key", + { containerTag: "c" }, + "https://api.example.com", + { + signal: controller.signal, + }, + ) + + controller.abort() + await expect(promise).rejects.toThrow() + expect(fetchMock.mock.calls.length).toBe(1) + }) + + it("applies the 30s deadline when no caller signal is provided", async () => { + const fetchMock = vi.fn().mockImplementation((_url, init) => { + const signal = (init as RequestInit).signal as AbortSignal + // The timeout must be armed even without a caller signal. + expect(signal).toBeTruthy() + return Promise.resolve(new Response(null, { status: 200 })) + }) + globalThis.fetch = fetchMock as unknown as typeof fetch + + await expect( + forgetMemoryRequest( + "sm_key", + { containerTag: "c" }, + "https://api.example.com", + ), + ).resolves.toBeUndefined() + }) +}) diff --git a/packages/tools/src/shared/forget-memory.ts b/packages/tools/src/shared/forget-memory.ts index 8691c92ac..184bd960c 100644 --- a/packages/tools/src/shared/forget-memory.ts +++ b/packages/tools/src/shared/forget-memory.ts @@ -33,7 +33,13 @@ export async function forgetMemoryRequest( Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify(params), - signal: options?.signal ?? AbortSignal.timeout(FETCH_TIMEOUT_MS), + // Compose, don't choose: `??` let a caller-supplied signal silently + // drop the 30s safety timeout (and vice versa). Any signal firing — + // caller abort or the deadline — must abort the request. + signal: AbortSignal.any([ + ...(options?.signal ? [options.signal] : []), + AbortSignal.timeout(FETCH_TIMEOUT_MS), + ]), }) if (!response.ok) {