From 8c80b2e7c83a28eea75735636f343a204bd2046e Mon Sep 17 00:00:00 2001 From: kartik-mem0 Date: Wed, 18 Mar 2026 13:11:36 +0530 Subject: [PATCH] feat(test): integration test for ts-sdk --- .github/workflows/ts-sdk-ci.yml | 2 + mem0-ts/src/client/mem0.ts | 26 ++- .../client/tests/integration/batch.test.ts | 86 ++++++++ .../src/client/tests/integration/crud.test.ts | 187 ++++++++++++++++++ .../integration/feedback-and-export.test.ts | 83 ++++++++ .../src/client/tests/integration/helpers.ts | 129 ++++++++++++ .../tests/integration/initialization.test.ts | 63 ++++++ .../tests/integration/management.test.ts | 143 ++++++++++++++ .../client/tests/integration/search.test.ts | 102 ++++++++++ 9 files changed, 813 insertions(+), 8 deletions(-) create mode 100644 mem0-ts/src/client/tests/integration/batch.test.ts create mode 100644 mem0-ts/src/client/tests/integration/crud.test.ts create mode 100644 mem0-ts/src/client/tests/integration/feedback-and-export.test.ts create mode 100644 mem0-ts/src/client/tests/integration/helpers.ts create mode 100644 mem0-ts/src/client/tests/integration/initialization.test.ts create mode 100644 mem0-ts/src/client/tests/integration/management.test.ts create mode 100644 mem0-ts/src/client/tests/integration/search.test.ts diff --git a/.github/workflows/ts-sdk-ci.yml b/.github/workflows/ts-sdk-ci.yml index a993a120c..e49cc7eb4 100644 --- a/.github/workflows/ts-sdk-ci.yml +++ b/.github/workflows/ts-sdk-ci.yml @@ -59,6 +59,8 @@ jobs: - name: Run tests working-directory: mem0-ts + env: + MEM0_API_KEY: ${{ secrets.MEM0_API_KEY }} run: pnpm run test:ci - name: Verify package exports diff --git a/mem0-ts/src/client/mem0.ts b/mem0-ts/src/client/mem0.ts index a67d59e2d..ff14cb61e 100644 --- a/mem0-ts/src/client/mem0.ts +++ b/mem0-ts/src/client/mem0.ts @@ -674,12 +674,17 @@ export default class MemoryClient { async createWebhook(webhook: WebhookPayload): Promise { if (this.telemetryId === "") await this.ping(); this._captureEvent("create_webhook", []); + const { eventTypes, projectId, webhookId, ...rest } = webhook; + const payload = { + ...rest, + event_types: eventTypes, + }; const response = await this._fetchWithErrorHandling( `${this.host}/api/v1/webhooks/projects/${this.projectId}/`, { method: "POST", headers: this.headers, - body: JSON.stringify(webhook), + body: JSON.stringify(payload), }, ); return response; @@ -688,16 +693,18 @@ export default class MemoryClient { async updateWebhook(webhook: WebhookPayload): Promise<{ message: string }> { if (this.telemetryId === "") await this.ping(); this._captureEvent("update_webhook", []); - const project_id = webhook.projectId || this.projectId; + const { eventTypes, projectId, webhookId, ...rest } = webhook; + const payload = { + ...rest, + event_types: eventTypes, + project: projectId || this.projectId, + }; const response = await this._fetchWithErrorHandling( - `${this.host}/api/v1/webhooks/${webhook.webhookId}/`, + `${this.host}/api/v1/webhooks/${webhookId}/`, { method: "PUT", headers: this.headers, - body: JSON.stringify({ - ...webhook, - projectId: project_id, - }), + body: JSON.stringify(payload), }, ); return response; @@ -723,12 +730,15 @@ export default class MemoryClient { if (this.telemetryId === "") await this.ping(); const payloadKeys = Object.keys(data || {}); this._captureEvent("feedback", [payloadKeys]); + const payload: Record = { ...data }; + if (this.organizationId) payload.org_id = String(this.organizationId); + if (this.projectId) payload.project_id = String(this.projectId); const response = await this._fetchWithErrorHandling( `${this.host}/v1/feedback/`, { method: "POST", headers: this.headers, - body: JSON.stringify(data), + body: JSON.stringify(payload), }, ); return response; diff --git a/mem0-ts/src/client/tests/integration/batch.test.ts b/mem0-ts/src/client/tests/integration/batch.test.ts new file mode 100644 index 000000000..ad9b12391 --- /dev/null +++ b/mem0-ts/src/client/tests/integration/batch.test.ts @@ -0,0 +1,86 @@ +/** + * Integration tests: Batch operations. + * + * Tests batch update and batch delete against the real API. + * + * Run: MEM0_API_KEY=your-key npx jest batch.test.ts --forceExit + */ +import { MemoryClient } from "../../mem0"; +import { randomUUID } from "crypto"; +import { + describeIntegration, + createTestClient, + suppressTelemetryNoise, + seedTestMemories, + waitForMemories, + cleanupTestUser, +} from "./helpers"; + +jest.setTimeout(120_000); + +const TEST_USER_ID = `integration-batch-${randomUUID()}`; + +describeIntegration("MemoryClient Integration — Batch Operations", () => { + let client: MemoryClient; + let cleanup: () => void; + let memoryIds: string[] = []; + + beforeAll(async () => { + cleanup = suppressTelemetryNoise(); + client = createTestClient(); + memoryIds = await seedTestMemories(client, TEST_USER_ID); + }); + + afterAll(async () => { + await cleanupTestUser(client, TEST_USER_ID); + cleanup(); + }); + + test("batch updates memories", async () => { + expect(memoryIds.length).toBeGreaterThanOrEqual(1); + + const batchPayload = memoryIds + .slice(0, Math.min(2, memoryIds.length)) + .map((id) => ({ + memoryId: id, + text: `Batch updated content for ${id}`, + })); + + const result = await client.batchUpdate(batchPayload); + expect(result).toBeDefined(); + + // Verify the update took effect on at least one memory + const updated = await client.get(memoryIds[0]); + expect(typeof updated.memory).toBe("string"); + }); + + test("batch deletes throwaway memories", async () => { + // Add a throwaway memory, wait for it, then batch-delete + await client.add( + [ + { + role: "user" as const, + content: "Temporary memory for batch delete test.", + }, + { role: "assistant" as const, content: "Noted!" }, + ], + { user_id: TEST_USER_ID }, + ); + + // Wait for new memory to appear + const before = await waitForMemories( + client, + TEST_USER_ID, + memoryIds.length + 1, + ); + const throwawayIds = before + .filter((m) => !memoryIds.includes(m.id)) + .map((m) => m.id); + + // Assert we actually have something to delete + expect(throwawayIds.length).toBeGreaterThan(0); + + const result = await client.batchDelete(throwawayIds); + expect(result).toBeDefined(); + }); +}); diff --git a/mem0-ts/src/client/tests/integration/crud.test.ts b/mem0-ts/src/client/tests/integration/crud.test.ts new file mode 100644 index 000000000..53f9ba16b --- /dev/null +++ b/mem0-ts/src/client/tests/integration/crud.test.ts @@ -0,0 +1,187 @@ +/** + * Integration tests: Memory CRUD operations. + * + * Tests the full lifecycle: add → get → getAll → update → delete. + * Validates response shapes against the real API. + * + * Run: MEM0_API_KEY=your-key npx jest crud.test.ts --forceExit + */ +import { MemoryClient } from "../../mem0"; +import { MemoryError } from "../../../common/exceptions"; +import { randomUUID } from "crypto"; +import { + describeIntegration, + createTestClient, + suppressTelemetryNoise, + waitForMemories, + cleanupTestUser, +} from "./helpers"; + +jest.setTimeout(120_000); + +const TEST_USER_ID = `integration-crud-${randomUUID()}`; + +describeIntegration("MemoryClient Integration — CRUD", () => { + let client: MemoryClient; + let cleanup: () => void; + let memoryIds: string[] = []; + + beforeAll(() => { + cleanup = suppressTelemetryNoise(); + client = createTestClient(); + }); + + afterAll(async () => { + await cleanupTestUser(client, TEST_USER_ID); + cleanup(); + }); + + // ─── Add ────────────────────────────────────────────────── + describe("add memories", () => { + test("add returns a pending response with event_id", async () => { + const messages = [ + { + role: "user" as const, + content: "Hi, I'm integration-test-user. My favorite color is blue.", + }, + { + role: "assistant" as const, + content: + "Nice to meet you! I'll remember that your favorite color is blue.", + }, + ]; + + const result = await client.add(messages, { user_id: TEST_USER_ID }); + + // API processes memories asynchronously — returns PENDING + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeGreaterThan(0); + + // Validate response shape + for (const item of result) { + expect(item).toHaveProperty("status"); + expect(item).toHaveProperty("event_id"); + } + }); + + test("adds a second batch of messages", async () => { + const messages = [ + { + role: "user" as const, + content: "I work as a software engineer at Acme Corp.", + }, + { + role: "assistant" as const, + content: "Got it, you're a software engineer at Acme Corp!", + }, + ]; + + const result = await client.add(messages, { user_id: TEST_USER_ID }); + expect(Array.isArray(result)).toBe(true); + }); + + test("memories become available after async processing", async () => { + const memories = await waitForMemories(client, TEST_USER_ID, 1); + + expect(memories.length).toBeGreaterThan(0); + + // Store IDs for later tests + memoryIds = memories.map((m) => m.id); + expect(memoryIds.length).toBeGreaterThan(0); + expect(typeof memoryIds[0]).toBe("string"); + }); + }); + + // ─── Get by ID ──────────────────────────────────────────── + describe("get memory by ID", () => { + test("retrieves a specific memory with correct shape", async () => { + const memoryId = memoryIds[0]; + expect(memoryId).toBeDefined(); + + const memory = await client.get(memoryId); + + expect(memory.id).toBe(memoryId); + expect(typeof memory.memory).toBe("string"); + expect(memory.memory!.length).toBeGreaterThan(0); + expect(typeof memory.user_id).toBe("string"); + expect( + memory.metadata === null || typeof memory.metadata === "object", + ).toBe(true); + expect( + Array.isArray(memory.categories) || memory.categories === null, + ).toBe(true); + expect(new Date(memory.created_at || "").toString()).not.toBe( + "Invalid Date", + ); + expect(new Date(memory.updated_at || "").toString()).not.toBe( + "Invalid Date", + ); + }); + }); + + // ─── Get all ────────────────────────────────────────────── + describe("get all memories", () => { + test("returns all memories for test user", async () => { + const memories = await client.getAll({ user_id: TEST_USER_ID }); + + expect(Array.isArray(memories)).toBe(true); + expect(memories.length).toBeGreaterThanOrEqual(memoryIds.length); + + for (const mem of memories) { + expect(typeof mem.id).toBe("string"); + expect(typeof mem.memory).toBe("string"); + } + }); + }); + + // ─── Update ─────────────────────────────────────────────── + describe("update memory", () => { + test("updates memory text and verifies the content changed", async () => { + const memoryId = memoryIds[0]; + + // Read original text before update + const original = await client.get(memoryId); + const originalText = original.memory; + + await client.update(memoryId, { + text: "My favorite color is green (updated)", + }); + + const updated = await client.get(memoryId); + expect(typeof updated.memory).toBe("string"); + expect(updated.memory).not.toBe(originalText); + }); + }); + + // ─── Delete single ──────────────────────────────────────── + describe("delete memory", () => { + test("deletes a single memory by ID", async () => { + const memoryId = memoryIds[0]; + expect(memoryId).toBeDefined(); + + const result = await client.delete(memoryId); + expect(result).toBeDefined(); + expect(typeof result.message).toBe("string"); + }); + + test("getting deleted memory throws MemoryError", async () => { + const memoryId = memoryIds[0]; + await expect(client.get(memoryId)).rejects.toThrow(MemoryError); + }); + }); + + // ─── Delete all + delete user ───────────────────────────── + describe("cleanup operations", () => { + test("deletes all memories for test user", async () => { + const result = await client.deleteAll({ user_id: TEST_USER_ID }); + expect(result).toBeDefined(); + expect(typeof result.message).toBe("string"); + }); + + test("deletes the test user entity", async () => { + const result = await client.deleteUsers({ user_id: TEST_USER_ID }); + expect(result).toBeDefined(); + expect(result.message).toBe("Entity deleted successfully."); + }); + }); +}); diff --git a/mem0-ts/src/client/tests/integration/feedback-and-export.test.ts b/mem0-ts/src/client/tests/integration/feedback-and-export.test.ts new file mode 100644 index 000000000..4d988e402 --- /dev/null +++ b/mem0-ts/src/client/tests/integration/feedback-and-export.test.ts @@ -0,0 +1,83 @@ +/** + * Integration tests: Feedback and memory export. + * + * Tests feedback submission and the export lifecycle + * (create → retrieve) against the real API. + * + * Run: MEM0_API_KEY=your-key npx jest feedback-and-export.test.ts --forceExit + */ +import { MemoryClient } from "../../mem0"; +import { Feedback } from "../../mem0.types"; +import { randomUUID } from "crypto"; +import { + describeIntegration, + createTestClient, + suppressTelemetryNoise, + seedTestMemories, + cleanupTestUser, +} from "./helpers"; + +jest.setTimeout(120_000); + +const TEST_USER_ID = `integration-fb-export-${randomUUID()}`; + +describeIntegration("MemoryClient Integration — Feedback & Export", () => { + let client: MemoryClient; + let cleanup: () => void; + let memoryIds: string[] = []; + + beforeAll(async () => { + cleanup = suppressTelemetryNoise(); + client = createTestClient(); + memoryIds = await seedTestMemories(client, TEST_USER_ID); + }); + + afterAll(async () => { + await cleanupTestUser(client, TEST_USER_ID); + cleanup(); + }); + + // ─── Feedback ───────────────────────────────────────────── + describe("feedback", () => { + test("submits positive feedback on a memory", async () => { + const memoryId = memoryIds[0]; + expect(memoryId).toBeDefined(); + + const result = await client.feedback({ + memory_id: memoryId, + feedback: Feedback.POSITIVE, + }); + + expect(result).toBeDefined(); + }); + }); + + // ─── Export ─────────────────────────────────────────────── + describe("memory export", () => { + let exportId: string | undefined; + + test("creates a memory export", async () => { + const result = await client.createMemoryExport({ + filters: { AND: [{ user_id: TEST_USER_ID }] }, + schema: { memory: "string", user_id: "string" }, + }); + + expect(result).toBeDefined(); + expect( + typeof result.id === "string" || typeof result.message === "string", + ).toBe(true); + + exportId = result.id; + }); + + test("retrieves a memory export", async () => { + if (!exportId) return; + + const result = await client.getMemoryExport({ + memory_export_id: exportId, + }); + + expect(result).toBeDefined(); + }); + }); +}); diff --git a/mem0-ts/src/client/tests/integration/helpers.ts b/mem0-ts/src/client/tests/integration/helpers.ts new file mode 100644 index 000000000..4bb3a5fc2 --- /dev/null +++ b/mem0-ts/src/client/tests/integration/helpers.ts @@ -0,0 +1,129 @@ +/** + * Shared helpers for MemoryClient real integration tests. + * + * Provides environment gating, client factory, polling helpers, + * and console suppression for telemetry noise. + */ +import { MemoryClient } from "../../mem0"; +import type { Memory } from "../../mem0.types"; + +// ─── Environment gate ──────────────────────────────────── +export const API_KEY = process.env.MEM0_API_KEY; +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. + */ +export function createTestClient(): MemoryClient { + return new MemoryClient({ apiKey: API_KEY! }); +} + +/** + * Poll getAll until memories appear for a user. + * The Mem0 API processes memories asynchronously — after add() + * we need to wait for them to be available. + */ +export async function waitForMemories( + client: MemoryClient, + userId: string, + minCount: number, + maxWaitMs = 30_000, +): Promise { + const start = Date.now(); + while (Date.now() - start < maxWaitMs) { + const memories = await client.getAll({ user_id: userId }); + if (Array.isArray(memories) && memories.length >= minCount) { + return memories; + } + await new Promise((r) => setTimeout(r, 2_000)); + } + return await client.getAll({ user_id: userId }); +} + +/** + * Suppress telemetry console noise during tests. + * Returns a cleanup function to call in afterAll. + */ +export function suppressTelemetryNoise(): () => void { + const originalError = console.error; + const originalWarn = console.warn; + + jest.spyOn(console, "error").mockImplementation((...args: unknown[]) => { + if ( + String(args[0] ?? "").match( + /Telemetry|Failed to initialize|Failed to capture/, + ) + ) + return; + originalError(...args); + }); + jest.spyOn(console, "warn").mockImplementation((...args: unknown[]) => { + if (String(args[0] ?? "").match(/telemetry|Telemetry/)) return; + originalWarn(...args); + }); + + return () => jest.restoreAllMocks(); +} + +/** + * Add test memories and wait for them to be processed. + * Returns the memory IDs once available. + */ +export async function seedTestMemories( + client: MemoryClient, + userId: string, +): Promise { + await client.add( + [ + { + role: "user" as const, + content: "Hi, I'm integration-test-user. My favorite color is blue.", + }, + { + role: "assistant" as const, + content: + "Nice to meet you! I'll remember that your favorite color is blue.", + }, + ], + { user_id: userId }, + ); + + await client.add( + [ + { + role: "user" as const, + content: "I work as a software engineer at Acme Corp.", + }, + { + role: "assistant" as const, + content: "Got it, you're a software engineer at Acme Corp!", + }, + ], + { user_id: userId }, + ); + + // Wait for async processing + const memories = await waitForMemories(client, userId, 1); + return memories.map((m) => m.id); +} + +/** + * Clean up all test data for a user. Best-effort — ignores errors. + */ +export async function cleanupTestUser( + client: MemoryClient, + userId: string, +): Promise { + try { + await client.deleteAll({ user_id: userId }); + } catch { + // ignore + } + try { + await client.deleteUsers({ user_id: userId }); + } catch { + // ignore + } +} diff --git a/mem0-ts/src/client/tests/integration/initialization.test.ts b/mem0-ts/src/client/tests/integration/initialization.test.ts new file mode 100644 index 000000000..6336d0c11 --- /dev/null +++ b/mem0-ts/src/client/tests/integration/initialization.test.ts @@ -0,0 +1,63 @@ +/** + * Integration tests: Client initialization and error handling. + * + * Tests ping, org/project resolution, and invalid credentials. + * These tests do NOT need pre-seeded memories. + * + * Run: MEM0_API_KEY=your-key npx jest initialization.test.ts --forceExit + */ +import { MemoryClient } from "../../mem0"; +import { + MemoryError, + MemoryNotFoundError, + ValidationError, +} from "../../../common/exceptions"; +import { + describeIntegration, + createTestClient, + suppressTelemetryNoise, +} from "./helpers"; + +jest.setTimeout(60_000); + +describeIntegration("MemoryClient Integration — Initialization", () => { + let client: MemoryClient; + let cleanup: () => void; + + beforeAll(() => { + cleanup = suppressTelemetryNoise(); + client = createTestClient(); + }); + + afterAll(() => cleanup()); + + test("client pings successfully and resolves org/project", async () => { + await client.ping(); + expect(client.organizationId).toBeTruthy(); + expect(client.projectId).toBeTruthy(); + }); + + test("get with invalid ID throws ValidationError", async () => { + // Non-UUID string triggers a 400 ValidationError, not a 404 + await expect(client.get("nonexistent-memory-id-12345")).rejects.toThrow( + ValidationError, + ); + }); + + test("get with non-existent UUID throws MemoryNotFoundError", async () => { + await expect( + client.get("00000000-0000-0000-0000-000000000000"), + ).rejects.toThrow(MemoryNotFoundError); + }); + + test("all SDK exceptions are MemoryError subclasses", async () => { + await expect(client.get("nonexistent-memory-id-12345")).rejects.toThrow( + MemoryError, + ); + }); + + test("invalid API key throws on ping", async () => { + const badClient = new MemoryClient({ apiKey: "invalid-key-12345" }); + await expect(badClient.ping()).rejects.toThrow(); + }); +}); diff --git a/mem0-ts/src/client/tests/integration/management.test.ts b/mem0-ts/src/client/tests/integration/management.test.ts new file mode 100644 index 000000000..38b259051 --- /dev/null +++ b/mem0-ts/src/client/tests/integration/management.test.ts @@ -0,0 +1,143 @@ +/** + * Integration tests: User management, project, and webhooks. + * + * Tests users(), getProject(), and the full webhook lifecycle + * (create → list → update → delete) against the real API. + * + * Run: MEM0_API_KEY=your-key npx jest management.test.ts --forceExit + */ +import { MemoryClient } from "../../mem0"; +import type { Webhook } from "../../mem0.types"; +import { WebhookEvent } from "../../mem0.types"; +import { randomUUID } from "crypto"; +import { + describeIntegration, + createTestClient, + suppressTelemetryNoise, + seedTestMemories, + cleanupTestUser, +} from "./helpers"; + +jest.setTimeout(120_000); + +const TEST_USER_ID = `integration-mgmt-${randomUUID()}`; + +describeIntegration( + "MemoryClient Integration — Users, Project & Webhooks", + () => { + let client: MemoryClient; + let cleanup: () => void; + let webhookId: string | undefined; + + beforeAll(async () => { + cleanup = suppressTelemetryNoise(); + client = createTestClient(); + await seedTestMemories(client, TEST_USER_ID); + }); + + afterAll(async () => { + if (webhookId) { + try { + await client.deleteWebhook({ webhookId }); + } catch { + // ignore + } + } + await cleanupTestUser(client, TEST_USER_ID); + cleanup(); + }); + + // ─── Users ──────────────────────────────────────────────── + describe("user management", () => { + test("lists users and finds test user", async () => { + const allUsers = await client.users(); + + expect(typeof allUsers.count).toBe("number"); + expect(Array.isArray(allUsers.results)).toBe(true); + + if (allUsers.results.length > 0) { + const user = allUsers.results[0]; + expect(typeof user.id).toBe("string"); + expect(typeof user.name).toBe("string"); + expect(typeof user.type).toBe("string"); + } + + const testUser = allUsers.results.find((u) => u.name === TEST_USER_ID); + expect(testUser).toBeDefined(); + }); + }); + + // ─── Project ────────────────────────────────────────────── + describe("project management", () => { + test("gets project with custom_instructions field", async () => { + const project = await client.getProject({ + fields: ["custom_instructions"], + }); + + expect(project).toBeDefined(); + expect(typeof project).toBe("object"); + expect("custom_instructions" in project).toBe(true); + }); + }); + + // ─── Webhooks ───────────────────────────────────────────── + describe("webhook lifecycle", () => { + const webhookUrl = `https://example.com/webhook-test-${randomUUID()}`; + + test("creates a webhook", async () => { + const created = await client.createWebhook({ + name: `integration-test-webhook-${randomUUID().slice(0, 8)}`, + url: webhookUrl, + eventTypes: [WebhookEvent.MEMORY_ADDED, WebhookEvent.MEMORY_UPDATED], + projectId: String(client.projectId), + webhookId: "", + }); + + expect(created).toBeDefined(); + expect(typeof created.webhook_id).toBe("string"); + + webhookId = created.webhook_id; + }); + + test("lists webhooks and finds the created one", async () => { + const webhooks = await client.getWebhooks(); + + expect(Array.isArray(webhooks)).toBe(true); + + if (webhookId) { + const found = webhooks.find( + (w: Webhook) => w.webhook_id === webhookId, + ); + expect(found).toBeDefined(); + } + }); + + test("updates a webhook", async () => { + if (!webhookId) return; + + const result = await client.updateWebhook({ + webhookId, + name: `updated-webhook-${randomUUID().slice(0, 8)}`, + url: webhookUrl, + eventTypes: [ + WebhookEvent.MEMORY_ADDED, + WebhookEvent.MEMORY_UPDATED, + WebhookEvent.MEMORY_DELETED, + ], + projectId: String(client.projectId), + }); + + expect(result).toBeDefined(); + }); + + test("deletes the webhook", async () => { + if (!webhookId) return; + + const result = await client.deleteWebhook({ webhookId }); + expect(result).toBeDefined(); + + webhookId = undefined; + }); + }); + }, +); diff --git a/mem0-ts/src/client/tests/integration/search.test.ts b/mem0-ts/src/client/tests/integration/search.test.ts new file mode 100644 index 000000000..2e6d71e63 --- /dev/null +++ b/mem0-ts/src/client/tests/integration/search.test.ts @@ -0,0 +1,102 @@ +/** + * Integration tests: Search and history operations. + * + * Tests search v1, search v2, and memory history against the real API. + * + * Run: MEM0_API_KEY=your-key npx jest search.test.ts --forceExit + */ +import { MemoryClient } from "../../mem0"; +import { randomUUID } from "crypto"; +import { + describeIntegration, + createTestClient, + suppressTelemetryNoise, + seedTestMemories, + cleanupTestUser, +} from "./helpers"; + +jest.setTimeout(120_000); + +const TEST_USER_ID = `integration-search-${randomUUID()}`; + +describeIntegration("MemoryClient Integration — Search & History", () => { + let client: MemoryClient; + let cleanup: () => void; + let memoryIds: string[] = []; + + beforeAll(async () => { + cleanup = suppressTelemetryNoise(); + client = createTestClient(); + memoryIds = await seedTestMemories(client, TEST_USER_ID); + }); + + afterAll(async () => { + await cleanupTestUser(client, TEST_USER_ID); + cleanup(); + }); + + // ─── 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, + }); + + expect(Array.isArray(results)).toBe(true); + expect(results.length).toBeGreaterThan(0); + + const first = results[0]; + expect(typeof first.id).toBe("string"); + expect(typeof first.memory).toBe("string"); + expect(typeof first.score).toBe("number"); + expect(first.score).toBeGreaterThan(0); + }); + }); + + // ─── 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 }], + }, + api_version: "v2", + }); + + expect(Array.isArray(results)).toBe(true); + expect(results.length).toBeGreaterThan(0); + + const first = results[0]; + expect(typeof first.id).toBe("string"); + expect(typeof first.memory).toBe("string"); + expect(typeof first.score).toBe("number"); + }); + }); + + // ─── History ────────────────────────────────────────────── + describe("memory history", () => { + test("returns history with at least an ADD event", async () => { + const memoryId = memoryIds[0]; + const history = await client.history(memoryId); + + expect(Array.isArray(history)).toBe(true); + expect(history.length).toBeGreaterThanOrEqual(1); + + const entry = history[0]; + expect(typeof entry.id).toBe("string"); + expect(typeof entry.memory_id).toBe("string"); + expect(["ADD", "UPDATE", "DELETE", "NOOP"]).toContain(entry.event); + expect(new Date(entry.created_at).toString()).not.toBe("Invalid Date"); + expect(new Date(entry.updated_at).toString()).not.toBe("Invalid Date"); + expect( + entry.new_memory === null || typeof entry.new_memory === "string", + ).toBe(true); + expect( + entry.old_memory === null || typeof entry.old_memory === "string", + ).toBe(true); + + const events = history.map((h) => h.event); + expect(events).toContain("ADD"); + }); + }); +});