diff --git a/mem0-ts/src/client/tests/integration/helpers.ts b/mem0-ts/src/client/tests/integration/helpers.ts index df77d7220..af8c44f13 100644 --- a/mem0-ts/src/client/tests/integration/helpers.ts +++ b/mem0-ts/src/client/tests/integration/helpers.ts @@ -3,9 +3,13 @@ * * Provides environment gating, client factory, polling helpers, * and console suppression for telemetry noise. + * + * API credit budget: these helpers are designed to minimize API calls. + * Each CI run should use ~40 calls total across all test files. */ import { MemoryClient } from "../../mem0"; import type { Memory } from "../../mem0.types"; +import { NetworkError, RateLimitError } from "../../../common/exceptions"; // ─── Environment gate ──────────────────────────────────── export const API_KEY = process.env.MEM0_API_KEY; @@ -15,9 +19,35 @@ export const describeIntegration = API_KEY ? describe : describe.skip; * Create a MemoryClient with the real API key. * Call this inside beforeAll — not at module scope — so it only * runs when the suite is not skipped. + * + * The returned client retries transient errors (502, 503, 504, 429) + * with backoff so CI runs are not flaky. */ export function createTestClient(): MemoryClient { - return new MemoryClient({ apiKey: API_KEY! }); + const client = new MemoryClient({ apiKey: API_KEY! }); + + const originalFetch = (client as any)._fetchWithErrorHandling.bind(client); + (client as any)._fetchWithErrorHandling = async ( + url: string, + options: any, + ) => { + const MAX_RETRIES = 2; + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + return await originalFetch(url, options); + } catch (error: any) { + const isTransient = + error instanceof NetworkError || error instanceof RateLimitError; + if (isTransient && attempt < MAX_RETRIES) { + await new Promise((r) => setTimeout(r, 3_000 * attempt)); + continue; + } + throw error; + } + } + }; + + return client; } /** @@ -42,6 +72,27 @@ export async function waitForMemories( return await client.getAll({ user_id: userId }); } +/** + * Poll search until results appear. Only used by search tests — + * other test files should NOT call this to avoid wasting API credits. + */ +export async function waitForSearchResults( + client: MemoryClient, + query: string, + options: Record, + maxWaitMs = 30_000, +): Promise { + const start = Date.now(); + while (Date.now() - start < maxWaitMs) { + const results = await client.search(query, options); + if (Array.isArray(results) && results.length > 0) { + return results; + } + await new Promise((r) => setTimeout(r, 3_000)); + } + return await client.search(query, options); +} + /** * Suppress telemetry console noise during tests. * Returns a cleanup function to call in afterAll. @@ -69,7 +120,10 @@ export function suppressTelemetryNoise(): () => void { /** * Add test memories and wait for them to be processed. - * Returns the memory IDs once available. + * Returns the memory IDs once available via getAll. + * + * NOTE: This only waits for the listing index. If your test needs + * search results, call waitForSearchResults() separately. */ export async function seedTestMemories( client: MemoryClient, @@ -104,17 +158,7 @@ export async function seedTestMemories( { user_id: userId }, ); - // Wait for async processing (listing index) const memories = await waitForMemories(client, userId, 1); - - // Also wait for search index — it can lag behind getAll under load (e.g. CI) - const start = Date.now(); - while (Date.now() - start < 60_000) { - const results = await client.search("favorite color", { user_id: userId }); - if (Array.isArray(results) && results.length > 0) break; - await new Promise((r) => setTimeout(r, 3_000)); - } - return memories.map((m) => m.id); } diff --git a/mem0-ts/src/client/tests/integration/search.test.ts b/mem0-ts/src/client/tests/integration/search.test.ts index 2e6d71e63..9d085a31b 100644 --- a/mem0-ts/src/client/tests/integration/search.test.ts +++ b/mem0-ts/src/client/tests/integration/search.test.ts @@ -13,6 +13,7 @@ import { suppressTelemetryNoise, seedTestMemories, cleanupTestUser, + waitForSearchResults, } from "./helpers"; jest.setTimeout(120_000); @@ -38,9 +39,12 @@ describeIntegration("MemoryClient Integration — Search & History", () => { // ─── Search v1 ──────────────────────────────────────────── describe("search v1", () => { test("searches memories by user_id and returns results with scores", async () => { - const results = await client.search("What is my favorite color?", { - user_id: TEST_USER_ID, - }); + // Search index may lag behind listing index — poll until ready + const results = await waitForSearchResults( + client, + "What is my favorite color?", + { user_id: TEST_USER_ID }, + ); expect(Array.isArray(results)).toBe(true); expect(results.length).toBeGreaterThan(0); @@ -56,12 +60,14 @@ describeIntegration("MemoryClient Integration — Search & History", () => { // ─── Search v2 ──────────────────────────────────────────── describe("search v2", () => { test("searches with OR filters and returns results", async () => { - const results = await client.search("What do you know about me?", { - filters: { - OR: [{ user_id: TEST_USER_ID }], + const results = await waitForSearchResults( + client, + "What do you know about me?", + { + filters: { OR: [{ user_id: TEST_USER_ID }] }, + api_version: "v2", }, - api_version: "v2", - }); + ); expect(Array.isArray(results)).toBe(true); expect(results.length).toBeGreaterThan(0);