refactor: fix webhook create/update serialization, add payload types, and MEMORY_CATEGORIZED event (#4429)
Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
This commit is contained in:
@@ -7,6 +7,23 @@ mode: "wide"
|
||||
<Tabs>
|
||||
<Tab title="Python">
|
||||
|
||||
<Update label="2026-03-19" description="v1.0.7">
|
||||
|
||||
**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)
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-03-16" description="v1.0.6">
|
||||
|
||||
**Bug Fixes:**
|
||||
@@ -745,6 +762,20 @@ mode: "wide"
|
||||
|
||||
<Tab title="TypeScript">
|
||||
|
||||
<Update label="2026-03-19" description="v2.4.2">
|
||||
|
||||
**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)
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-03-16" description="v2.4.1">
|
||||
|
||||
**Bug Fixes:**
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
module.exports = {
|
||||
...require("./jest.config"),
|
||||
testMatch: ["**/integration/**/*.test.ts"],
|
||||
globalSetup: "<rootDir>/src/client/tests/integration/global-setup.ts",
|
||||
globalTeardown: "<rootDir>/src/client/tests/integration/global-teardown.ts",
|
||||
// Run integration tests serially to avoid rate limiting and race conditions
|
||||
maxWorkers: 1,
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -12,7 +12,8 @@ export type {
|
||||
PromptUpdatePayload,
|
||||
SearchOptions,
|
||||
Webhook,
|
||||
WebhookPayload,
|
||||
WebhookCreatePayload,
|
||||
WebhookUpdatePayload,
|
||||
Messages,
|
||||
Message,
|
||||
AllUsers,
|
||||
|
||||
@@ -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<Webhook> {
|
||||
async createWebhook(webhook: WebhookCreatePayload): Promise<Webhook> {
|
||||
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<string, any> = {};
|
||||
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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -51,43 +51,56 @@ export async function withRetry<T>(
|
||||
* 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<Memory[]> {
|
||||
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<string, any>,
|
||||
maxWaitMs = 60_000,
|
||||
maxRetries = 4,
|
||||
): Promise<Memory[]> {
|
||||
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`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 = "";
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<string, { status: number; body: unknown }>) {
|
||||
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<string, { status: number; body: unknown }>();
|
||||
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<string, { status: number; body: unknown }>();
|
||||
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<string, { status: number; body: unknown }>();
|
||||
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<string, { status: number; body: unknown }>();
|
||||
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<string, { status: number; body: unknown }>();
|
||||
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<string, { status: number; body: unknown }>();
|
||||
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" });
|
||||
|
||||
|
||||
+1
-1
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user