From 6663b738d5aca68945ac8987bd505723908a4463 Mon Sep 17 00:00:00 2001 From: Kartik Date: Thu, 19 Mar 2026 22:26:17 +0530 Subject: [PATCH] refactor: fix webhook create/update serialization, add payload types, and MEMORY_CATEGORIZED event (#4429) Co-authored-by: Saket Aryan --- docs/changelog.mdx | 31 +++ mem0-ts/jest.integration.config.js | 3 +- mem0-ts/package.json | 2 +- mem0-ts/src/client/index.ts | 3 +- mem0-ts/src/client/mem0.ts | 26 ++- mem0-ts/src/client/mem0.types.ts | 14 +- .../src/client/tests/integration/crud.test.ts | 36 +-- .../src/client/tests/integration/helpers.ts | 33 ++- .../tests/integration/management.test.ts | 121 +++++++++- .../tests/memoryClient.webhooks.test.ts | 219 +++++++++++------- pyproject.toml | 2 +- 11 files changed, 362 insertions(+), 128 deletions(-) diff --git a/docs/changelog.mdx b/docs/changelog.mdx index 153f37090..2b2b7e1f0 100644 --- a/docs/changelog.mdx +++ b/docs/changelog.mdx @@ -7,6 +7,23 @@ mode: "wide" + + +**Bug Fixes:** +- **Core:** Fixed control characters in LLM JSON responses causing parse failures (#4420) +- **Core:** Replaced hardcoded US/Pacific timezone references with `timezone.utc` (#4404) +- **Core:** Preserved `http_auth` in `_safe_deepcopy_config` for OpenSearch (#4418) +- **Core:** Normalized malformed LLM fact output before embedding (#4224) +- **Embeddings:** Pass `encoding_format='float'` in OpenAI embeddings for proxy compatibility (#4058) +- **LLMs:** Fixed Ollama to pass tools to `client.chat` and parse `tool_calls` from response (#4176) +- **Reranker:** Support nested LLM config in `LLMReranker` for non-OpenAI providers (#4405) +- **Vector Stores:** Cast `vector_distance` to float in Redis search (#4377) + +**Improvements:** +- **Embeddings:** Improved Ollama embedder with model name normalization and error handling (#4403) + + + **Bug Fixes:** @@ -745,6 +762,20 @@ mode: "wide" + + +**Bug Fixes:** +- **Client:** Fixed webhook `createWebhook` and `updateWebhook` API serialization +- **Client:** Added missing `MEMORY_CATEGORIZED` event type to `WebhookEvent` enum +- **Types:** Added `WebhookCreatePayload` and `WebhookUpdatePayload` for better type safety + +**Tests:** +- Added end-to-end unit test coverage for the platform client — CRUD, batch, search, webhooks, users, project, and initialization (#4357) +- Added real API integration tests for memory CRUD, batch operations, search, user management, project configuration, and webhook lifecycle (#4395) +- Deleted obsolete e2e test files replaced by the new structured test suite (#4419) + + + **Bug Fixes:** diff --git a/mem0-ts/jest.integration.config.js b/mem0-ts/jest.integration.config.js index 50eb8e21e..4ddf4b291 100644 --- a/mem0-ts/jest.integration.config.js +++ b/mem0-ts/jest.integration.config.js @@ -2,6 +2,7 @@ module.exports = { ...require("./jest.config"), testMatch: ["**/integration/**/*.test.ts"], - globalSetup: "/src/client/tests/integration/global-setup.ts", globalTeardown: "/src/client/tests/integration/global-teardown.ts", + // Run integration tests serially to avoid rate limiting and race conditions + maxWorkers: 1, }; diff --git a/mem0-ts/package.json b/mem0-ts/package.json index 8c9e6012e..8f94c30c7 100644 --- a/mem0-ts/package.json +++ b/mem0-ts/package.json @@ -1,6 +1,6 @@ { "name": "mem0ai", - "version": "2.4.1", + "version": "2.4.2", "description": "The Memory Layer For Your AI Apps", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/mem0-ts/src/client/index.ts b/mem0-ts/src/client/index.ts index d39f068be..b6d9be766 100644 --- a/mem0-ts/src/client/index.ts +++ b/mem0-ts/src/client/index.ts @@ -12,7 +12,8 @@ export type { PromptUpdatePayload, SearchOptions, Webhook, - WebhookPayload, + WebhookCreatePayload, + WebhookUpdatePayload, Messages, Message, AllUsers, diff --git a/mem0-ts/src/client/mem0.ts b/mem0-ts/src/client/mem0.ts index a67d59e2d..1c48f6e23 100644 --- a/mem0-ts/src/client/mem0.ts +++ b/mem0-ts/src/client/mem0.ts @@ -10,7 +10,8 @@ import { PromptUpdatePayload, SearchOptions, Webhook, - WebhookPayload, + WebhookCreatePayload, + WebhookUpdatePayload, Message, FeedbackPayload, CreateMemoryExportPayload, @@ -671,33 +672,40 @@ export default class MemoryClient { return response; } - async createWebhook(webhook: WebhookPayload): Promise { + async createWebhook(webhook: WebhookCreatePayload): Promise { if (this.telemetryId === "") await this.ping(); this._captureEvent("create_webhook", []); + const body = { + name: webhook.name, + url: webhook.url, + event_types: webhook.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(body), }, ); return response; } - async updateWebhook(webhook: WebhookPayload): Promise<{ message: string }> { + async updateWebhook( + webhook: WebhookUpdatePayload, + ): Promise<{ message: string }> { if (this.telemetryId === "") await this.ping(); this._captureEvent("update_webhook", []); - const project_id = webhook.projectId || this.projectId; + const body: Record = {}; + if (webhook.name != null) body.name = webhook.name; + if (webhook.url != null) body.url = webhook.url; + if (webhook.eventTypes != null) body.event_types = webhook.eventTypes; const response = await this._fetchWithErrorHandling( `${this.host}/api/v1/webhooks/${webhook.webhookId}/`, { method: "PUT", headers: this.headers, - body: JSON.stringify({ - ...webhook, - projectId: project_id, - }), + body: JSON.stringify(body), }, ); return response; diff --git a/mem0-ts/src/client/mem0.types.ts b/mem0-ts/src/client/mem0.types.ts index 0b568a9ff..fa0fd1ed6 100644 --- a/mem0-ts/src/client/mem0.types.ts +++ b/mem0-ts/src/client/mem0.types.ts @@ -174,6 +174,7 @@ export enum WebhookEvent { MEMORY_ADDED = "memory_add", MEMORY_UPDATED = "memory_update", MEMORY_DELETED = "memory_delete", + MEMORY_CATEGORIZED = "memory_categorize", } export interface Webhook { @@ -187,12 +188,17 @@ export interface Webhook { event_types?: WebhookEvent[]; } -export interface WebhookPayload { - eventTypes: WebhookEvent[]; - projectId: string; - webhookId: string; +export interface WebhookCreatePayload { name: string; url: string; + eventTypes: WebhookEvent[]; +} + +export interface WebhookUpdatePayload { + webhookId: string; + name?: string; + url?: string; + eventTypes?: WebhookEvent[]; } export interface FeedbackPayload { diff --git a/mem0-ts/src/client/tests/integration/crud.test.ts b/mem0-ts/src/client/tests/integration/crud.test.ts index 8600d6571..5cd5421cf 100644 --- a/mem0-ts/src/client/tests/integration/crud.test.ts +++ b/mem0-ts/src/client/tests/integration/crud.test.ts @@ -177,23 +177,6 @@ describeIntegration("MemoryClient Integration — CRUD", () => { }); }); - // ─── 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); - }); - }); - // ─── Edge cases ────────────────────────────────────────── describe("edge cases", () => { test("add with metadata attaches metadata to the memory", async () => { @@ -234,6 +217,25 @@ describeIntegration("MemoryClient Integration — CRUD", () => { }); }); + // ─── Delete single ──────────────────────────────────────── + // NOTE: Delete tests run last to avoid race conditions with + // other tests that depend on the seeded memories. + 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 () => { diff --git a/mem0-ts/src/client/tests/integration/helpers.ts b/mem0-ts/src/client/tests/integration/helpers.ts index d74156e7d..df4378b33 100644 --- a/mem0-ts/src/client/tests/integration/helpers.ts +++ b/mem0-ts/src/client/tests/integration/helpers.ts @@ -51,43 +51,56 @@ export async function withRetry( * 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. + * + * Polls every 15 seconds with a maximum of 4 retries to avoid + * hitting rate limits. Throws if results aren't available after + * all retries. */ export async function waitForMemories( client: MemoryClient, userId: string, minCount: number, - maxWaitMs = 60_000, + maxRetries = 4, ): Promise { - const start = Date.now(); - while (Date.now() - start < maxWaitMs) { + for (let attempt = 1; attempt <= maxRetries; attempt++) { const memories = await withRetry(() => client.getAll({ user_id: userId })); if (Array.isArray(memories) && memories.length >= minCount) { return memories; } - await new Promise((r) => setTimeout(r, 3_000)); + if (attempt < maxRetries) { + await new Promise((r) => setTimeout(r, 15_000)); + } } - return await client.getAll({ user_id: userId }); + throw new Error( + `waitForMemories: expected at least ${minCount} memories for user "${userId}" but did not get them after ${maxRetries} attempts`, + ); } /** * Poll search until results appear. Only used by search tests — * other test files should NOT call this to avoid wasting API credits. + * + * Polls every 15 seconds with a maximum of 4 retries. Throws if + * no results are found after all retries. */ export async function waitForSearchResults( client: MemoryClient, query: string, options: Record, - maxWaitMs = 60_000, + maxRetries = 4, ): Promise { - const start = Date.now(); - while (Date.now() - start < maxWaitMs) { + for (let attempt = 1; attempt <= maxRetries; attempt++) { const results = await withRetry(() => client.search(query, options)); if (Array.isArray(results) && results.length > 0) { return results; } - await new Promise((r) => setTimeout(r, 3_000)); + if (attempt < maxRetries) { + await new Promise((r) => setTimeout(r, 15_000)); + } } - return await client.search(query, options); + throw new Error( + `waitForSearchResults: no results for query "${query}" after ${maxRetries} attempts`, + ); } /** diff --git a/mem0-ts/src/client/tests/integration/management.test.ts b/mem0-ts/src/client/tests/integration/management.test.ts index 95df93b6f..dc79ff86e 100644 --- a/mem0-ts/src/client/tests/integration/management.test.ts +++ b/mem0-ts/src/client/tests/integration/management.test.ts @@ -1,15 +1,12 @@ /** - * Integration tests: User management and project configuration. + * Integration tests: User management, project configuration, and webhooks. * - * Tests users(), getProject(), and updateProject() against the real API. - * - * Note: Webhook tests (createWebhook, updateWebhook) are excluded because - * the SDK has a known bug where it sends camelCase keys (eventTypes) instead - * of snake_case (event_types). These will be added once the SDK is fixed. + * Tests users(), getProject(), updateProject(), and webhook CRUD against the real API. * * Run: MEM0_API_KEY=your-key npx jest management.test.ts --forceExit */ import { MemoryClient } from "../../mem0"; +import { WebhookEvent } from "../../mem0.types"; import { randomUUID } from "crypto"; import { describeIntegration, @@ -17,6 +14,7 @@ import { suppressTelemetryNoise, seedTestMemories, cleanupTestUser, + withRetry, } from "./helpers"; jest.setTimeout(120_000); @@ -95,4 +93,115 @@ describeIntegration("MemoryClient Integration — Users & Project", () => { }); }); }); + + // ─── Webhooks ────────────────────────────────────────────── + describe("webhook management", () => { + let createdWebhookId: string; + const hookName = `test-hook-${randomUUID().slice(0, 8)}`; + const hookUrl = `https://example.com/webhook/${randomUUID().slice(0, 8)}`; + const updatedName = `updated-hook-${randomUUID().slice(0, 8)}`; + + afterAll(async () => { + if (createdWebhookId) { + try { + await client.deleteWebhook({ webhookId: createdWebhookId }); + } catch { + // ignore — may already be deleted + } + } + }); + + // ─── Create ──────────────────────────────────────────── + test("createWebhook returns a webhook_id", async () => { + const result = await withRetry(() => + client.createWebhook({ + name: hookName, + url: hookUrl, + eventTypes: [WebhookEvent.MEMORY_ADDED, WebhookEvent.MEMORY_UPDATED], + }), + ); + createdWebhookId = result.webhook_id!; + expect(result.webhook_id).toBeDefined(); + }); + + test("createWebhook returns the correct name", async () => { + const webhooks = await withRetry(() => client.getWebhooks()); + const wh = webhooks.find((w) => w.webhook_id === createdWebhookId); + expect(wh!.name).toBe(hookName); + }); + + test("createWebhook returns the correct url", async () => { + const webhooks = await withRetry(() => client.getWebhooks()); + const wh = webhooks.find((w) => w.webhook_id === createdWebhookId); + expect(wh!.url).toBe(hookUrl); + }); + + test("createWebhook returns the correct event_types", async () => { + const webhooks = await withRetry(() => client.getWebhooks()); + const wh = webhooks.find((w) => w.webhook_id === createdWebhookId); + expect(wh!.event_types?.sort()).toStrictEqual( + [WebhookEvent.MEMORY_ADDED, WebhookEvent.MEMORY_UPDATED].sort(), + ); + }); + + // ─── List ────────────────────────────────────────────── + test("getWebhooks returns an array", async () => { + const webhooks = await withRetry(() => client.getWebhooks()); + expect(Array.isArray(webhooks)).toBe(true); + }); + + test("getWebhooks includes the created webhook", async () => { + const webhooks = await withRetry(() => client.getWebhooks()); + const found = webhooks.find((w) => w.webhook_id === createdWebhookId); + expect(found).toBeDefined(); + }); + + test("getWebhooks shows the webhook as active", async () => { + const webhooks = await withRetry(() => client.getWebhooks()); + const found = webhooks.find((w) => w.webhook_id === createdWebhookId); + expect(found!.is_active).toBe(true); + }); + + // ─── Update ──────────────────────────────────────────── + test("updateWebhook returns a success message", async () => { + const result = await withRetry(() => + client.updateWebhook({ + webhookId: createdWebhookId, + name: updatedName, + url: "https://example.com/updated", + eventTypes: [WebhookEvent.MEMORY_DELETED], + }), + ); + expect(result.message).toBeDefined(); + }); + + test("updateWebhook persists the new name", async () => { + const webhooks = await withRetry(() => client.getWebhooks()); + const updated = webhooks.find((w) => w.webhook_id === createdWebhookId); + expect(updated!.name).toBe(updatedName); + }); + + test("updateWebhook persists the new event_types", async () => { + const webhooks = await withRetry(() => client.getWebhooks()); + const updated = webhooks.find((w) => w.webhook_id === createdWebhookId); + expect(updated!.event_types?.sort()).toStrictEqual( + [WebhookEvent.MEMORY_DELETED].sort(), + ); + }); + + // ─── Delete ──────────────────────────────────────────── + test("deleteWebhook returns a response", async () => { + const result = await withRetry(() => + client.deleteWebhook({ webhookId: createdWebhookId }), + ); + expect(result).toBeDefined(); + }); + + test("deleteWebhook removes the webhook from the list", async () => { + const webhooks = await withRetry(() => client.getWebhooks()); + const found = webhooks.find((w) => w.webhook_id === createdWebhookId); + expect(found).toBeUndefined(); + createdWebhookId = ""; + }); + }); }); diff --git a/mem0-ts/src/client/tests/memoryClient.webhooks.test.ts b/mem0-ts/src/client/tests/memoryClient.webhooks.test.ts index 6b053b2c0..6e4de484f 100644 --- a/mem0-ts/src/client/tests/memoryClient.webhooks.test.ts +++ b/mem0-ts/src/client/tests/memoryClient.webhooks.test.ts @@ -1,6 +1,7 @@ /** * MemoryClient unit tests — getWebhooks, createWebhook, updateWebhook, deleteWebhook. - * Tests verify request URL and HTTP method, not mock response values. + * Tests verify request URL, HTTP method, and payload serialization. + * One expect per test case. */ import { MemoryClient } from "../mem0"; import { WebhookEvent } from "../mem0.types"; @@ -14,17 +15,26 @@ import { installConsoleSuppression(); -describe("MemoryClient - Webhooks", () => { - test("getWebhooks sends GET to /api/v1/webhooks/projects/:id/", async () => { +// ─── Helpers ────────────────────────────────────────────── +function webhookMock(extra?: Map) { + return setupMockFetch(extra); +} + +function createClient() { + return new MemoryClient({ + apiKey: TEST_API_KEY, + organizationId: TEST_ORG_ID, + projectId: TEST_PROJECT_ID, + }); +} + +// ─── getWebhooks ────────────────────────────────────────── +describe("MemoryClient - getWebhooks", () => { + test("sends GET to /api/v1/webhooks/projects/:id/", async () => { const extra = new Map(); extra.set("/api/v1/webhooks/projects/", { status: 200, body: [] }); - const mock = setupMockFetch(extra); - - const client = new MemoryClient({ - apiKey: TEST_API_KEY, - organizationId: TEST_ORG_ID, - projectId: TEST_PROJECT_ID, - }); + const mock = webhookMock(extra); + const client = createClient(); await client.getWebhooks(); const call = mock.mock.calls.find( @@ -33,117 +43,170 @@ describe("MemoryClient - Webhooks", () => { ); expect(call).toBeDefined(); }); +}); - test("createWebhook sends POST to /api/v1/webhooks/projects/:id/", async () => { +// ─── createWebhook ──────────────────────────────────────── +describe("MemoryClient - createWebhook", () => { + async function callCreate() { const extra = new Map(); extra.set("/api/v1/webhooks/projects/", { status: 200, body: { webhook_id: "wh_new" }, }); - const mock = setupMockFetch(extra); - - const client = new MemoryClient({ - apiKey: TEST_API_KEY, - organizationId: TEST_ORG_ID, - projectId: TEST_PROJECT_ID, - }); + const mock = webhookMock(extra); + const client = createClient(); await client.createWebhook({ name: "new-hook", url: "https://example.com", eventTypes: [WebhookEvent.MEMORY_ADDED], - projectId: TEST_PROJECT_ID, - webhookId: "", }); + return mock; + } + test("sends POST to /api/v1/webhooks/projects/:id/", async () => { + const mock = await callCreate(); expect(findFetchCall(mock, "/api/v1/webhooks/", "POST")).toBeDefined(); }); - test("createWebhook includes webhook payload in body", async () => { - const extra = new Map(); - extra.set("/api/v1/webhooks/projects/", { - status: 200, - body: { webhook_id: "wh_new" }, - }); - const mock = setupMockFetch(extra); - - const client = new MemoryClient({ - apiKey: TEST_API_KEY, - organizationId: TEST_ORG_ID, - projectId: TEST_PROJECT_ID, - }); - await client.createWebhook({ - name: "new-hook", - url: "https://example.com", - eventTypes: [WebhookEvent.MEMORY_ADDED], - projectId: TEST_PROJECT_ID, - webhookId: "", - }); - - const call = findFetchCall(mock, "/api/v1/webhooks/", "POST"); - const body = getFetchBody(call!); + test("body contains name", async () => { + const mock = await callCreate(); + const body = getFetchBody( + findFetchCall(mock, "/api/v1/webhooks/", "POST")!, + ); expect(body.name).toBe("new-hook"); + }); + + test("body contains url", async () => { + const mock = await callCreate(); + const body = getFetchBody( + findFetchCall(mock, "/api/v1/webhooks/", "POST")!, + ); expect(body.url).toBe("https://example.com"); }); - test("updateWebhook sends PUT to /api/v1/webhooks/:id/", async () => { + test("body contains event_types in snake_case", async () => { + const mock = await callCreate(); + const body = getFetchBody( + findFetchCall(mock, "/api/v1/webhooks/", "POST")!, + ); + expect(body.event_types).toStrictEqual([WebhookEvent.MEMORY_ADDED]); + }); + + test("body does not contain camelCase eventTypes", async () => { + const mock = await callCreate(); + const body = getFetchBody( + findFetchCall(mock, "/api/v1/webhooks/", "POST")!, + ); + expect(body.eventTypes).toBeUndefined(); + }); + + test("body does not contain projectId", async () => { + const mock = await callCreate(); + const body = getFetchBody( + findFetchCall(mock, "/api/v1/webhooks/", "POST")!, + ); + expect(body.projectId).toBeUndefined(); + }); + + test("body does not contain webhookId", async () => { + const mock = await callCreate(); + const body = getFetchBody( + findFetchCall(mock, "/api/v1/webhooks/", "POST")!, + ); + expect(body.webhookId).toBeUndefined(); + }); +}); + +// ─── updateWebhook ──────────────────────────────────────── +describe("MemoryClient - updateWebhook", () => { + async function callUpdate() { const extra = new Map(); extra.set("/api/v1/webhooks/wh_1/", { status: 200, body: { message: "Webhook updated" }, }); - const mock = setupMockFetch(extra); - - const client = new MemoryClient({ - apiKey: TEST_API_KEY, - organizationId: TEST_ORG_ID, - projectId: TEST_PROJECT_ID, - }); + const mock = webhookMock(extra); + const client = createClient(); await client.updateWebhook({ webhookId: "wh_1", name: "updated-hook", url: "https://new-url.com", eventTypes: [WebhookEvent.MEMORY_ADDED], - projectId: TEST_PROJECT_ID, }); + return mock; + } + test("sends PUT to /api/v1/webhooks/:id/", async () => { + const mock = await callUpdate(); expect(findFetchCall(mock, "/api/v1/webhooks/wh_1/", "PUT")).toBeDefined(); }); - test("updateWebhook includes updated fields in body", async () => { - const extra = new Map(); - extra.set("/api/v1/webhooks/wh_1/", { - status: 200, - body: { message: "Webhook updated" }, - }); - const mock = setupMockFetch(extra); - - const client = new MemoryClient({ - apiKey: TEST_API_KEY, - organizationId: TEST_ORG_ID, - projectId: TEST_PROJECT_ID, - }); - await client.updateWebhook({ - webhookId: "wh_1", - name: "updated-hook", - url: "https://new-url.com", - eventTypes: [WebhookEvent.MEMORY_ADDED], - projectId: TEST_PROJECT_ID, - }); - - const call = findFetchCall(mock, "/api/v1/webhooks/wh_1/", "PUT"); - const body = getFetchBody(call!); + test("body contains name", async () => { + const mock = await callUpdate(); + const body = getFetchBody( + findFetchCall(mock, "/api/v1/webhooks/wh_1/", "PUT")!, + ); expect(body.name).toBe("updated-hook"); + }); + + test("body contains url", async () => { + const mock = await callUpdate(); + const body = getFetchBody( + findFetchCall(mock, "/api/v1/webhooks/wh_1/", "PUT")!, + ); expect(body.url).toBe("https://new-url.com"); }); - test("deleteWebhook sends DELETE to /api/v1/webhooks/:id/", async () => { + test("body contains event_types in snake_case", async () => { + const mock = await callUpdate(); + const body = getFetchBody( + findFetchCall(mock, "/api/v1/webhooks/wh_1/", "PUT")!, + ); + expect(body.event_types).toStrictEqual([WebhookEvent.MEMORY_ADDED]); + }); + + test("body does not contain camelCase eventTypes", async () => { + const mock = await callUpdate(); + const body = getFetchBody( + findFetchCall(mock, "/api/v1/webhooks/wh_1/", "PUT")!, + ); + expect(body.eventTypes).toBeUndefined(); + }); + + test("body does not contain project_id", async () => { + const mock = await callUpdate(); + const body = getFetchBody( + findFetchCall(mock, "/api/v1/webhooks/wh_1/", "PUT")!, + ); + expect(body.project_id).toBeUndefined(); + }); + + test("body does not contain projectId", async () => { + const mock = await callUpdate(); + const body = getFetchBody( + findFetchCall(mock, "/api/v1/webhooks/wh_1/", "PUT")!, + ); + expect(body.projectId).toBeUndefined(); + }); + + test("body does not contain webhookId", async () => { + const mock = await callUpdate(); + const body = getFetchBody( + findFetchCall(mock, "/api/v1/webhooks/wh_1/", "PUT")!, + ); + expect(body.webhookId).toBeUndefined(); + }); +}); + +// ─── deleteWebhook ──────────────────────────────────────── +describe("MemoryClient - deleteWebhook", () => { + test("sends DELETE to /api/v1/webhooks/:id/", async () => { const extra = new Map(); extra.set("/api/v1/webhooks/wh_1/", { status: 200, body: { message: "Webhook deleted" }, }); - const mock = setupMockFetch(extra); - + const mock = webhookMock(extra); const client = new MemoryClient({ apiKey: TEST_API_KEY }); await client.deleteWebhook({ webhookId: "wh_1" }); diff --git a/pyproject.toml b/pyproject.toml index 90a72a432..036819381 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mem0ai" -version = "1.0.6" +version = "1.0.7" description = "Long-term memory for AI Agents" authors = [ { name = "Mem0", email = "founders@mem0.ai" }