mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-10 03:28:14 +00:00
Merge 014e1e924d into ac21804982
This commit is contained in:
commit
6e1b5c0788
4 changed files with 113 additions and 4 deletions
66
apps/mcp/src/server/client/index.test.ts
Normal file
66
apps/mcp/src/server/client/index.test.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { afterEach, describe, expect, it, vi } from "vitest"
|
||||
import { SupermemoryClient } from "./index"
|
||||
|
||||
const API_URL = "https://api.example.com"
|
||||
|
||||
describe("SupermemoryClient.getDocuments", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
function stubFetch() {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
Response.json({
|
||||
documents: [],
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
limit: 200,
|
||||
totalItems: 0,
|
||||
totalPages: 0,
|
||||
},
|
||||
}),
|
||||
)
|
||||
vi.stubGlobal("fetch", fetchMock)
|
||||
return fetchMock
|
||||
}
|
||||
|
||||
it("cancels through a caller-provided signal", async () => {
|
||||
const fetchMock = stubFetch()
|
||||
const controller = new AbortController()
|
||||
|
||||
await new SupermemoryClient("sm_test_key", "user_1", API_URL).getDocuments(
|
||||
["user_1"],
|
||||
1,
|
||||
200,
|
||||
{ signal: controller.signal },
|
||||
)
|
||||
|
||||
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
|
||||
controller.abort()
|
||||
expect(init.signal?.aborted).toBe(true)
|
||||
})
|
||||
|
||||
it("keeps the timeout when a caller-provided signal is present", async () => {
|
||||
const timeoutController = new AbortController()
|
||||
const timeoutSpy = vi
|
||||
.spyOn(AbortSignal, "timeout")
|
||||
.mockReturnValue(timeoutController.signal)
|
||||
const fetchMock = stubFetch()
|
||||
|
||||
await new SupermemoryClient("sm_test_key", "user_1", API_URL).getDocuments(
|
||||
["user_1"],
|
||||
1,
|
||||
200,
|
||||
{ signal: new AbortController().signal },
|
||||
)
|
||||
|
||||
expect(timeoutSpy).toHaveBeenCalledWith(30_000)
|
||||
|
||||
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
|
||||
// Firing only the timeout leg aborts the request: a caller signal adds
|
||||
// cancellation, it does not remove the 30s bound.
|
||||
timeoutController.abort()
|
||||
expect(init.signal?.aborted).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
@ -339,7 +339,14 @@ export class SupermemoryClient {
|
|||
options?: { signal?: AbortSignal },
|
||||
): Promise<DocumentsApiResponse> {
|
||||
try {
|
||||
const signal = options?.signal ?? AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
||||
// Compose rather than choose: a caller-supplied signal must add
|
||||
// cancellation on top of the timeout, not replace it.
|
||||
const signal = options?.signal
|
||||
? AbortSignal.any([
|
||||
options.signal,
|
||||
AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
||||
])
|
||||
: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
||||
const response = await fetch(`${this.apiUrl}/v3/documents/documents`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,11 @@ export async function forgetMemoryRequest(
|
|||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
signal: options?.signal ?? AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
||||
// Compose rather than choose: a caller-supplied signal must add cancellation
|
||||
// on top of the timeout, not replace it, or the request becomes unbounded.
|
||||
signal: options?.signal
|
||||
? AbortSignal.any([options.signal, AbortSignal.timeout(FETCH_TIMEOUT_MS)])
|
||||
: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ describe("memoryForget", () => {
|
|||
expect(init.signal).toBeInstanceOf(AbortSignal)
|
||||
})
|
||||
|
||||
it("uses a caller-provided signal instead of creating a timeout", async () => {
|
||||
it("cancels through a caller-provided signal", async () => {
|
||||
const fetchMock = stubFetch()
|
||||
const controller = new AbortController()
|
||||
|
||||
|
|
@ -192,7 +192,39 @@ describe("memoryForget", () => {
|
|||
)
|
||||
|
||||
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
|
||||
expect(init.signal).toBe(controller.signal)
|
||||
// The request signal is a composite, not the caller's own, but aborting
|
||||
// the caller still aborts the request.
|
||||
expect(init.signal).not.toBe(controller.signal)
|
||||
controller.abort()
|
||||
expect(init.signal?.aborted).toBe(true)
|
||||
})
|
||||
|
||||
it("keeps the timeout when a caller-provided signal is present", async () => {
|
||||
const timeoutController = new AbortController()
|
||||
const timeoutSpy = vi
|
||||
.spyOn(AbortSignal, "timeout")
|
||||
.mockReturnValue(timeoutController.signal)
|
||||
const fetchMock = stubFetch()
|
||||
const controller = new AbortController()
|
||||
|
||||
try {
|
||||
await forgetMemoryRequest(
|
||||
API_KEY,
|
||||
{ containerTag: "user_1", id: "mem_1" },
|
||||
undefined,
|
||||
{ signal: controller.signal },
|
||||
)
|
||||
|
||||
expect(timeoutSpy).toHaveBeenCalledWith(30_000)
|
||||
|
||||
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
|
||||
// Firing only the timeout leg aborts the request: a caller signal adds
|
||||
// cancellation, it does not remove the 30s bound.
|
||||
timeoutController.abort()
|
||||
expect(init.signal?.aborted).toBe(true)
|
||||
} finally {
|
||||
timeoutSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
it("throws a descriptive error on non-2xx responses", async () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue