chore: end to end test coverage for ts sdk (#4357)
This commit is contained in:
@@ -18,9 +18,26 @@ export type {
|
||||
AllUsers,
|
||||
User,
|
||||
FeedbackPayload,
|
||||
Feedback,
|
||||
} from "./mem0.types";
|
||||
|
||||
// Re-export enums as values (not type-only)
|
||||
export { Feedback, WebhookEvent } from "./mem0.types";
|
||||
|
||||
// Export the main client
|
||||
export { MemoryClient };
|
||||
export default MemoryClient;
|
||||
|
||||
// Export structured exceptions
|
||||
export {
|
||||
MemoryError,
|
||||
AuthenticationError,
|
||||
RateLimitError,
|
||||
ValidationError,
|
||||
MemoryNotFoundError,
|
||||
NetworkError,
|
||||
ConfigurationError,
|
||||
MemoryQuotaExceededError,
|
||||
createExceptionFromResponse,
|
||||
} from "../common/exceptions";
|
||||
|
||||
export type { MemoryErrorOptions } from "../common/exceptions";
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
GetMemoryExportPayload,
|
||||
} from "./mem0.types";
|
||||
import { captureClientEvent, generateHash } from "./telemetry";
|
||||
import { createExceptionFromResponse, MemoryError } from "../common/exceptions";
|
||||
|
||||
class APIError extends Error {
|
||||
constructor(message: string) {
|
||||
@@ -155,7 +156,7 @@ export default class MemoryClient {
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
throw new APIError(`API request failed: ${errorData}`);
|
||||
throw createExceptionFromResponse(response.status, errorData);
|
||||
}
|
||||
const jsonResponse = await response.json();
|
||||
return jsonResponse;
|
||||
@@ -200,8 +201,8 @@ export default class MemoryClient {
|
||||
if (project_id && !this.projectId) this.projectId = project_id;
|
||||
if (user_email) this.telemetryId = user_email;
|
||||
} catch (error: any) {
|
||||
// Convert generic errors to APIError with meaningful messages
|
||||
if (error instanceof APIError) {
|
||||
// Pass through structured exceptions and APIError
|
||||
if (error instanceof MemoryError || error instanceof APIError) {
|
||||
throw error;
|
||||
} else {
|
||||
throw new APIError(
|
||||
@@ -310,7 +311,7 @@ export default class MemoryClient {
|
||||
this._validateOrgProject();
|
||||
const payloadKeys = Object.keys(options || {});
|
||||
this._captureEvent("get_all", [payloadKeys]);
|
||||
const { api_version, page, page_size, ...otherOptions } = options!;
|
||||
const { api_version, page, page_size, ...otherOptions } = options ?? {};
|
||||
if (this.organizationName != null && this.projectName != null) {
|
||||
otherOptions.org_name = this.organizationName;
|
||||
otherOptions.project_name = this.projectName;
|
||||
@@ -361,7 +362,7 @@ export default class MemoryClient {
|
||||
this._validateOrgProject();
|
||||
const payloadKeys = Object.keys(options || {});
|
||||
this._captureEvent("search", [payloadKeys]);
|
||||
const { api_version, ...otherOptions } = options!;
|
||||
const { api_version, ...otherOptions } = options ?? {};
|
||||
const payload = { query, ...otherOptions };
|
||||
if (this.organizationName != null && this.projectName != null) {
|
||||
payload.org_name = this.organizationName;
|
||||
|
||||
@@ -170,7 +170,7 @@ export interface PromptUpdatePayload {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
enum WebhookEvent {
|
||||
export enum WebhookEvent {
|
||||
MEMORY_ADDED = "memory_add",
|
||||
MEMORY_UPDATED = "memory_update",
|
||||
MEMORY_DELETED = "memory_delete",
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Test helpers for MemoryClient unit tests.
|
||||
* Provides mock fetch, factory functions, and constants.
|
||||
*/
|
||||
|
||||
// ─── Mock Fetch ──────────────────────────────────────────
|
||||
|
||||
interface MockResponse {
|
||||
status: number;
|
||||
body: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock fetch function that matches URL patterns to responses.
|
||||
* Patterns are matched using string includes, sorted longest-first
|
||||
* so more specific routes (e.g. /v1/memories/search/) win over
|
||||
* broader ones (e.g. /v1/memories/) regardless of insertion order.
|
||||
*/
|
||||
export function createMockFetch(
|
||||
responses: Map<string, MockResponse>,
|
||||
): jest.Mock {
|
||||
return jest.fn(
|
||||
async (url: string | URL | Request, _options?: RequestInit) => {
|
||||
const urlStr =
|
||||
typeof url === "string"
|
||||
? url
|
||||
: url instanceof URL
|
||||
? url.toString()
|
||||
: url.url;
|
||||
|
||||
// Sort patterns longest-first so specific routes match before broad ones
|
||||
const sortedPatterns = [...responses.entries()].sort(
|
||||
(a, b) => b[0].length - a[0].length,
|
||||
);
|
||||
|
||||
for (const [pattern, response] of sortedPatterns) {
|
||||
if (urlStr.includes(pattern)) {
|
||||
return {
|
||||
ok: response.status >= 200 && response.status < 300,
|
||||
status: response.status,
|
||||
statusText: response.status === 200 ? "OK" : "Error",
|
||||
json: async () => response.body,
|
||||
text: async () =>
|
||||
typeof response.body === "string"
|
||||
? response.body
|
||||
: JSON.stringify(response.body),
|
||||
} as Response;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: "Not Found",
|
||||
json: async () => ({ error: "Not found" }),
|
||||
text: async () => "Not found",
|
||||
} as Response;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Factory Functions ───────────────────────────────────
|
||||
|
||||
export interface MockMemory {
|
||||
id: string;
|
||||
memory?: string;
|
||||
data?: { memory: string } | null;
|
||||
event?: string;
|
||||
user_id?: string;
|
||||
agent_id?: string | null;
|
||||
app_id?: string | null;
|
||||
run_id?: string | null;
|
||||
hash?: string;
|
||||
categories?: string[];
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
score?: number;
|
||||
metadata?: Record<string, unknown> | null;
|
||||
owner?: string | null;
|
||||
}
|
||||
|
||||
export function createMockMemory(
|
||||
overrides: Partial<MockMemory> = {},
|
||||
): MockMemory {
|
||||
return {
|
||||
id: "mem_test_123",
|
||||
memory: "Test memory content",
|
||||
user_id: "user_test",
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-01T00:00:00Z",
|
||||
categories: [],
|
||||
metadata: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export interface MockMemoryHistory {
|
||||
id: string;
|
||||
memory_id: string;
|
||||
input: Array<{ role: string; content: string }>;
|
||||
old_memory: string | null;
|
||||
new_memory: string | null;
|
||||
user_id: string;
|
||||
categories: string[];
|
||||
event: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export function createMockMemoryHistory(
|
||||
overrides: Partial<MockMemoryHistory> = {},
|
||||
): MockMemoryHistory {
|
||||
return {
|
||||
id: "hist_test_123",
|
||||
memory_id: "mem_test_123",
|
||||
input: [{ role: "user", content: "test" }],
|
||||
old_memory: null,
|
||||
new_memory: "Test memory",
|
||||
user_id: "user_test",
|
||||
categories: [],
|
||||
event: "ADD",
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-01T00:00:00Z",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export interface MockUser {
|
||||
id: string;
|
||||
name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
total_memories: number;
|
||||
owner: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export function createMockUser(overrides: Partial<MockUser> = {}): MockUser {
|
||||
return {
|
||||
id: "user_123",
|
||||
name: "test_user",
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-01T00:00:00Z",
|
||||
total_memories: 5,
|
||||
owner: "owner_123",
|
||||
type: "user",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export interface MockAllUsers {
|
||||
count: number;
|
||||
results: MockUser[];
|
||||
next: string | null;
|
||||
previous: string | null;
|
||||
}
|
||||
|
||||
export function createMockAllUsers(users: MockUser[] = []): MockAllUsers {
|
||||
return {
|
||||
count: users.length,
|
||||
results: users,
|
||||
next: null,
|
||||
previous: null,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────
|
||||
|
||||
export const TEST_API_KEY = "test-api-key-12345";
|
||||
export const TEST_HOST = "https://api.test.mem0.ai";
|
||||
export const TEST_ORG_ID = "org_test_123";
|
||||
export const TEST_PROJECT_ID = "proj_test_456";
|
||||
|
||||
export const MOCK_PING_RESPONSE = {
|
||||
status: "ok",
|
||||
org_id: TEST_ORG_ID,
|
||||
project_id: TEST_PROJECT_ID,
|
||||
user_email: "test@example.com",
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a standard set of mock responses for common MemoryClient operations.
|
||||
* Returns a Map that can be extended with additional patterns before passing to createMockFetch.
|
||||
*/
|
||||
export function createStandardMockResponses(): Map<string, MockResponse> {
|
||||
const responses = new Map<string, MockResponse>();
|
||||
responses.set("/v1/ping/", { status: 200, body: MOCK_PING_RESPONSE });
|
||||
return responses;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* MemoryClient unit tests — batchUpdate, batchDelete.
|
||||
* Tests verify payload transformation (memoryId → memory_id, string → object).
|
||||
*/
|
||||
import { MemoryClient } from "../mem0";
|
||||
import { TEST_API_KEY } from "./helpers";
|
||||
import {
|
||||
setupMockFetch,
|
||||
findFetchCall,
|
||||
getFetchBody,
|
||||
installConsoleSuppression,
|
||||
} from "./setup";
|
||||
|
||||
installConsoleSuppression();
|
||||
|
||||
// ─── batchUpdate() ──────────────────────────────────────
|
||||
|
||||
describe("MemoryClient - batchUpdate()", () => {
|
||||
test("sends PUT to /v1/batch/", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/batch/", { status: 200, body: { message: "OK" } });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.batchUpdate([{ memoryId: "mem_1", text: "updated 1" }]);
|
||||
|
||||
expect(findFetchCall(mock, "/v1/batch/", "PUT")).toBeDefined();
|
||||
});
|
||||
|
||||
test("transforms memoryId to memory_id in request body", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/batch/", { status: 200, body: { message: "OK" } });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.batchUpdate([
|
||||
{ memoryId: "mem_1", text: "updated 1" },
|
||||
{ memoryId: "mem_2", text: "updated 2" },
|
||||
]);
|
||||
|
||||
const call = findFetchCall(mock, "/v1/batch/", "PUT");
|
||||
const body = getFetchBody(call!);
|
||||
expect(body.memories).toEqual([
|
||||
{ memory_id: "mem_1", text: "updated 1" },
|
||||
{ memory_id: "mem_2", text: "updated 2" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("handles empty array without crashing", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/batch/", { status: 200, body: { message: "OK" } });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.batchUpdate([]);
|
||||
|
||||
const call = findFetchCall(mock, "/v1/batch/", "PUT");
|
||||
expect(getFetchBody(call!).memories).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── batchDelete() ──────────────────────────────────────
|
||||
|
||||
describe("MemoryClient - batchDelete()", () => {
|
||||
test("sends DELETE to /v1/batch/", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/batch/", { status: 200, body: { message: "OK" } });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.batchDelete(["mem_1"]);
|
||||
|
||||
expect(findFetchCall(mock, "/v1/batch/", "DELETE")).toBeDefined();
|
||||
});
|
||||
|
||||
test("wraps string IDs into {memory_id} objects", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/batch/", { status: 200, body: { message: "OK" } });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.batchDelete(["mem_1", "mem_2", "mem_3"]);
|
||||
|
||||
const call = findFetchCall(mock, "/v1/batch/", "DELETE");
|
||||
expect(getFetchBody(call!).memories).toEqual([
|
||||
{ memory_id: "mem_1" },
|
||||
{ memory_id: "mem_2" },
|
||||
{ memory_id: "mem_3" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("handles empty array without crashing", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/batch/", { status: 200, body: { message: "OK" } });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.batchDelete([]);
|
||||
|
||||
const call = findFetchCall(mock, "/v1/batch/", "DELETE");
|
||||
expect(getFetchBody(call!).memories).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,380 @@
|
||||
/**
|
||||
* MemoryClient unit tests — add, get, getAll, update, delete, deleteAll, history.
|
||||
* Tests verify request construction, not mock response echo.
|
||||
*/
|
||||
import { MemoryClient } from "../mem0";
|
||||
import type { Memory, MemoryHistory } from "../mem0.types";
|
||||
import {
|
||||
createMockMemory,
|
||||
createMockMemoryHistory,
|
||||
TEST_API_KEY,
|
||||
TEST_ORG_ID,
|
||||
TEST_PROJECT_ID,
|
||||
} from "./helpers";
|
||||
import {
|
||||
setupMockFetch,
|
||||
findFetchCall,
|
||||
getFetchBody,
|
||||
installConsoleSuppression,
|
||||
} from "./setup";
|
||||
|
||||
installConsoleSuppression();
|
||||
|
||||
// ─── add() ───────────────────────────────────────────────
|
||||
|
||||
describe("MemoryClient - add()", () => {
|
||||
test("sends POST to /v1/memories/", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/", { status: 200, body: [createMockMemory()] });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.add([{ role: "user", content: "Hello" }], { user_id: "u1" });
|
||||
|
||||
expect(findFetchCall(mock, "/v1/memories/", "POST")).toBeDefined();
|
||||
});
|
||||
|
||||
test("includes messages in request body", async () => {
|
||||
const messages = [{ role: "user" as const, content: "Hello, I am Alex" }];
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/", { status: 200, body: [createMockMemory()] });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.add(messages, { user_id: "u1" });
|
||||
|
||||
const call = findFetchCall(mock, "/v1/memories/", "POST");
|
||||
expect(getFetchBody(call!).messages).toEqual(messages);
|
||||
});
|
||||
|
||||
test("includes user_id in request body", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/", { status: 200, body: [createMockMemory()] });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.add([{ role: "user", content: "test" }], {
|
||||
user_id: "user_1",
|
||||
});
|
||||
|
||||
const call = findFetchCall(mock, "/v1/memories/", "POST");
|
||||
expect(getFetchBody(call!).user_id).toBe("user_1");
|
||||
});
|
||||
|
||||
test("attaches org_id from constructor to payload", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/", { status: 200, body: [createMockMemory()] });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({
|
||||
apiKey: TEST_API_KEY,
|
||||
organizationId: TEST_ORG_ID,
|
||||
projectId: TEST_PROJECT_ID,
|
||||
});
|
||||
await client.add([{ role: "user", content: "test" }], { user_id: "u1" });
|
||||
|
||||
const call = findFetchCall(mock, "/v1/memories/", "POST");
|
||||
const body = getFetchBody(call!);
|
||||
expect(body.org_id).toBe(TEST_ORG_ID);
|
||||
});
|
||||
|
||||
test("attaches project_id from constructor to payload", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/", { status: 200, body: [createMockMemory()] });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({
|
||||
apiKey: TEST_API_KEY,
|
||||
organizationId: TEST_ORG_ID,
|
||||
projectId: TEST_PROJECT_ID,
|
||||
});
|
||||
await client.add([{ role: "user", content: "test" }], { user_id: "u1" });
|
||||
|
||||
const call = findFetchCall(mock, "/v1/memories/", "POST");
|
||||
const body = getFetchBody(call!);
|
||||
expect(body.project_id).toBe(TEST_PROJECT_ID);
|
||||
});
|
||||
|
||||
test("sends empty messages array without crashing", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/", { status: 200, body: [] });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.add([], { user_id: "u1" });
|
||||
|
||||
const call = findFetchCall(mock, "/v1/memories/", "POST");
|
||||
expect(getFetchBody(call!).messages).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── get() ───────────────────────────────────────────────
|
||||
|
||||
describe("MemoryClient - get()", () => {
|
||||
test("sends GET to /v1/memories/:id/", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/mem_123/", {
|
||||
status: 200,
|
||||
body: createMockMemory(),
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.get("mem_123");
|
||||
|
||||
const call = mock.mock.calls.find(
|
||||
(c: [string, RequestInit]) =>
|
||||
c[0].includes("/v1/memories/mem_123/") && !c[1]?.method,
|
||||
);
|
||||
expect(call).toBeDefined();
|
||||
});
|
||||
|
||||
test("throws on 404 with error message from server", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/nonexistent/", {
|
||||
status: 404,
|
||||
body: "Memory not found",
|
||||
});
|
||||
setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await expect(client.get("nonexistent")).rejects.toThrow("Memory not found");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── getAll() ────────────────────────────────────────────
|
||||
|
||||
describe("MemoryClient - getAll()", () => {
|
||||
test("uses v2 POST endpoint when api_version=v2", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/memories/", { status: 200, body: [] });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.getAll({ user_id: "u1", api_version: "v2" });
|
||||
|
||||
expect(findFetchCall(mock, "/v2/memories/", "POST")).toBeDefined();
|
||||
});
|
||||
|
||||
test("uses v1 GET endpoint by default with user_id as query param", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/", { status: 200, body: [] });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.getAll({ user_id: "u1" });
|
||||
|
||||
const call = mock.mock.calls.find(
|
||||
(c: [string, RequestInit]) =>
|
||||
c[0].includes("/v1/memories/?") && !c[1]?.method,
|
||||
);
|
||||
expect(call).toBeDefined();
|
||||
expect(call![0]).toContain("user_id=u1");
|
||||
});
|
||||
|
||||
test("appends page and page_size to URL as query params", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/memories/", { status: 200, body: [] });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.getAll({
|
||||
user_id: "u1",
|
||||
api_version: "v2",
|
||||
page: 2,
|
||||
page_size: 25,
|
||||
});
|
||||
|
||||
const call = mock.mock.calls.find((c: [string, RequestInit]) =>
|
||||
c[0].includes("page="),
|
||||
);
|
||||
expect(call![0]).toContain("page=2");
|
||||
expect(call![0]).toContain("page_size=25");
|
||||
});
|
||||
|
||||
test("does not crash when called without options", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/", { status: 200, body: [] });
|
||||
setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
const result: Memory[] = await client.getAll();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── update() ────────────────────────────────────────────
|
||||
|
||||
describe("MemoryClient - update()", () => {
|
||||
test("sends PUT to /v1/memories/:id/ with text", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/mem_123/", {
|
||||
status: 200,
|
||||
body: createMockMemory(),
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.update("mem_123", { text: "Updated text" });
|
||||
|
||||
const call = findFetchCall(mock, "/v1/memories/mem_123/", "PUT");
|
||||
expect(call).toBeDefined();
|
||||
expect(getFetchBody(call!).text).toBe("Updated text");
|
||||
});
|
||||
|
||||
test("sends metadata in PUT body", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/mem_123/", {
|
||||
status: 200,
|
||||
body: createMockMemory(),
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.update("mem_123", { metadata: { priority: "high" } });
|
||||
|
||||
const call = findFetchCall(mock, "/v1/memories/mem_123/", "PUT");
|
||||
expect(getFetchBody(call!).metadata).toEqual({ priority: "high" });
|
||||
});
|
||||
|
||||
test("sends timestamp in PUT body", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/mem_123/", {
|
||||
status: 200,
|
||||
body: createMockMemory(),
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.update("mem_123", { timestamp: 1710600000 });
|
||||
|
||||
const call = findFetchCall(mock, "/v1/memories/mem_123/", "PUT");
|
||||
expect(getFetchBody(call!).timestamp).toBe(1710600000);
|
||||
});
|
||||
|
||||
test("includes all fields when text + metadata + timestamp provided", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/mem_123/", {
|
||||
status: 200,
|
||||
body: createMockMemory(),
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.update("mem_123", {
|
||||
text: "Updated",
|
||||
metadata: { source: "test" },
|
||||
timestamp: 1710600000,
|
||||
});
|
||||
|
||||
const call = findFetchCall(mock, "/v1/memories/mem_123/", "PUT");
|
||||
const body = getFetchBody(call!);
|
||||
expect(body.text).toBe("Updated");
|
||||
expect(body.metadata).toEqual({ source: "test" });
|
||||
expect(body.timestamp).toBe(1710600000);
|
||||
});
|
||||
|
||||
test("throws when no fields provided", async () => {
|
||||
setupMockFetch();
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await expect(client.update("mem_123", {})).rejects.toThrow(
|
||||
"At least one of text, metadata, or timestamp must be provided",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── delete() ────────────────────────────────────────────
|
||||
|
||||
describe("MemoryClient - delete()", () => {
|
||||
test("sends DELETE to /v1/memories/:id/", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/mem_123/", {
|
||||
status: 200,
|
||||
body: { message: "Memory deleted successfully" },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.delete("mem_123");
|
||||
|
||||
expect(
|
||||
findFetchCall(mock, "/v1/memories/mem_123/", "DELETE"),
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── deleteAll() ─────────────────────────────────────────
|
||||
|
||||
describe("MemoryClient - deleteAll()", () => {
|
||||
test("sends DELETE to /v1/memories/ with user_id as query param", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/", { status: 200, body: { message: "Deleted" } });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.deleteAll({ user_id: "u1" });
|
||||
|
||||
const call = mock.mock.calls.find(
|
||||
(c: [string, RequestInit]) =>
|
||||
c[0].includes("/v1/memories/?") && c[1]?.method === "DELETE",
|
||||
);
|
||||
expect(call).toBeDefined();
|
||||
expect(call![0]).toContain("user_id=u1");
|
||||
});
|
||||
|
||||
test("URL-encodes special characters in user_id", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/", { status: 200, body: { message: "Deleted" } });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.deleteAll({ user_id: "user@email.com" });
|
||||
|
||||
const call = mock.mock.calls.find(
|
||||
(c: [string, RequestInit]) =>
|
||||
c[0].includes("/v1/memories/?") && c[1]?.method === "DELETE",
|
||||
);
|
||||
expect(call).toBeDefined();
|
||||
expect(call![0]).toContain("user_id=");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── history() ───────────────────────────────────────────
|
||||
|
||||
describe("MemoryClient - history()", () => {
|
||||
test("sends GET to /v1/memories/:id/history/", async () => {
|
||||
const historyEntries = [
|
||||
createMockMemoryHistory({
|
||||
memory_id: "mem_123",
|
||||
event: "ADD",
|
||||
old_memory: null,
|
||||
new_memory: "I am Alex",
|
||||
}),
|
||||
];
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/mem_123/history/", {
|
||||
status: 200,
|
||||
body: historyEntries,
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.history("mem_123");
|
||||
|
||||
const call = mock.mock.calls.find(
|
||||
(c: [string, RequestInit]) =>
|
||||
c[0].includes("/v1/memories/mem_123/history/") && !c[1]?.method,
|
||||
);
|
||||
expect(call).toBeDefined();
|
||||
});
|
||||
|
||||
test("handles empty history without crashing", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/mem_123/history/", { status: 200, body: [] });
|
||||
setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
const result: MemoryHistory[] = await client.history("mem_123");
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,501 @@
|
||||
/**
|
||||
* MemoryClient E2E integration tests.
|
||||
*
|
||||
* These tests exercise realistic usage patterns with mock HTTP responses.
|
||||
* Skipped by default — run with MEM0_RUN_E2E=1 to enable.
|
||||
*
|
||||
* Run: MEM0_RUN_E2E=1 npx jest memoryClient.e2e.test.ts
|
||||
*/
|
||||
import { MemoryClient } from "../mem0";
|
||||
import type {
|
||||
Memory,
|
||||
AllUsers,
|
||||
MemoryHistory,
|
||||
User,
|
||||
Messages,
|
||||
} from "../mem0.types";
|
||||
import {
|
||||
createMockFetch,
|
||||
createMockMemory,
|
||||
createMockMemoryHistory,
|
||||
createMockUser,
|
||||
createMockAllUsers,
|
||||
TEST_API_KEY,
|
||||
MOCK_PING_RESPONSE,
|
||||
} from "./helpers";
|
||||
|
||||
const originalFetch = global.fetch;
|
||||
const originalConsoleError = console.error;
|
||||
const originalConsoleWarn = console.warn;
|
||||
|
||||
beforeAll(() => {
|
||||
jest.spyOn(console, "error").mockImplementation((...args: unknown[]) => {
|
||||
if (
|
||||
String(args[0] ?? "").match(
|
||||
/Telemetry|Failed to initialize|Failed to capture/,
|
||||
)
|
||||
)
|
||||
return;
|
||||
originalConsoleError(...args);
|
||||
});
|
||||
jest.spyOn(console, "warn").mockImplementation((...args: unknown[]) => {
|
||||
if (String(args[0] ?? "").match(/telemetry|Telemetry/)) return;
|
||||
originalConsoleWarn(...args);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => jest.restoreAllMocks());
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
});
|
||||
|
||||
// Shared test data matching realistic API responses
|
||||
const userId = "test_user_abc123";
|
||||
const memoryId = "mem_550e8400";
|
||||
|
||||
const mockMemory = createMockMemory({
|
||||
id: memoryId,
|
||||
memory: "Alex is a vegetarian",
|
||||
user_id: userId,
|
||||
event: "ADD",
|
||||
data: { memory: "Alex is a vegetarian" },
|
||||
categories: ["personal"],
|
||||
metadata: null,
|
||||
created_at: "2026-03-17T10:00:00Z",
|
||||
updated_at: "2026-03-17T10:00:00Z",
|
||||
score: 0.95,
|
||||
});
|
||||
|
||||
function mockFetchForTest(
|
||||
extraPatterns?: Record<string, { status: number; body: unknown }>,
|
||||
) {
|
||||
const responses = new Map<string, { status: number; body: unknown }>();
|
||||
responses.set("/v1/ping/", { status: 200, body: MOCK_PING_RESPONSE });
|
||||
responses.set("/v1/memories/search/", { status: 200, body: [mockMemory] });
|
||||
responses.set("/v2/memories/search/", { status: 200, body: [mockMemory] });
|
||||
responses.set("/history/", {
|
||||
status: 200,
|
||||
body: [
|
||||
createMockMemoryHistory({
|
||||
memory_id: memoryId,
|
||||
user_id: userId,
|
||||
event: "ADD",
|
||||
old_memory: null,
|
||||
new_memory: "Alex is a vegetarian",
|
||||
}),
|
||||
],
|
||||
});
|
||||
responses.set("/v1/entities/", {
|
||||
status: 200,
|
||||
body: createMockAllUsers([
|
||||
createMockUser({ id: "entity_1", name: userId, type: "user" }),
|
||||
]),
|
||||
});
|
||||
// This must come last — it's a broad pattern that matches /v1/memories/:id/ and /v1/memories/
|
||||
responses.set("/v1/memories/", { status: 200, body: [mockMemory] });
|
||||
|
||||
if (extraPatterns) {
|
||||
for (const [k, v] of Object.entries(extraPatterns)) {
|
||||
responses.set(k, v);
|
||||
}
|
||||
}
|
||||
|
||||
global.fetch = createMockFetch(responses);
|
||||
}
|
||||
|
||||
const describeOrSkip = process.env.MEM0_RUN_E2E ? describe : describe.skip;
|
||||
|
||||
describeOrSkip("MemoryClient API (E2E)", () => {
|
||||
beforeEach(() => mockFetchForTest());
|
||||
|
||||
const messages1 = [
|
||||
{ role: "user" as const, content: "Hey, I am Alex. I'm now a vegetarian." },
|
||||
{ role: "assistant" as const, content: "Hello Alex! Glad to hear!" },
|
||||
];
|
||||
|
||||
describe("add messages", () => {
|
||||
let res: Memory[];
|
||||
|
||||
beforeEach(async () => {
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
res = await client.add(messages1, { user_id: userId });
|
||||
});
|
||||
|
||||
test("returns an array", () => {
|
||||
expect(Array.isArray(res)).toBe(true);
|
||||
});
|
||||
|
||||
test("first message has a string id", () => {
|
||||
expect(typeof res[0].id).toBe("string");
|
||||
});
|
||||
|
||||
test("first message has a string data.memory", () => {
|
||||
expect(typeof res[0].data?.memory).toBe("string");
|
||||
});
|
||||
|
||||
test("first message has a string event", () => {
|
||||
expect(typeof res[0].event).toBe("string");
|
||||
});
|
||||
});
|
||||
|
||||
describe("retrieve specific memory by ID", () => {
|
||||
let memory: Memory;
|
||||
|
||||
beforeEach(async () => {
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
const memories = await client.getAll({ user_id: userId });
|
||||
memory = Array.isArray(memories) ? memories[0] : memories;
|
||||
});
|
||||
|
||||
test("returns string id", () => {
|
||||
expect(typeof memory.id).toBe("string");
|
||||
});
|
||||
|
||||
test("returns string memory content", () => {
|
||||
expect(typeof memory.memory).toBe("string");
|
||||
});
|
||||
|
||||
test("returns string user_id", () => {
|
||||
expect(typeof memory.user_id).toBe("string");
|
||||
});
|
||||
|
||||
test("user_id matches the requested userId", () => {
|
||||
expect(memory.user_id).toBe(userId);
|
||||
});
|
||||
|
||||
test("metadata is null or an object", () => {
|
||||
expect(
|
||||
memory.metadata === null || typeof memory.metadata === "object",
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("categories is an array or null", () => {
|
||||
expect(
|
||||
Array.isArray(memory.categories) || memory.categories === null,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("each category is a string", () => {
|
||||
if (Array.isArray(memory.categories)) {
|
||||
expect(
|
||||
memory.categories.every((c: string) => typeof c === "string"),
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("created_at is a valid date", () => {
|
||||
expect(new Date(memory.created_at || "").toString()).not.toBe(
|
||||
"Invalid Date",
|
||||
);
|
||||
});
|
||||
|
||||
test("updated_at is a valid date", () => {
|
||||
expect(new Date(memory.updated_at || "").toString()).not.toBe(
|
||||
"Invalid Date",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("retrieve all users", () => {
|
||||
let allUsers: AllUsers;
|
||||
|
||||
beforeEach(async () => {
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
allUsers = await client.users();
|
||||
});
|
||||
|
||||
test("count is a number", () => {
|
||||
expect(typeof allUsers.count).toBe("number");
|
||||
});
|
||||
|
||||
test("first user has a string id", () => {
|
||||
expect(typeof allUsers.results[0].id).toBe("string");
|
||||
});
|
||||
|
||||
test("first user has a string name", () => {
|
||||
expect(typeof allUsers.results[0].name).toBe("string");
|
||||
});
|
||||
|
||||
test("first user has a string created_at", () => {
|
||||
expect(typeof allUsers.results[0].created_at).toBe("string");
|
||||
});
|
||||
|
||||
test("first user has a string updated_at", () => {
|
||||
expect(typeof allUsers.results[0].updated_at).toBe("string");
|
||||
});
|
||||
|
||||
test("first user has a number total_memories", () => {
|
||||
expect(typeof allUsers.results[0].total_memories).toBe("number");
|
||||
});
|
||||
|
||||
test("first user has a string type", () => {
|
||||
expect(typeof allUsers.results[0].type).toBe("string");
|
||||
});
|
||||
|
||||
test("results contain an entity matching userId", () => {
|
||||
const entity = allUsers.results.find(
|
||||
(user: User) => user.name === userId,
|
||||
);
|
||||
expect(entity).not.toBeUndefined();
|
||||
});
|
||||
|
||||
test("matched entity has a string id", () => {
|
||||
const entity = allUsers.results.find(
|
||||
(user: User) => user.name === userId,
|
||||
);
|
||||
expect(typeof entity?.id).toBe("string");
|
||||
});
|
||||
});
|
||||
|
||||
describe("retrieve all memories for the user", () => {
|
||||
let memories: Memory[];
|
||||
let memory: Memory;
|
||||
|
||||
beforeEach(async () => {
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
memories = await client.getAll({ user_id: userId });
|
||||
memory = memories[0];
|
||||
});
|
||||
|
||||
test("returns an array", () => {
|
||||
expect(Array.isArray(memories)).toBe(true);
|
||||
});
|
||||
|
||||
test("first memory has a string id", () => {
|
||||
expect(typeof memory.id).toBe("string");
|
||||
});
|
||||
|
||||
test("first memory has a string memory content", () => {
|
||||
expect(typeof memory.memory).toBe("string");
|
||||
});
|
||||
|
||||
test("first memory has a string user_id", () => {
|
||||
expect(typeof memory.user_id).toBe("string");
|
||||
});
|
||||
|
||||
test("first memory user_id matches the requested userId", () => {
|
||||
expect(memory.user_id).toBe(userId);
|
||||
});
|
||||
|
||||
test("first memory metadata is null or an object", () => {
|
||||
expect(
|
||||
memory.metadata === null || typeof memory.metadata === "object",
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("first memory categories is an array or null", () => {
|
||||
expect(
|
||||
Array.isArray(memory.categories) || memory.categories === null,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("first memory created_at is a valid date", () => {
|
||||
expect(new Date(memory.created_at || "").toString()).not.toBe(
|
||||
"Invalid Date",
|
||||
);
|
||||
});
|
||||
|
||||
test("first memory updated_at is a valid date", () => {
|
||||
expect(new Date(memory.updated_at || "").toString()).not.toBe(
|
||||
"Invalid Date",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("search with API version 2", () => {
|
||||
let results: Memory[];
|
||||
let memory: Memory;
|
||||
|
||||
beforeEach(async () => {
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
results = await client.search("What do you know about me?", {
|
||||
filters: {
|
||||
OR: [{ user_id: userId }, { agent_id: "shopping-assistant" }],
|
||||
},
|
||||
threshold: 0.1,
|
||||
api_version: "v2",
|
||||
});
|
||||
memory = results[0];
|
||||
});
|
||||
|
||||
test("returns an array", () => {
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
});
|
||||
|
||||
test("first result has a string id", () => {
|
||||
expect(typeof memory.id).toBe("string");
|
||||
});
|
||||
|
||||
test("first result has a string memory content", () => {
|
||||
expect(typeof memory.memory).toBe("string");
|
||||
});
|
||||
|
||||
test("first result metadata is null or an object", () => {
|
||||
expect(
|
||||
memory.metadata === null || typeof memory.metadata === "object",
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("first result categories is an array or null", () => {
|
||||
expect(
|
||||
Array.isArray(memory.categories) || memory.categories === null,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("first result created_at is a valid date", () => {
|
||||
expect(new Date(memory.created_at || "").toString()).not.toBe(
|
||||
"Invalid Date",
|
||||
);
|
||||
});
|
||||
|
||||
test("first result has a number score", () => {
|
||||
expect(typeof memory.score).toBe("number");
|
||||
});
|
||||
});
|
||||
|
||||
describe("search with API version 1", () => {
|
||||
let results: Memory[];
|
||||
let memory: Memory;
|
||||
|
||||
beforeEach(async () => {
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
results = await client.search("What is my name?", {
|
||||
user_id: userId,
|
||||
});
|
||||
memory = results[0];
|
||||
});
|
||||
|
||||
test("returns an array", () => {
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
});
|
||||
|
||||
test("first result has a string id", () => {
|
||||
expect(typeof memory.id).toBe("string");
|
||||
});
|
||||
|
||||
test("first result has a string memory content", () => {
|
||||
expect(typeof memory.memory).toBe("string");
|
||||
});
|
||||
|
||||
test("first result has a string user_id", () => {
|
||||
expect(typeof memory.user_id).toBe("string");
|
||||
});
|
||||
|
||||
test("first result user_id matches the requested userId", () => {
|
||||
expect(memory.user_id).toBe(userId);
|
||||
});
|
||||
|
||||
test("first result has a number score", () => {
|
||||
expect(typeof memory.score).toBe("number");
|
||||
});
|
||||
});
|
||||
|
||||
describe("retrieve history of a specific memory", () => {
|
||||
let history: MemoryHistory[];
|
||||
let entry: MemoryHistory;
|
||||
|
||||
beforeEach(async () => {
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
history = await client.history(memoryId);
|
||||
entry = history[0];
|
||||
});
|
||||
|
||||
test("returns an array", () => {
|
||||
expect(Array.isArray(history)).toBe(true);
|
||||
});
|
||||
|
||||
test("first entry has a string id", () => {
|
||||
expect(typeof entry.id).toBe("string");
|
||||
});
|
||||
|
||||
test("first entry has a string memory_id", () => {
|
||||
expect(typeof entry.memory_id).toBe("string");
|
||||
});
|
||||
|
||||
test("first entry has a string user_id", () => {
|
||||
expect(typeof entry.user_id).toBe("string");
|
||||
});
|
||||
|
||||
test("first entry user_id matches the requested userId", () => {
|
||||
expect(entry.user_id).toBe(userId);
|
||||
});
|
||||
|
||||
test("old_memory is null or a string", () => {
|
||||
expect(
|
||||
entry.old_memory === null || typeof entry.old_memory === "string",
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("new_memory is null or a string", () => {
|
||||
expect(
|
||||
entry.new_memory === null || typeof entry.new_memory === "string",
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("created_at is a valid date", () => {
|
||||
expect(new Date(entry.created_at).toString()).not.toBe("Invalid Date");
|
||||
});
|
||||
|
||||
test("updated_at is a valid date", () => {
|
||||
expect(new Date(entry.updated_at).toString()).not.toBe("Invalid Date");
|
||||
});
|
||||
|
||||
test("event is one of ADD, UPDATE, DELETE, NOOP", () => {
|
||||
expect(["ADD", "UPDATE", "DELETE", "NOOP"]).toContain(entry.event);
|
||||
});
|
||||
|
||||
test("ADD event has null old_memory", () => {
|
||||
expect(entry.old_memory).toBeNull();
|
||||
});
|
||||
|
||||
test("ADD event has non-null new_memory", () => {
|
||||
expect(entry.new_memory).not.toBeNull();
|
||||
});
|
||||
|
||||
test("input is an array or null", () => {
|
||||
expect(Array.isArray(entry.input) || entry.input === null).toBe(true);
|
||||
});
|
||||
|
||||
test("each input item is an object", () => {
|
||||
if (Array.isArray(entry.input)) {
|
||||
expect(entry.input.every((i: Messages) => typeof i === "object")).toBe(
|
||||
true,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("each input item has a string content", () => {
|
||||
if (Array.isArray(entry.input)) {
|
||||
expect(
|
||||
entry.input.every((i: Messages) => typeof i.content === "string"),
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("each input item has a valid role", () => {
|
||||
if (Array.isArray(entry.input)) {
|
||||
expect(
|
||||
entry.input.every((i: Messages) =>
|
||||
["user", "assistant"].includes(i.role),
|
||||
),
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("delete user", () => {
|
||||
test("returns success message", async () => {
|
||||
const client = new MemoryClient({
|
||||
apiKey: TEST_API_KEY,
|
||||
organizationId: "org_test",
|
||||
projectId: "proj_test",
|
||||
});
|
||||
client.client.delete = jest.fn().mockResolvedValue({
|
||||
data: { message: "Entity deleted successfully!" },
|
||||
});
|
||||
|
||||
const result = await client.deleteUsers({ user_id: userId });
|
||||
expect(result.message).toBe("Entity deleted successfully.");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* MemoryClient unit tests — constructor, validation, ping.
|
||||
*/
|
||||
import { MemoryClient } from "../mem0";
|
||||
import {
|
||||
MemoryNotFoundError,
|
||||
ValidationError,
|
||||
MemoryError,
|
||||
} from "../../common/exceptions";
|
||||
import {
|
||||
createMockFetch,
|
||||
TEST_API_KEY,
|
||||
TEST_HOST,
|
||||
TEST_ORG_ID,
|
||||
TEST_PROJECT_ID,
|
||||
} from "./helpers";
|
||||
import {
|
||||
setupMockFetch,
|
||||
installConsoleSuppression,
|
||||
MOCK_PING_RESPONSE,
|
||||
} from "./setup";
|
||||
|
||||
installConsoleSuppression();
|
||||
|
||||
// ─── Initialization ──────────────────────────────────────
|
||||
|
||||
describe("MemoryClient - Initialization", () => {
|
||||
beforeEach(() => setupMockFetch());
|
||||
|
||||
test("throws when API key is empty string", () => {
|
||||
expect(() => new MemoryClient({ apiKey: "" })).toThrow(
|
||||
"Mem0 API key is required",
|
||||
);
|
||||
});
|
||||
|
||||
test("throws when API key is whitespace only", () => {
|
||||
expect(() => new MemoryClient({ apiKey: " " })).toThrow(
|
||||
"Mem0 API key cannot be empty",
|
||||
);
|
||||
});
|
||||
|
||||
test("throws when API key is not a string", () => {
|
||||
expect(
|
||||
() => new MemoryClient({ apiKey: 123 as unknown as string }),
|
||||
).toThrow("Mem0 API key must be a string");
|
||||
});
|
||||
|
||||
test("sets default host to https://api.mem0.ai", () => {
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
expect(client.host).toBe("https://api.mem0.ai");
|
||||
});
|
||||
|
||||
test("uses custom host when provided", () => {
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY, host: TEST_HOST });
|
||||
expect(client.host).toBe(TEST_HOST);
|
||||
});
|
||||
|
||||
test("sets organizationId from constructor", () => {
|
||||
const client = new MemoryClient({
|
||||
apiKey: TEST_API_KEY,
|
||||
organizationId: TEST_ORG_ID,
|
||||
projectId: TEST_PROJECT_ID,
|
||||
});
|
||||
expect(client.organizationId).toBe(TEST_ORG_ID);
|
||||
});
|
||||
|
||||
test("sets projectId from constructor", () => {
|
||||
const client = new MemoryClient({
|
||||
apiKey: TEST_API_KEY,
|
||||
organizationId: TEST_ORG_ID,
|
||||
projectId: TEST_PROJECT_ID,
|
||||
});
|
||||
expect(client.projectId).toBe(TEST_PROJECT_ID);
|
||||
});
|
||||
|
||||
test("sets Authorization header with Token prefix", () => {
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
expect(client.headers["Authorization"]).toBe(`Token ${TEST_API_KEY}`);
|
||||
});
|
||||
|
||||
test("creates axios client with 60s timeout", () => {
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
expect(client.client.defaults.timeout).toBe(60000);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Ping ────────────────────────────────────────────────
|
||||
|
||||
describe("MemoryClient - ping()", () => {
|
||||
test("sets organizationId from ping response", async () => {
|
||||
setupMockFetch();
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.ping();
|
||||
expect(client.organizationId).toBe(TEST_ORG_ID);
|
||||
});
|
||||
|
||||
test("sets projectId from ping response", async () => {
|
||||
setupMockFetch();
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.ping();
|
||||
expect(client.projectId).toBe(TEST_PROJECT_ID);
|
||||
});
|
||||
|
||||
test("sets telemetryId from user_email in response", async () => {
|
||||
setupMockFetch();
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.ping();
|
||||
expect(client.telemetryId).toBe("test@example.com");
|
||||
});
|
||||
|
||||
test("preserves constructor organizationId over ping response", async () => {
|
||||
setupMockFetch();
|
||||
const client = new MemoryClient({
|
||||
apiKey: TEST_API_KEY,
|
||||
organizationId: "my_org",
|
||||
projectId: "my_proj",
|
||||
});
|
||||
await client.ping();
|
||||
expect(client.organizationId).toBe("my_org");
|
||||
});
|
||||
|
||||
test("preserves constructor projectId over ping response", async () => {
|
||||
setupMockFetch();
|
||||
const client = new MemoryClient({
|
||||
apiKey: TEST_API_KEY,
|
||||
organizationId: "my_org",
|
||||
projectId: "my_proj",
|
||||
});
|
||||
await client.ping();
|
||||
expect(client.projectId).toBe("my_proj");
|
||||
});
|
||||
|
||||
test("throws AuthenticationError on 401 response", async () => {
|
||||
const { AuthenticationError } = await import("../../common/exceptions");
|
||||
const responses = new Map<string, { status: number; body: unknown }>();
|
||||
responses.set("/v1/ping/", {
|
||||
status: 401,
|
||||
body: "Invalid API key",
|
||||
});
|
||||
global.fetch = createMockFetch(responses);
|
||||
|
||||
const client = new MemoryClient({ apiKey: "bad-key" });
|
||||
await expect(client.ping()).rejects.toThrow(AuthenticationError);
|
||||
});
|
||||
|
||||
test("throws on invalid (non-object) response format", async () => {
|
||||
const responses = new Map<string, { status: number; body: unknown }>();
|
||||
responses.set("/v1/ping/", { status: 200, body: "not an object" });
|
||||
global.fetch = createMockFetch(responses);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await expect(client.ping()).rejects.toThrow("Invalid response format");
|
||||
});
|
||||
|
||||
test("throws on status !== ok in response", async () => {
|
||||
const responses = new Map<string, { status: number; body: unknown }>();
|
||||
responses.set("/v1/ping/", {
|
||||
status: 200,
|
||||
body: { status: "error", message: "API Key is invalid" },
|
||||
});
|
||||
global.fetch = createMockFetch(responses);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await expect(client.ping()).rejects.toThrow("API Key is invalid");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Error Handling ──────────────────────────────────────
|
||||
|
||||
describe("MemoryClient - Error Handling", () => {
|
||||
test("404 throws MemoryNotFoundError with server response text", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/gone/", { status: 404, body: "Memory not found" });
|
||||
setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await expect(client.get("gone")).rejects.toThrow(MemoryNotFoundError);
|
||||
await expect(client.get("gone")).rejects.toThrow("Memory not found");
|
||||
});
|
||||
|
||||
test("500 throws MemoryError with server response text", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/err/", {
|
||||
status: 500,
|
||||
body: "Internal server error",
|
||||
});
|
||||
setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await expect(client.get("err")).rejects.toThrow(MemoryError);
|
||||
await expect(client.get("err")).rejects.toThrow("Internal server error");
|
||||
});
|
||||
|
||||
test("400 throws ValidationError with details from server", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/bad/", {
|
||||
status: 400,
|
||||
body: "Invalid request: user_id is required",
|
||||
});
|
||||
setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await expect(client.get("bad")).rejects.toThrow(ValidationError);
|
||||
await expect(client.get("bad")).rejects.toThrow(
|
||||
"Invalid request: user_id is required",
|
||||
);
|
||||
});
|
||||
|
||||
test("Authorization header is included in fetch calls", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/mem_1/", {
|
||||
status: 200,
|
||||
body: { id: "mem_1" },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.get("mem_1");
|
||||
|
||||
const call = mock.mock.calls.find((c: [string, RequestInit]) =>
|
||||
c[0].includes("/v1/memories/mem_1/"),
|
||||
);
|
||||
const headers = call![1].headers as Record<string, string>;
|
||||
expect(headers["Authorization"]).toContain(TEST_API_KEY);
|
||||
});
|
||||
|
||||
test("network failure (fetch throws) is propagated", async () => {
|
||||
global.fetch = jest.fn(async (url: string | URL | Request) => {
|
||||
const urlStr = typeof url === "string" ? url : url.toString();
|
||||
if (urlStr.includes("/v1/memories/net_err/")) {
|
||||
throw new TypeError("Failed to fetch");
|
||||
}
|
||||
if (urlStr.includes("/v1/ping/")) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => MOCK_PING_RESPONSE,
|
||||
text: async () => JSON.stringify(MOCK_PING_RESPONSE),
|
||||
} as Response;
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
status: 404,
|
||||
text: async () => "Not found",
|
||||
} as Response;
|
||||
});
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await expect(client.get("net_err")).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* MemoryClient unit tests — getProject, updateProject, exports, feedback.
|
||||
* Tests verify request construction and validation behavior.
|
||||
*/
|
||||
import { MemoryClient } from "../mem0";
|
||||
import { Feedback } from "../mem0.types";
|
||||
import {
|
||||
createMockFetch,
|
||||
TEST_API_KEY,
|
||||
TEST_ORG_ID,
|
||||
TEST_PROJECT_ID,
|
||||
} from "./helpers";
|
||||
import {
|
||||
setupMockFetch,
|
||||
findFetchCall,
|
||||
getFetchBody,
|
||||
installConsoleSuppression,
|
||||
} from "./setup";
|
||||
|
||||
installConsoleSuppression();
|
||||
|
||||
// ─── getProject() ───────────────────────────────────────
|
||||
|
||||
describe("MemoryClient - getProject()", () => {
|
||||
test("throws when organizationId and projectId not set", async () => {
|
||||
const responses = new Map<string, { status: number; body: unknown }>();
|
||||
responses.set("/v1/ping/", { status: 200, body: { status: "ok" } });
|
||||
global.fetch = createMockFetch(responses);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
try {
|
||||
await client.ping();
|
||||
} catch {
|
||||
// ping might throw — but orgId stays null
|
||||
}
|
||||
|
||||
await expect(
|
||||
client.getProject({ fields: ["custom_instructions"] }),
|
||||
).rejects.toThrow("organizationId and projectId must be set");
|
||||
});
|
||||
|
||||
test("sends GET to /api/v1/orgs/organizations/:orgId/projects/:projId/", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/api/v1/orgs/organizations/", {
|
||||
status: 200,
|
||||
body: { custom_instructions: "Be helpful" },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({
|
||||
apiKey: TEST_API_KEY,
|
||||
organizationId: TEST_ORG_ID,
|
||||
projectId: TEST_PROJECT_ID,
|
||||
});
|
||||
await client.getProject({ fields: ["custom_instructions"] });
|
||||
|
||||
const call = mock.mock.calls.find(
|
||||
(c: [string, RequestInit]) =>
|
||||
c[0].includes("/api/v1/orgs/organizations/") && !c[1]?.method,
|
||||
);
|
||||
expect(call).toBeDefined();
|
||||
expect(call![0]).toContain("fields=custom_instructions");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── updateProject() ────────────────────────────────────
|
||||
|
||||
describe("MemoryClient - updateProject()", () => {
|
||||
test("sends PATCH to /api/v1/orgs/organizations/:orgId/projects/:projId/", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/api/v1/orgs/organizations/", {
|
||||
status: 200,
|
||||
body: { custom_instructions: "Updated" },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({
|
||||
apiKey: TEST_API_KEY,
|
||||
organizationId: TEST_ORG_ID,
|
||||
projectId: TEST_PROJECT_ID,
|
||||
});
|
||||
await client.updateProject({
|
||||
custom_instructions: "Updated instructions",
|
||||
});
|
||||
|
||||
const call = findFetchCall(mock, "/api/v1/orgs/organizations/", "PATCH");
|
||||
expect(call).toBeDefined();
|
||||
});
|
||||
|
||||
test("includes custom_instructions in PATCH body", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/api/v1/orgs/organizations/", {
|
||||
status: 200,
|
||||
body: { custom_instructions: "Updated" },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({
|
||||
apiKey: TEST_API_KEY,
|
||||
organizationId: TEST_ORG_ID,
|
||||
projectId: TEST_PROJECT_ID,
|
||||
});
|
||||
await client.updateProject({
|
||||
custom_instructions: "Updated instructions",
|
||||
});
|
||||
|
||||
const call = findFetchCall(mock, "/api/v1/orgs/organizations/", "PATCH");
|
||||
expect(getFetchBody(call!).custom_instructions).toBe(
|
||||
"Updated instructions",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── feedback() ─────────────────────────────────────────
|
||||
|
||||
describe("MemoryClient - feedback()", () => {
|
||||
test("sends POST to /v1/feedback/ with payload", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/feedback/", {
|
||||
status: 200,
|
||||
body: { message: "Feedback recorded" },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.feedback({
|
||||
memory_id: "mem_123",
|
||||
feedback: Feedback.POSITIVE,
|
||||
feedback_reason: "Very helpful",
|
||||
});
|
||||
|
||||
const call = findFetchCall(mock, "/v1/feedback/", "POST");
|
||||
expect(call).toBeDefined();
|
||||
});
|
||||
|
||||
test("includes memory_id, feedback, and reason in body", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/feedback/", {
|
||||
status: 200,
|
||||
body: { message: "Feedback recorded" },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.feedback({
|
||||
memory_id: "mem_123",
|
||||
feedback: Feedback.POSITIVE,
|
||||
feedback_reason: "Very helpful",
|
||||
});
|
||||
|
||||
const call = findFetchCall(mock, "/v1/feedback/", "POST");
|
||||
const body = getFetchBody(call!);
|
||||
expect(body.memory_id).toBe("mem_123");
|
||||
expect(body.feedback).toBe("POSITIVE");
|
||||
expect(body.feedback_reason).toBe("Very helpful");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Memory Exports ─────────────────────────────────────
|
||||
|
||||
describe("MemoryClient - Memory Exports", () => {
|
||||
test("createMemoryExport throws when missing filters or schema", async () => {
|
||||
setupMockFetch();
|
||||
const client = new MemoryClient({
|
||||
apiKey: TEST_API_KEY,
|
||||
organizationId: TEST_ORG_ID,
|
||||
projectId: TEST_PROJECT_ID,
|
||||
});
|
||||
await expect(
|
||||
client.createMemoryExport({
|
||||
filters: null as never,
|
||||
schema: null as never,
|
||||
}),
|
||||
).rejects.toThrow("Missing filters or schema");
|
||||
});
|
||||
|
||||
test("createMemoryExport sends POST to /v1/exports/", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/exports/", {
|
||||
status: 200,
|
||||
body: { message: "Export created", id: "exp_123" },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({
|
||||
apiKey: TEST_API_KEY,
|
||||
organizationId: TEST_ORG_ID,
|
||||
projectId: TEST_PROJECT_ID,
|
||||
});
|
||||
await client.createMemoryExport({
|
||||
schema: { fields: ["memory", "user_id"] },
|
||||
filters: { user_id: "u1" },
|
||||
});
|
||||
|
||||
expect(findFetchCall(mock, "/v1/exports/", "POST")).toBeDefined();
|
||||
});
|
||||
|
||||
test("createMemoryExport attaches org_id and project_id to body", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/exports/", {
|
||||
status: 200,
|
||||
body: { message: "Created", id: "exp_1" },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({
|
||||
apiKey: TEST_API_KEY,
|
||||
organizationId: TEST_ORG_ID,
|
||||
projectId: TEST_PROJECT_ID,
|
||||
});
|
||||
await client.createMemoryExport({
|
||||
schema: { fields: ["memory"] },
|
||||
filters: { user_id: "u1" },
|
||||
});
|
||||
|
||||
const call = findFetchCall(mock, "/v1/exports/", "POST");
|
||||
const body = getFetchBody(call!);
|
||||
expect(body.org_id).toBe(TEST_ORG_ID);
|
||||
expect(body.project_id).toBe(TEST_PROJECT_ID);
|
||||
});
|
||||
|
||||
test("getMemoryExport throws when missing both id and filters", async () => {
|
||||
setupMockFetch();
|
||||
const client = new MemoryClient({
|
||||
apiKey: TEST_API_KEY,
|
||||
organizationId: TEST_ORG_ID,
|
||||
projectId: TEST_PROJECT_ID,
|
||||
});
|
||||
await expect(client.getMemoryExport({} as never)).rejects.toThrow(
|
||||
"Missing memory_export_id or filters",
|
||||
);
|
||||
});
|
||||
|
||||
test("getMemoryExport sends POST to /v1/exports/get/", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/exports/get/", {
|
||||
status: 200,
|
||||
body: { message: "Export data", id: "exp_123" },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({
|
||||
apiKey: TEST_API_KEY,
|
||||
organizationId: TEST_ORG_ID,
|
||||
projectId: TEST_PROJECT_ID,
|
||||
});
|
||||
await client.getMemoryExport({ memory_export_id: "exp_123" });
|
||||
|
||||
expect(findFetchCall(mock, "/v1/exports/get/", "POST")).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* MemoryClient unit tests — search (v1/v2 routing, filters).
|
||||
* Tests verify request construction, not mock response echo.
|
||||
*/
|
||||
import { MemoryClient } from "../mem0";
|
||||
import type { Memory } from "../mem0.types";
|
||||
import { createMockMemory, TEST_API_KEY } from "./helpers";
|
||||
import {
|
||||
setupMockFetch,
|
||||
findFetchCall,
|
||||
getFetchBody,
|
||||
installConsoleSuppression,
|
||||
} from "./setup";
|
||||
|
||||
installConsoleSuppression();
|
||||
|
||||
describe("MemoryClient - search()", () => {
|
||||
test("sends POST to /v1/memories/search/ by default", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/search/", { status: 200, body: [] });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.search("What is my name?", { user_id: "u1" });
|
||||
|
||||
expect(findFetchCall(mock, "/v1/memories/search/", "POST")).toBeDefined();
|
||||
});
|
||||
|
||||
test("includes query in request body", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/search/", { status: 200, body: [] });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.search("What is my name?", { user_id: "u1" });
|
||||
|
||||
const call = findFetchCall(mock, "/v1/memories/search/", "POST");
|
||||
expect(getFetchBody(call!).query).toBe("What is my name?");
|
||||
});
|
||||
|
||||
test("includes user_id in request body", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/search/", { status: 200, body: [] });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.search("test", { user_id: "u1" });
|
||||
|
||||
const call = findFetchCall(mock, "/v1/memories/search/", "POST");
|
||||
expect(getFetchBody(call!).user_id).toBe("u1");
|
||||
});
|
||||
|
||||
test("uses /v2/memories/search/ when api_version=v2", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/memories/search/", { status: 200, body: [] });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.search("test", { user_id: "u1", api_version: "v2" });
|
||||
|
||||
expect(findFetchCall(mock, "/v2/memories/search/", "POST")).toBeDefined();
|
||||
});
|
||||
|
||||
test("passes filters through to the v2 API body", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/memories/search/", { status: 200, body: [] });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.search("query", {
|
||||
api_version: "v2",
|
||||
filters: { OR: [{ user_id: "u1" }, { agent_id: "a1" }] },
|
||||
});
|
||||
|
||||
const call = findFetchCall(mock, "/v2/memories/search/", "POST");
|
||||
const body = getFetchBody(call!);
|
||||
expect(body.filters).toEqual({
|
||||
OR: [{ user_id: "u1" }, { agent_id: "a1" }],
|
||||
});
|
||||
});
|
||||
|
||||
test("does not crash when called without options", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/search/", { status: 200, body: [] });
|
||||
setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
const result: Memory[] = await client.search("query");
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
|
||||
test("handles empty results array", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/search/", { status: 200, body: [] });
|
||||
setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
const result: Memory[] = await client.search("nonexistent query", {
|
||||
user_id: "u1",
|
||||
});
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -1,391 +0,0 @@
|
||||
import { MemoryClient } from "../mem0";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const apiKey = process.env.MEM0_API_KEY || "";
|
||||
// const client = new MemoryClient({ apiKey, host: 'https://api.mem0.ai', organizationId: "org_gRNd1RrQa4y52iK4tG8o59hXyVbaULikgq4kethC", projectId: "proj_7RfMkWs0PMgXYweGUNKqV9M9mgIRNt5XcupE7mSP" });
|
||||
// const client = new MemoryClient({ apiKey, host: 'https://api.mem0.ai', organizationName: "saket-default-org", projectName: "default-project" });
|
||||
const client = new MemoryClient({ apiKey, host: "https://api.mem0.ai" });
|
||||
|
||||
// Generate a random string
|
||||
const randomString = () => {
|
||||
return (
|
||||
Math.random().toString(36).substring(2, 15) +
|
||||
Math.random().toString(36).substring(2, 15)
|
||||
);
|
||||
};
|
||||
|
||||
describe("MemoryClient API", () => {
|
||||
let userId: string, memoryId: string;
|
||||
|
||||
beforeAll(() => {
|
||||
userId = randomString();
|
||||
});
|
||||
|
||||
const messages1 = [
|
||||
{ role: "user", content: "Hey, I am Alex. I'm now a vegetarian." },
|
||||
{ role: "assistant", content: "Hello Alex! Glad to hear!" },
|
||||
];
|
||||
|
||||
it("should add messages successfully", async () => {
|
||||
const res = await client.add(messages1, { user_id: userId || "" });
|
||||
|
||||
// Validate the response contains an iterable list
|
||||
expect(Array.isArray(res)).toBe(true);
|
||||
|
||||
// Validate the fields of the first message in the response
|
||||
const message = res[0];
|
||||
expect(typeof message.id).toBe("string");
|
||||
expect(typeof message.data?.memory).toBe("string");
|
||||
expect(typeof message.event).toBe("string");
|
||||
|
||||
// Store the memory ID for later use
|
||||
memoryId = message.id;
|
||||
});
|
||||
|
||||
it("should retrieve the specific memory by ID", async () => {
|
||||
const memory = await client.get(memoryId);
|
||||
|
||||
// Validate that the memory fields have the correct types and values
|
||||
|
||||
// Should be a string (memory id)
|
||||
expect(typeof memory.id).toBe("string");
|
||||
|
||||
// Should be a string (the actual memory content)
|
||||
expect(typeof memory.memory).toBe("string");
|
||||
|
||||
// Should be a string and equal to the userId
|
||||
expect(typeof memory.user_id).toBe("string");
|
||||
expect(memory.user_id).toBe(userId);
|
||||
|
||||
// Should be null or any object (metadata)
|
||||
expect(
|
||||
memory.metadata === null || typeof memory.metadata === "object",
|
||||
).toBe(true);
|
||||
|
||||
// Should be an array of strings or null (categories)
|
||||
expect(Array.isArray(memory.categories) || memory.categories === null).toBe(
|
||||
true,
|
||||
);
|
||||
if (Array.isArray(memory.categories)) {
|
||||
memory.categories.forEach((category) => {
|
||||
expect(typeof category).toBe("string");
|
||||
});
|
||||
}
|
||||
|
||||
// Should be a valid date (created_at)
|
||||
expect(new Date(memory.created_at || "").toString()).not.toBe(
|
||||
"Invalid Date",
|
||||
);
|
||||
|
||||
// Should be a valid date (updated_at)
|
||||
expect(new Date(memory.updated_at || "").toString()).not.toBe(
|
||||
"Invalid Date",
|
||||
);
|
||||
});
|
||||
|
||||
it("should retrieve all users successfully", async () => {
|
||||
const allUsers = await client.users();
|
||||
|
||||
// Validate the number of users is a number
|
||||
expect(typeof allUsers.count).toBe("number");
|
||||
|
||||
// Validate the structure of the first user
|
||||
const firstUser = allUsers.results[0];
|
||||
expect(typeof firstUser.id).toBe("string");
|
||||
expect(typeof firstUser.name).toBe("string");
|
||||
expect(typeof firstUser.created_at).toBe("string");
|
||||
expect(typeof firstUser.updated_at).toBe("string");
|
||||
expect(typeof firstUser.total_memories).toBe("number");
|
||||
expect(typeof firstUser.type).toBe("string");
|
||||
|
||||
// Find the user with the name matching userId
|
||||
const entity = allUsers.results.find((user) => user.name === userId);
|
||||
expect(entity).not.toBeUndefined();
|
||||
|
||||
// Store the entity ID for later use
|
||||
const entity_id = entity?.id;
|
||||
expect(typeof entity_id).toBe("string");
|
||||
});
|
||||
|
||||
it("should retrieve all memories for the user", async () => {
|
||||
const res3 = await client.getAll({ user_id: userId });
|
||||
|
||||
// Validate that res3 is an iterable list (array)
|
||||
expect(Array.isArray(res3)).toBe(true);
|
||||
|
||||
if (res3.length > 0) {
|
||||
// Iterate through the first memory for validation (you can loop through all if needed)
|
||||
const memory = res3[0];
|
||||
|
||||
// Should be a string (memory id)
|
||||
expect(typeof memory.id).toBe("string");
|
||||
|
||||
// Should be a string (the actual memory content)
|
||||
expect(typeof memory.memory).toBe("string");
|
||||
|
||||
// Should be a string and equal to the userId
|
||||
expect(typeof memory.user_id).toBe("string");
|
||||
expect(memory.user_id).toBe(userId);
|
||||
|
||||
// Should be null or an object (metadata)
|
||||
expect(
|
||||
memory.metadata === null || typeof memory.metadata === "object",
|
||||
).toBe(true);
|
||||
|
||||
// Should be an array of strings or null (categories)
|
||||
expect(
|
||||
Array.isArray(memory.categories) || memory.categories === null,
|
||||
).toBe(true);
|
||||
if (Array.isArray(memory.categories)) {
|
||||
memory.categories.forEach((category) => {
|
||||
expect(typeof category).toBe("string");
|
||||
});
|
||||
}
|
||||
|
||||
// Should be a valid date (created_at)
|
||||
expect(new Date(memory.created_at || "").toString()).not.toBe(
|
||||
"Invalid Date",
|
||||
);
|
||||
|
||||
// Should be a valid date (updated_at)
|
||||
expect(new Date(memory.updated_at || "").toString()).not.toBe(
|
||||
"Invalid Date",
|
||||
);
|
||||
} else {
|
||||
// If there are no memories, assert that the list is empty
|
||||
expect(res3.length).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("should search and return results based on provided query and filters (API version 2)", async () => {
|
||||
const searchOptionsV2 = {
|
||||
query: "What do you know about me?",
|
||||
filters: {
|
||||
OR: [{ user_id: userId }, { agent_id: "shopping-assistant" }],
|
||||
},
|
||||
threshold: 0.1,
|
||||
api_version: "v2",
|
||||
};
|
||||
|
||||
const searchResultV2 = await client.search(
|
||||
"What do you know about me?",
|
||||
searchOptionsV2,
|
||||
);
|
||||
|
||||
// Validate that searchResultV2 is an iterable list (array)
|
||||
expect(Array.isArray(searchResultV2)).toBe(true);
|
||||
|
||||
if (searchResultV2.length > 0) {
|
||||
// Iterate through the first search result for validation (you can loop through all if needed)
|
||||
const memory = searchResultV2[0];
|
||||
|
||||
// Should be a string (memory id)
|
||||
expect(typeof memory.id).toBe("string");
|
||||
|
||||
// Should be a string (the actual memory content)
|
||||
expect(typeof memory.memory).toBe("string");
|
||||
|
||||
if (memory.user_id) {
|
||||
// Should be a string and equal to userId
|
||||
expect(typeof memory.user_id).toBe("string");
|
||||
expect(memory.user_id).toBe(userId);
|
||||
}
|
||||
|
||||
if (memory.agent_id) {
|
||||
// Should be a string (agent_id)
|
||||
expect(typeof memory.agent_id).toBe("string");
|
||||
expect(memory.agent_id).toBe("shopping-assistant");
|
||||
}
|
||||
|
||||
// Should be null or an object (metadata)
|
||||
expect(
|
||||
memory.metadata === null || typeof memory.metadata === "object",
|
||||
).toBe(true);
|
||||
|
||||
// Should be an array of strings or null (categories)
|
||||
expect(
|
||||
Array.isArray(memory.categories) || memory.categories === null,
|
||||
).toBe(true);
|
||||
if (Array.isArray(memory.categories)) {
|
||||
memory.categories.forEach((category) => {
|
||||
expect(typeof category).toBe("string");
|
||||
});
|
||||
}
|
||||
|
||||
// Should be a valid date (created_at)
|
||||
expect(new Date(memory.created_at || "").toString()).not.toBe(
|
||||
"Invalid Date",
|
||||
);
|
||||
|
||||
// Should be a valid date (updated_at)
|
||||
expect(new Date(memory.updated_at || "").toString()).not.toBe(
|
||||
"Invalid Date",
|
||||
);
|
||||
|
||||
// Should be a number (score)
|
||||
expect(typeof memory.score).toBe("number");
|
||||
} else {
|
||||
// If no search results, assert that the list is empty
|
||||
expect(searchResultV2.length).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("should search and return results based on provided query (API version 1)", async () => {
|
||||
const searchResultV1 = await client.search("What is my name?", {
|
||||
user_id: userId,
|
||||
});
|
||||
|
||||
// Validate that searchResultV1 is an iterable list (array)
|
||||
expect(Array.isArray(searchResultV1)).toBe(true);
|
||||
|
||||
if (searchResultV1.length > 0) {
|
||||
// Iterate through the first search result for validation (you can loop through all if needed)
|
||||
const memory = searchResultV1[0];
|
||||
|
||||
// Should be a string (memory id)
|
||||
expect(typeof memory.id).toBe("string");
|
||||
|
||||
// Should be a string (the actual memory content)
|
||||
expect(typeof memory.memory).toBe("string");
|
||||
|
||||
// Should be a string and equal to userId
|
||||
expect(typeof memory.user_id).toBe("string");
|
||||
expect(memory.user_id).toBe(userId);
|
||||
|
||||
// Should be null or an object (metadata)
|
||||
expect(
|
||||
memory.metadata === null || typeof memory.metadata === "object",
|
||||
).toBe(true);
|
||||
|
||||
// Should be an array of strings or null (categories)
|
||||
expect(
|
||||
Array.isArray(memory.categories) || memory.categories === null,
|
||||
).toBe(true);
|
||||
if (Array.isArray(memory.categories)) {
|
||||
memory.categories.forEach((category) => {
|
||||
expect(typeof category).toBe("string");
|
||||
});
|
||||
}
|
||||
|
||||
// Should be a valid date (created_at)
|
||||
expect(new Date(memory.created_at || "").toString()).not.toBe(
|
||||
"Invalid Date",
|
||||
);
|
||||
|
||||
// Should be a valid date (updated_at)
|
||||
expect(new Date(memory.updated_at || "").toString()).not.toBe(
|
||||
"Invalid Date",
|
||||
);
|
||||
|
||||
// Should be a number (score)
|
||||
expect(typeof memory.score).toBe("number");
|
||||
} else {
|
||||
// If no search results, assert that the list is empty
|
||||
expect(searchResultV1.length).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("should retrieve history of a specific memory and validate the fields", async () => {
|
||||
const res22 = await client.history(memoryId);
|
||||
|
||||
// Validate that res22 is an iterable list (array)
|
||||
expect(Array.isArray(res22)).toBe(true);
|
||||
|
||||
if (res22.length > 0) {
|
||||
// Iterate through the first history entry for validation (you can loop through all if needed)
|
||||
const historyEntry = res22[0];
|
||||
|
||||
// Should be a string (history entry id)
|
||||
expect(typeof historyEntry.id).toBe("string");
|
||||
|
||||
// Should be a string (memory id related to the history entry)
|
||||
expect(typeof historyEntry.memory_id).toBe("string");
|
||||
|
||||
// Should be a string and equal to userId
|
||||
expect(typeof historyEntry.user_id).toBe("string");
|
||||
expect(historyEntry.user_id).toBe(userId);
|
||||
|
||||
// Should be a string or null (old memory)
|
||||
expect(
|
||||
historyEntry.old_memory === null ||
|
||||
typeof historyEntry.old_memory === "string",
|
||||
).toBe(true);
|
||||
|
||||
// Should be a string or null (new memory)
|
||||
expect(
|
||||
historyEntry.new_memory === null ||
|
||||
typeof historyEntry.new_memory === "string",
|
||||
).toBe(true);
|
||||
|
||||
// Should be an array of strings or null (categories)
|
||||
expect(
|
||||
Array.isArray(historyEntry.categories) ||
|
||||
historyEntry.categories === null,
|
||||
).toBe(true);
|
||||
if (Array.isArray(historyEntry.categories)) {
|
||||
historyEntry.categories.forEach((category) => {
|
||||
expect(typeof category).toBe("string");
|
||||
});
|
||||
}
|
||||
|
||||
// Should be a valid date (created_at)
|
||||
expect(new Date(historyEntry.created_at).toString()).not.toBe(
|
||||
"Invalid Date",
|
||||
);
|
||||
|
||||
// Should be a valid date (updated_at)
|
||||
expect(new Date(historyEntry.updated_at).toString()).not.toBe(
|
||||
"Invalid Date",
|
||||
);
|
||||
|
||||
// Should be a string, one of: ADD, UPDATE, DELETE, NOOP
|
||||
expect(["ADD", "UPDATE", "DELETE", "NOOP"]).toContain(historyEntry.event);
|
||||
|
||||
// Validate conditions based on event type
|
||||
if (historyEntry.event === "ADD") {
|
||||
expect(historyEntry.old_memory).toBeNull();
|
||||
expect(historyEntry.new_memory).not.toBeNull();
|
||||
} else if (historyEntry.event === "UPDATE") {
|
||||
expect(historyEntry.old_memory).not.toBeNull();
|
||||
expect(historyEntry.new_memory).not.toBeNull();
|
||||
} else if (historyEntry.event === "DELETE") {
|
||||
expect(historyEntry.old_memory).not.toBeNull();
|
||||
expect(historyEntry.new_memory).toBeNull();
|
||||
}
|
||||
|
||||
// Should be a list of objects or null (input)
|
||||
expect(
|
||||
Array.isArray(historyEntry.input) || historyEntry.input === null,
|
||||
).toBe(true);
|
||||
if (Array.isArray(historyEntry.input)) {
|
||||
historyEntry.input.forEach((input) => {
|
||||
// Each input should be an object
|
||||
expect(typeof input).toBe("object");
|
||||
|
||||
// Should have string content
|
||||
expect(typeof input.content).toBe("string");
|
||||
|
||||
// Should have a role that is either 'user' or 'assistant'
|
||||
expect(["user", "assistant"]).toContain(input.role);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// If no history entries, assert that the list is empty
|
||||
expect(res22.length).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("should delete the user successfully", async () => {
|
||||
const allUsers = await client.users();
|
||||
const entity = allUsers.results.find((user) => user.name === userId);
|
||||
|
||||
if (entity) {
|
||||
const deletedUser = await client.deleteUser(entity.id);
|
||||
|
||||
// Validate the deletion message
|
||||
expect(deletedUser.message).toBe("Entity deleted successfully!");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* MemoryClient unit tests — users, deleteUser, deleteUsers.
|
||||
* Tests verify entity type routing and request construction.
|
||||
*/
|
||||
import { MemoryClient } from "../mem0";
|
||||
import {
|
||||
createMockUser,
|
||||
createMockAllUsers,
|
||||
TEST_API_KEY,
|
||||
TEST_ORG_ID,
|
||||
TEST_PROJECT_ID,
|
||||
} from "./helpers";
|
||||
import {
|
||||
setupMockFetch,
|
||||
findFetchCall,
|
||||
installConsoleSuppression,
|
||||
} from "./setup";
|
||||
|
||||
installConsoleSuppression();
|
||||
|
||||
// ─── users() ────────────────────────────────────────────
|
||||
|
||||
describe("MemoryClient - users()", () => {
|
||||
test("sends GET to /v1/entities/", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/entities/", {
|
||||
status: 200,
|
||||
body: createMockAllUsers([createMockUser()]),
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.users();
|
||||
|
||||
const call = mock.mock.calls.find(
|
||||
(c: [string, RequestInit]) =>
|
||||
c[0].includes("/v1/entities/") && !c[1]?.method,
|
||||
);
|
||||
expect(call).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── deleteUsers() ──────────────────────────────────────
|
||||
|
||||
describe("MemoryClient - deleteUsers()", () => {
|
||||
function createClientWithMockedAxios() {
|
||||
setupMockFetch();
|
||||
const client = new MemoryClient({
|
||||
apiKey: TEST_API_KEY,
|
||||
organizationId: TEST_ORG_ID,
|
||||
projectId: TEST_PROJECT_ID,
|
||||
});
|
||||
const axiosDeleteMock = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ data: { message: "Deleted" } });
|
||||
client.client.delete = axiosDeleteMock;
|
||||
return { client, axiosDeleteMock };
|
||||
}
|
||||
|
||||
test("routes user_id to DELETE /v2/entities/user/:name/", async () => {
|
||||
const { client, axiosDeleteMock } = createClientWithMockedAxios();
|
||||
await client.deleteUsers({ user_id: "u1" });
|
||||
|
||||
expect(axiosDeleteMock).toHaveBeenCalledWith("/v2/entities/user/u1/", {
|
||||
params: expect.objectContaining({
|
||||
org_id: TEST_ORG_ID,
|
||||
project_id: TEST_PROJECT_ID,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
test("routes agent_id to DELETE /v2/entities/agent/:name/", async () => {
|
||||
const { client, axiosDeleteMock } = createClientWithMockedAxios();
|
||||
await client.deleteUsers({ agent_id: "agent_1" });
|
||||
|
||||
expect(axiosDeleteMock).toHaveBeenCalledWith(
|
||||
"/v2/entities/agent/agent_1/",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
test("routes app_id to DELETE /v2/entities/app/:name/", async () => {
|
||||
const { client, axiosDeleteMock } = createClientWithMockedAxios();
|
||||
await client.deleteUsers({ app_id: "app_1" });
|
||||
|
||||
expect(axiosDeleteMock).toHaveBeenCalledWith(
|
||||
"/v2/entities/app/app_1/",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
test("routes run_id to DELETE /v2/entities/run/:name/", async () => {
|
||||
const { client, axiosDeleteMock } = createClientWithMockedAxios();
|
||||
await client.deleteUsers({ run_id: "run_1" });
|
||||
|
||||
expect(axiosDeleteMock).toHaveBeenCalledWith(
|
||||
"/v2/entities/run/run_1/",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
test("returns 'Entity deleted successfully.' for single entity", async () => {
|
||||
const { client } = createClientWithMockedAxios();
|
||||
const result = await client.deleteUsers({ user_id: "u1" });
|
||||
expect(result.message).toBe("Entity deleted successfully.");
|
||||
});
|
||||
|
||||
test("returns 'All users, agents, apps and runs deleted.' when no params given", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/entities/", {
|
||||
status: 200,
|
||||
body: createMockAllUsers([createMockUser({ name: "u1", type: "user" })]),
|
||||
});
|
||||
setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({
|
||||
apiKey: TEST_API_KEY,
|
||||
organizationId: TEST_ORG_ID,
|
||||
projectId: TEST_PROJECT_ID,
|
||||
});
|
||||
client.client.delete = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ data: { message: "Deleted" } });
|
||||
|
||||
const result = await client.deleteUsers();
|
||||
expect(result.message).toBe("All users, agents, apps and runs deleted.");
|
||||
});
|
||||
|
||||
test("throws when no entities exist to delete", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/entities/", {
|
||||
status: 200,
|
||||
body: createMockAllUsers([]),
|
||||
});
|
||||
setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({
|
||||
apiKey: TEST_API_KEY,
|
||||
organizationId: TEST_ORG_ID,
|
||||
projectId: TEST_PROJECT_ID,
|
||||
});
|
||||
client.client.delete = jest.fn();
|
||||
|
||||
await expect(client.deleteUsers()).rejects.toThrow("No entities to delete");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── deleteUser() (deprecated) ──────────────────────────
|
||||
|
||||
describe("MemoryClient - deleteUser() (deprecated)", () => {
|
||||
test("sends DELETE to /v1/entities/:type/:id/", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/entities/user/123/", {
|
||||
status: 200,
|
||||
body: { message: "Entity deleted successfully!" },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.deleteUser({
|
||||
entity_id: 123 as never,
|
||||
entity_type: "user",
|
||||
});
|
||||
|
||||
expect(
|
||||
findFetchCall(mock, "/v1/entities/user/123/", "DELETE"),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
test("defaults entity_type to 'user' when empty", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/entities/user/456/", {
|
||||
status: 200,
|
||||
body: { message: "Entity deleted successfully!" },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.deleteUser({ entity_id: 456 as never, entity_type: "" });
|
||||
|
||||
expect(
|
||||
findFetchCall(mock, "/v1/entities/user/456/", "DELETE"),
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* MemoryClient unit tests — getWebhooks, createWebhook, updateWebhook, deleteWebhook.
|
||||
* Tests verify request URL and HTTP method, not mock response values.
|
||||
*/
|
||||
import { MemoryClient } from "../mem0";
|
||||
import { WebhookEvent } from "../mem0.types";
|
||||
import { TEST_API_KEY, TEST_ORG_ID, TEST_PROJECT_ID } from "./helpers";
|
||||
import {
|
||||
setupMockFetch,
|
||||
findFetchCall,
|
||||
getFetchBody,
|
||||
installConsoleSuppression,
|
||||
} from "./setup";
|
||||
|
||||
installConsoleSuppression();
|
||||
|
||||
describe("MemoryClient - Webhooks", () => {
|
||||
test("getWebhooks 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,
|
||||
});
|
||||
await client.getWebhooks();
|
||||
|
||||
const call = mock.mock.calls.find(
|
||||
(c: [string, RequestInit]) =>
|
||||
c[0].includes("/api/v1/webhooks/projects/") && !c[1]?.method,
|
||||
);
|
||||
expect(call).toBeDefined();
|
||||
});
|
||||
|
||||
test("createWebhook sends POST 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: { 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: "",
|
||||
});
|
||||
|
||||
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!);
|
||||
expect(body.name).toBe("new-hook");
|
||||
expect(body.url).toBe("https://example.com");
|
||||
});
|
||||
|
||||
test("updateWebhook sends PUT 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 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,
|
||||
});
|
||||
|
||||
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!);
|
||||
expect(body.name).toBe("updated-hook");
|
||||
expect(body.url).toBe("https://new-url.com");
|
||||
});
|
||||
|
||||
test("deleteWebhook 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 client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.deleteWebhook({ webhookId: "wh_1" });
|
||||
|
||||
expect(
|
||||
findFetchCall(mock, "/api/v1/webhooks/wh_1/", "DELETE"),
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Shared test setup for MemoryClient unit tests.
|
||||
* Provides mock fetch wiring, console suppression, and utility finders.
|
||||
*/
|
||||
import {
|
||||
createMockFetch,
|
||||
createStandardMockResponses,
|
||||
MOCK_PING_RESPONSE,
|
||||
} from "./helpers";
|
||||
|
||||
// ─── Global fetch mock + telemetry suppression ───────────
|
||||
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
export function setupMockFetch(
|
||||
extraResponses?: Map<string, { status: number; body: unknown }>,
|
||||
): jest.Mock {
|
||||
const responses = createStandardMockResponses();
|
||||
if (extraResponses) {
|
||||
for (const [key, value] of extraResponses) {
|
||||
responses.set(key, value);
|
||||
}
|
||||
}
|
||||
const mockFetch = createMockFetch(responses);
|
||||
global.fetch = mockFetch;
|
||||
return mockFetch;
|
||||
}
|
||||
|
||||
const originalConsoleError = console.error;
|
||||
const originalConsoleWarn = console.warn;
|
||||
|
||||
export function installConsoleSuppression(): void {
|
||||
beforeAll(() => {
|
||||
jest.spyOn(console, "error").mockImplementation((...args: unknown[]) => {
|
||||
const msg = String(args[0] ?? "");
|
||||
if (
|
||||
msg.includes("Telemetry") ||
|
||||
msg.includes("Failed to initialize") ||
|
||||
msg.includes("Failed to capture")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
originalConsoleError(...args);
|
||||
});
|
||||
jest.spyOn(console, "warn").mockImplementation((...args: unknown[]) => {
|
||||
const msg = String(args[0] ?? "");
|
||||
if (msg.includes("telemetry") || msg.includes("Telemetry")) {
|
||||
return;
|
||||
}
|
||||
originalConsoleWarn(...args);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Helper: find specific fetch calls ───────────────────
|
||||
|
||||
export function findFetchCall(
|
||||
mock: jest.Mock,
|
||||
urlPattern: string,
|
||||
method?: string,
|
||||
): [string, RequestInit] | undefined {
|
||||
return mock.mock.calls.find((call: [string, RequestInit]) => {
|
||||
const urlMatch = call[0].includes(urlPattern);
|
||||
if (!method) return urlMatch;
|
||||
return urlMatch && call[1]?.method === method;
|
||||
});
|
||||
}
|
||||
|
||||
export function getFetchBody(
|
||||
call: [string, RequestInit],
|
||||
): Record<string, unknown> {
|
||||
return JSON.parse(call[1].body as string);
|
||||
}
|
||||
|
||||
export { MOCK_PING_RESPONSE };
|
||||
Reference in New Issue
Block a user