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:
@@ -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 = "";
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user