chore: end to end test coverage for ts sdk (#4357)

This commit is contained in:
Kartik
2026-03-17 21:13:52 +05:30
committed by GitHub
parent 336fbce60a
commit 3cdcb6564c
36 changed files with 4583 additions and 778 deletions
+75
View File
@@ -0,0 +1,75 @@
name: TypeScript SDK CI
on:
push:
branches: [main]
paths:
- 'mem0-ts/**'
- '.github/workflows/ts-sdk-ci.yml'
pull_request:
paths:
- 'mem0-ts/**'
jobs:
check_changes:
runs-on: ubuntu-latest
outputs:
ts_sdk_changed: ${{ steps.filter.outputs.ts_sdk }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
ts_sdk:
- 'mem0-ts/**'
build_ts_sdk:
needs: check_changes
if: needs.check_changes.outputs.ts_sdk_changed == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
cache-dependency-path: mem0-ts/pnpm-lock.yaml
- name: Install dependencies
working-directory: mem0-ts
run: pnpm install --frozen-lockfile
- name: Lint
working-directory: mem0-ts
run: npx prettier --check .
- name: Build
working-directory: mem0-ts
run: pnpm run build
- name: Run tests
working-directory: mem0-ts
run: pnpm run test:ci
- name: Verify package exports
working-directory: mem0-ts
run: |
node -e "const m = require('./dist/index.js'); console.log('Client exports:', Object.keys(m).length)"
node -e "const m = require('./dist/oss/index.js'); console.log('OSS exports:', Object.keys(m).length)"
- name: Upload coverage
if: matrix.node-version == 20
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: mem0-ts/coverage/
+6
View File
@@ -0,0 +1,6 @@
node_modules/
dist/
coverage/
*.db
.env
.env.*
+4
View File
@@ -0,0 +1,4 @@
node_modules/
dist/
coverage/
pnpm-lock.yaml
+1
View File
@@ -37,6 +37,7 @@
"start": "pnpm run example memory",
"example": "ts-node src/oss/examples/vector-stores/index.ts",
"test": "jest",
"test:ci": "jest --coverage --ci",
"test:ts": "jest --config jest.config.js",
"test:watch": "jest --config jest.config.js --watch",
"format": "npm run clean && prettier --write .",
+18 -1
View File
@@ -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";
+6 -5
View File
@@ -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;
+1 -1
View File
@@ -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",
+189
View File
@@ -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();
});
});
+83
View File
@@ -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 };
+246
View File
@@ -0,0 +1,246 @@
import {
MemoryError,
AuthenticationError,
RateLimitError,
ValidationError,
MemoryNotFoundError,
NetworkError,
ConfigurationError,
MemoryQuotaExceededError,
createExceptionFromResponse,
HTTP_STATUS_TO_EXCEPTION,
} from "./exceptions";
describe("MemoryError", () => {
const error = new MemoryError("test error", "MEM_001", {
details: { operation: "add" },
suggestion: "Try again",
debugInfo: { requestId: "req_123" },
});
test("is an instance of Error", () => {
expect(error).toBeInstanceOf(Error);
});
test("has correct message", () => {
expect(error.message).toBe("test error");
});
test("has correct errorCode", () => {
expect(error.errorCode).toBe("MEM_001");
});
test("has correct details", () => {
expect(error.details).toEqual({ operation: "add" });
});
test("has correct suggestion", () => {
expect(error.suggestion).toBe("Try again");
});
test("has correct debugInfo", () => {
expect(error.debugInfo).toEqual({ requestId: "req_123" });
});
test("defaults details to empty object", () => {
const err = new MemoryError("test error", "MEM_001");
expect(err.details).toEqual({});
});
test("defaults suggestion to undefined", () => {
const err = new MemoryError("test error", "MEM_001");
expect(err.suggestion).toBeUndefined();
});
test("defaults debugInfo to empty object", () => {
const err = new MemoryError("test error", "MEM_001");
expect(err.debugInfo).toEqual({});
});
test("is throwable and catchable", () => {
expect(() => {
throw new MemoryError("fail", "MEM_001");
}).toThrow("fail");
});
});
describe("Exception subclasses", () => {
const subclasses = [
{ Class: AuthenticationError, name: "AuthenticationError" },
{ Class: RateLimitError, name: "RateLimitError" },
{ Class: ValidationError, name: "ValidationError" },
{ Class: MemoryNotFoundError, name: "MemoryNotFoundError" },
{ Class: NetworkError, name: "NetworkError" },
{ Class: ConfigurationError, name: "ConfigurationError" },
{ Class: MemoryQuotaExceededError, name: "MemoryQuotaExceededError" },
] as const;
test.each(subclasses)("$name extends MemoryError", ({ Class }) => {
const error = new Class("test", "CODE_001");
expect(error).toBeInstanceOf(MemoryError);
});
test.each(subclasses)("$name extends Error", ({ Class }) => {
const error = new Class("test", "CODE_001");
expect(error).toBeInstanceOf(Error);
});
test.each(subclasses)("$name has correct name", ({ Class, name }) => {
const error = new Class("test", "CODE_001");
expect(error.name).toBe(name);
});
test.each(subclasses)("$name supports instanceof checks", ({ Class }) => {
const error = new Class("test", "CODE_001");
expect(error instanceof Class).toBe(true);
});
});
describe("createExceptionFromResponse", () => {
test("maps 401 to AuthenticationError", () => {
const error = createExceptionFromResponse(401, "Unauthorized");
expect(error).toBeInstanceOf(AuthenticationError);
});
test("maps 401 to errorCode HTTP_401", () => {
const error = createExceptionFromResponse(401, "Unauthorized");
expect(error.errorCode).toBe("HTTP_401");
});
test("maps 401 to authentication suggestion", () => {
const error = createExceptionFromResponse(401, "Unauthorized");
expect(error.suggestion).toBe(
"Please check your API key and authentication credentials",
);
});
test("maps 429 to RateLimitError", () => {
const error = createExceptionFromResponse(429, "Too many requests", {
debugInfo: { retryAfter: 60 },
});
expect(error).toBeInstanceOf(RateLimitError);
});
test("maps 429 passes debugInfo through", () => {
const error = createExceptionFromResponse(429, "Too many requests", {
debugInfo: { retryAfter: 60 },
});
expect(error.debugInfo).toEqual({ retryAfter: 60 });
});
test("maps 404 to MemoryNotFoundError", () => {
const error = createExceptionFromResponse(404, "Not found");
expect(error).toBeInstanceOf(MemoryNotFoundError);
});
test("maps 400 to ValidationError", () => {
const error = createExceptionFromResponse(400, "Bad request");
expect(error).toBeInstanceOf(ValidationError);
});
test("maps 413 to MemoryQuotaExceededError", () => {
const error = createExceptionFromResponse(413, "Quota exceeded");
expect(error).toBeInstanceOf(MemoryQuotaExceededError);
});
test.each([502, 503, 504])("maps %i to NetworkError", (code) => {
const error = createExceptionFromResponse(code, "Service unavailable");
expect(error).toBeInstanceOf(NetworkError);
});
test("maps 500 to MemoryError", () => {
const error = createExceptionFromResponse(500, "Internal error");
expect(error).toBeInstanceOf(MemoryError);
});
test("maps 500 to errorCode HTTP_500", () => {
const error = createExceptionFromResponse(500, "Internal error");
expect(error.errorCode).toBe("HTTP_500");
});
test("maps unknown status to MemoryError", () => {
const error = createExceptionFromResponse(418, "I am a teapot");
expect(error).toBeInstanceOf(MemoryError);
});
test("maps unknown status to correct errorCode", () => {
const error = createExceptionFromResponse(418, "I am a teapot");
expect(error.errorCode).toBe("HTTP_418");
});
test("maps unknown status to retry suggestion", () => {
const error = createExceptionFromResponse(418, "I am a teapot");
expect(error.suggestion).toBe("Please try again later");
});
test("uses response text as message", () => {
const error = createExceptionFromResponse(400, "Invalid user_id format");
expect(error.message).toBe("Invalid user_id format");
});
test("falls back to generic message when response text is empty", () => {
const error = createExceptionFromResponse(500, "");
expect(error.message).toBe("HTTP 500 error");
});
test("passes details through", () => {
const error = createExceptionFromResponse(400, "Bad request", {
details: { field: "user_id", value: "" },
});
expect(error.details).toEqual({ field: "user_id", value: "" });
});
});
describe("HTTP_STATUS_TO_EXCEPTION", () => {
test("maps 400 to ValidationError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[400]).toBe(ValidationError);
});
test("maps 401 to AuthenticationError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[401]).toBe(AuthenticationError);
});
test("maps 403 to AuthenticationError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[403]).toBe(AuthenticationError);
});
test("maps 404 to MemoryNotFoundError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[404]).toBe(MemoryNotFoundError);
});
test("maps 408 to NetworkError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[408]).toBe(NetworkError);
});
test("maps 409 to ValidationError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[409]).toBe(ValidationError);
});
test("maps 413 to MemoryQuotaExceededError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[413]).toBe(MemoryQuotaExceededError);
});
test("maps 422 to ValidationError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[422]).toBe(ValidationError);
});
test("maps 429 to RateLimitError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[429]).toBe(RateLimitError);
});
test("maps 500 to MemoryError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[500]).toBe(MemoryError);
});
test("maps 502 to NetworkError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[502]).toBe(NetworkError);
});
test("maps 503 to NetworkError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[503]).toBe(NetworkError);
});
test("maps 504 to NetworkError", () => {
expect(HTTP_STATUS_TO_EXCEPTION[504]).toBe(NetworkError);
});
});
+205
View File
@@ -0,0 +1,205 @@
/**
* Structured exception classes for mem0 TypeScript SDK.
*
* Provides specific, actionable exceptions with error codes, suggestions,
* and debug information. Maps HTTP status codes to appropriate exception types.
*
* @example
* ```typescript
* import { RateLimitError, MemoryNotFoundError } from 'mem0ai'
*
* try {
* await client.get(memoryId)
* } catch (e) {
* if (e instanceof MemoryNotFoundError) {
* console.log(e.suggestion) // "The requested resource was not found"
* } else if (e instanceof RateLimitError) {
* await sleep(e.debugInfo.retryAfter ?? 60)
* }
* }
* ```
*/
export interface MemoryErrorOptions {
details?: Record<string, unknown>;
suggestion?: string;
debugInfo?: Record<string, unknown>;
}
/**
* Base exception for all memory-related errors.
*
* Every mem0 exception includes an error code for programmatic handling,
* optional details, a user-friendly suggestion, and debug information.
*/
export class MemoryError extends Error {
readonly errorCode: string;
readonly details: Record<string, unknown>;
readonly suggestion?: string;
readonly debugInfo: Record<string, unknown>;
constructor(
message: string,
errorCode: string,
options: MemoryErrorOptions = {},
) {
super(message);
this.name = "MemoryError";
this.errorCode = errorCode;
this.details = options.details ?? {};
this.suggestion = options.suggestion;
this.debugInfo = options.debugInfo ?? {};
// Fix prototype chain for instanceof checks
Object.setPrototypeOf(this, new.target.prototype);
}
}
/** Raised when authentication fails (401, 403). */
export class AuthenticationError extends MemoryError {
constructor(
message: string,
errorCode: string,
options?: MemoryErrorOptions,
) {
super(message, errorCode, options);
this.name = "AuthenticationError";
}
}
/** Raised when rate limits are exceeded (429). */
export class RateLimitError extends MemoryError {
constructor(
message: string,
errorCode: string,
options?: MemoryErrorOptions,
) {
super(message, errorCode, options);
this.name = "RateLimitError";
}
}
/** Raised when input validation fails (400, 409, 422). */
export class ValidationError extends MemoryError {
constructor(
message: string,
errorCode: string,
options?: MemoryErrorOptions,
) {
super(message, errorCode, options);
this.name = "ValidationError";
}
}
/** Raised when a memory is not found (404). */
export class MemoryNotFoundError extends MemoryError {
constructor(
message: string,
errorCode: string,
options?: MemoryErrorOptions,
) {
super(message, errorCode, options);
this.name = "MemoryNotFoundError";
}
}
/** Raised when network connectivity issues occur (408, 502, 503, 504). */
export class NetworkError extends MemoryError {
constructor(
message: string,
errorCode: string,
options?: MemoryErrorOptions,
) {
super(message, errorCode, options);
this.name = "NetworkError";
}
}
/** Raised when client configuration is invalid. */
export class ConfigurationError extends MemoryError {
constructor(
message: string,
errorCode: string,
options?: MemoryErrorOptions,
) {
super(message, errorCode, options);
this.name = "ConfigurationError";
}
}
/** Raised when memory quota is exceeded (413). */
export class MemoryQuotaExceededError extends MemoryError {
constructor(
message: string,
errorCode: string,
options?: MemoryErrorOptions,
) {
super(message, errorCode, options);
this.name = "MemoryQuotaExceededError";
}
}
// ─── HTTP Status → Exception Mapping ─────────────────────
type MemoryErrorConstructor = new (
message: string,
errorCode: string,
options?: MemoryErrorOptions,
) => MemoryError;
export const HTTP_STATUS_TO_EXCEPTION: Record<number, MemoryErrorConstructor> =
{
400: ValidationError,
401: AuthenticationError,
403: AuthenticationError,
404: MemoryNotFoundError,
408: NetworkError,
409: ValidationError,
413: MemoryQuotaExceededError,
422: ValidationError,
429: RateLimitError,
500: MemoryError,
502: NetworkError,
503: NetworkError,
504: NetworkError,
};
const HTTP_SUGGESTIONS: Record<number, string> = {
400: "Please check your request parameters and try again",
401: "Please check your API key and authentication credentials",
403: "You don't have permission to perform this operation",
404: "The requested resource was not found",
408: "Request timed out. Please try again",
409: "Resource conflict. Please check your request",
413: "Request too large. Please reduce the size of your request",
422: "Invalid request data. Please check your input",
429: "Rate limit exceeded. Please wait before making more requests",
500: "Internal server error. Please try again later",
502: "Service temporarily unavailable. Please try again later",
503: "Service unavailable. Please try again later",
504: "Gateway timeout. Please try again later",
};
/**
* Create an appropriate exception based on HTTP response status code.
*
* @param statusCode - HTTP status code from the response
* @param responseText - Response body text
* @param options - Additional error context (details, debugInfo)
* @returns An instance of the appropriate MemoryError subclass
*/
export function createExceptionFromResponse(
statusCode: number,
responseText: string,
options: Omit<MemoryErrorOptions, "suggestion"> = {},
): MemoryError {
const ExceptionClass = HTTP_STATUS_TO_EXCEPTION[statusCode] ?? MemoryError;
const errorCode = `HTTP_${statusCode}`;
const suggestion = HTTP_SUGGESTIONS[statusCode] ?? "Please try again later";
return new ExceptionClass(
responseText || `HTTP ${statusCode} error`,
errorCode,
{ ...options, suggestion },
);
}
+9 -3
View File
@@ -23,11 +23,15 @@ export class ConfigManager {
// Normalize snake_case keys from Python SDK / OpenClaw configs
const baseURL =
userConf?.baseURL ??
(userConf as Record<string, unknown>)?.lmstudio_base_url as string | undefined ??
((userConf as Record<string, unknown>)?.lmstudio_base_url as
| string
| undefined) ??
userConf?.url;
const embeddingDims =
userConf?.embeddingDims ??
(userConf as Record<string, unknown>)?.embedding_dims as number | undefined;
((userConf as Record<string, unknown>)?.embedding_dims as
| number
| undefined);
return {
apiKey:
@@ -103,7 +107,9 @@ export class ConfigManager {
// Normalize snake_case keys from Python SDK / OpenClaw configs
const llmBaseURL =
userConf?.baseURL ??
(userConf as Record<string, unknown>)?.lmstudio_base_url as string | undefined ??
((userConf as Record<string, unknown>)?.lmstudio_base_url as
| string
| undefined) ??
defaultConf.baseURL;
return {
@@ -32,7 +32,7 @@ describe("backward compat: ConfigManager.mergeConfig", () => {
expect(cfg.embedder.provider).toBe("openai");
expect(cfg.vectorStore.provider).toBe("memory");
expect(cfg.vectorStore.config.collectionName).toBe("memories");
expect(cfg.vectorStore.config.dimension).toBe(1536);
expect(cfg.vectorStore.config.dimension).toBeUndefined();
expect(cfg.llm.provider).toBe("openai");
expect(cfg.historyStore).toBeDefined();
expect(cfg.historyStore!.provider).toBe("sqlite");
+5 -1
View File
@@ -282,7 +282,11 @@ export class Qdrant implements VectorStore {
},
});
} catch (error: any) {
if (error?.status === 409 || error?.status === 401 || error?.status === 403) {
if (
error?.status === 409 ||
error?.status === 401 ||
error?.status === 403
) {
// Collection already exists — verify configuration for the main collection
if (name === this.collectionName) {
try {
+92 -25
View File
@@ -190,7 +190,10 @@ describe("ConfigManager", () => {
const cfg = ConfigManager.mergeConfig({
embedder: {
provider: "lmstudio",
config: { model: "nomic-embed-text-v1.5", embedding_dims: 768 } as any,
config: {
model: "nomic-embed-text-v1.5",
embedding_dims: 768,
} as any,
},
vectorStore: { provider: "memory", config: {} },
llm: baseLlm,
@@ -313,7 +316,11 @@ describe("ConfigManager", () => {
},
vectorStore: {
provider: "qdrant",
config: { host: "192.168.200.12", port: 6333, checkCompatibility: false },
config: {
host: "192.168.200.12",
port: 6333,
checkCompatibility: false,
},
},
llm: {
provider: "lmstudio",
@@ -326,7 +333,9 @@ describe("ConfigManager", () => {
expect(cfg.embedder.provider).toBe("lmstudio");
expect(cfg.embedder.config.baseURL).toBe("http://192.168.200.83:1234/v1");
expect(cfg.embedder.config.model).toBe("text-embedding-gte-qwen2-1.5b-instruct");
expect(cfg.embedder.config.model).toBe(
"text-embedding-gte-qwen2-1.5b-instruct",
);
expect(cfg.embedder.config.embeddingDims).toBe(1536);
expect(cfg.llm.provider).toBe("lmstudio");
@@ -409,12 +418,18 @@ describe("Memory – LM Studio end-to-end flow", () => {
const mem = new MemoryClass({
embedder: {
provider: "lmstudio",
config: { model: "nomic-embed-text-v1.5", baseURL: "http://localhost:1234/v1" },
config: {
model: "nomic-embed-text-v1.5",
baseURL: "http://localhost:1234/v1",
},
},
vectorStore: { provider: "memory", config: { collectionName: "test" } },
llm: {
provider: "lmstudio",
config: { model: "meta-llama-3.1-70b", baseURL: "http://localhost:1234/v1" },
config: {
model: "meta-llama-3.1-70b",
baseURL: "http://localhost:1234/v1",
},
},
disableHistory: true,
});
@@ -423,11 +438,17 @@ describe("Memory – LM Studio end-to-end flow", () => {
expect(mockEmbedderFactory.create).toHaveBeenCalledWith(
"lmstudio",
expect.objectContaining({ model: "nomic-embed-text-v1.5", baseURL: "http://localhost:1234/v1" }),
expect.objectContaining({
model: "nomic-embed-text-v1.5",
baseURL: "http://localhost:1234/v1",
}),
);
expect(mockLlmFactory.create).toHaveBeenCalledWith(
"lmstudio",
expect.objectContaining({ model: "meta-llama-3.1-70b", baseURL: "http://localhost:1234/v1" }),
expect.objectContaining({
model: "meta-llama-3.1-70b",
baseURL: "http://localhost:1234/v1",
}),
);
});
@@ -435,10 +456,16 @@ describe("Memory – LM Studio end-to-end flow", () => {
const mem = new MemoryClass({
embedder: {
provider: "lmstudio",
config: { model: "nomic-embed-text-v1.5", baseURL: "http://localhost:1234/v1" },
config: {
model: "nomic-embed-text-v1.5",
baseURL: "http://localhost:1234/v1",
},
},
vectorStore: { provider: "qdrant", config: { collectionName: "test" } },
llm: { provider: "lmstudio", config: { baseURL: "http://localhost:1234/v1" } },
llm: {
provider: "lmstudio",
config: { baseURL: "http://localhost:1234/v1" },
},
disableHistory: true,
});
@@ -462,7 +489,10 @@ describe("Memory – LM Studio end-to-end flow", () => {
vectorStore: { provider: "memory", config: { collectionName: "test" } },
llm: {
provider: "lmstudio",
config: { model: "openai/gpt-oss-20b", lmstudio_base_url: "http://192.168.200.83:1234/v1" } as any,
config: {
model: "openai/gpt-oss-20b",
lmstudio_base_url: "http://192.168.200.83:1234/v1",
} as any,
},
disableHistory: true,
});
@@ -471,32 +501,57 @@ describe("Memory – LM Studio end-to-end flow", () => {
expect(mockEmbedderFactory.create).toHaveBeenCalledWith(
"lmstudio",
expect.objectContaining({ model: "text-embedding-gte-qwen2-1.5b-instruct", baseURL: "http://192.168.200.83:1234/v1" }),
expect.objectContaining({
model: "text-embedding-gte-qwen2-1.5b-instruct",
baseURL: "http://192.168.200.83:1234/v1",
}),
);
expect(mockLlmFactory.create).toHaveBeenCalledWith(
"lmstudio",
expect.objectContaining({ model: "openai/gpt-oss-20b", baseURL: "http://192.168.200.83:1234/v1" }),
expect.objectContaining({
model: "openai/gpt-oss-20b",
baseURL: "http://192.168.200.83:1234/v1",
}),
);
});
it("search flow works with lmstudio embedder", async () => {
mockVStore.search.mockResolvedValueOnce([{
id: "mem-1",
payload: { data: "User likes hiking", user_id: "u1", hash: "abc123", created_at: "2026-01-01" },
score: 0.95,
}]);
mockVStore.search.mockResolvedValueOnce([
{
id: "mem-1",
payload: {
data: "User likes hiking",
user_id: "u1",
hash: "abc123",
created_at: "2026-01-01",
},
score: 0.95,
},
]);
const mem = new MemoryClass({
embedder: {
provider: "lmstudio",
config: { model: "nomic-embed-text-v1.5", baseURL: "http://localhost:1234/v1", embeddingDims: 768 },
config: {
model: "nomic-embed-text-v1.5",
baseURL: "http://localhost:1234/v1",
embeddingDims: 768,
},
},
vectorStore: {
provider: "memory",
config: { collectionName: "test", dimension: 768 },
},
llm: {
provider: "lmstudio",
config: { baseURL: "http://localhost:1234/v1" },
},
vectorStore: { provider: "memory", config: { collectionName: "test", dimension: 768 } },
llm: { provider: "lmstudio", config: { baseURL: "http://localhost:1234/v1" } },
disableHistory: true,
});
const result = await mem.search("What does the user like?", { userId: "u1" });
const result = await mem.search("What does the user like?", {
userId: "u1",
});
expect(mockEmbedder.embed).toHaveBeenCalledWith("What does the user like?");
expect(mockVStore.search).toHaveBeenCalled();
@@ -505,19 +560,31 @@ describe("Memory – LM Studio end-to-end flow", () => {
});
it("add flow works with lmstudio LLM for fact extraction", async () => {
mockLlm.generateResponse.mockResolvedValueOnce('{"facts":["User loves sushi"]}');
mockLlm.generateResponse.mockResolvedValueOnce(
'{"facts":["User loves sushi"]}',
);
mockVStore.search.mockResolvedValue([]);
mockVStore.list.mockResolvedValue([[], 0]);
const mem = new MemoryClass({
embedder: {
provider: "lmstudio",
config: { model: "nomic-embed-text-v1.5", baseURL: "http://localhost:1234/v1", embeddingDims: 768 },
config: {
model: "nomic-embed-text-v1.5",
baseURL: "http://localhost:1234/v1",
embeddingDims: 768,
},
},
vectorStore: {
provider: "memory",
config: { collectionName: "test", dimension: 768 },
},
vectorStore: { provider: "memory", config: { collectionName: "test", dimension: 768 } },
llm: {
provider: "lmstudio",
config: { model: "meta-llama-3.1-70b", baseURL: "http://localhost:1234/v1" },
config: {
model: "meta-llama-3.1-70b",
baseURL: "http://localhost:1234/v1",
},
},
disableHistory: true,
});
-67
View File
@@ -1,67 +0,0 @@
/// <reference types="jest" />
import {
EmbedderFactory,
VectorStoreFactory,
} from "../src/utils/factory";
import { LMStudioEmbedder } from "../src/embeddings/lmstudio";
import { AzureAISearch } from "../src/vector_stores/azure_ai_search";
describe("EmbedderFactory", () => {
describe("create", () => {
it("should create LM Studio embedder with baseURL", () => {
const embedder = EmbedderFactory.create("lmstudio", {
model: "text-embedding-gte-qwen2-1.5b-instruct",
baseURL: "http://localhost:1234/v1",
});
expect(embedder).toBeInstanceOf(LMStudioEmbedder);
});
it("should throw error for unsupported embedder provider", () => {
expect(() => {
EmbedderFactory.create("unsupported-embedder", {});
}).toThrow("Unsupported embedder provider: unsupported-embedder");
});
});
});
describe("VectorStoreFactory", () => {
describe("create", () => {
it("should create Azure AI Search vector store", () => {
const config = {
collectionName: "test-memories",
serviceName: "test-service",
apiKey: "test-api-key",
embeddingModelDims: 1536,
compressionType: "none" as const,
useFloat16: false,
hybridSearch: false,
vectorFilterMode: "preFilter" as const,
};
const vectorStore = VectorStoreFactory.create("azure-ai-search", config);
expect(vectorStore).toBeInstanceOf(AzureAISearch);
});
it("should create memory vector store", () => {
const config = {
collectionName: "test-memories",
dimension: 1536,
};
const vectorStore = VectorStoreFactory.create("memory", config);
expect(vectorStore).toBeDefined();
expect(vectorStore.constructor.name).toBe("MemoryVectorStore");
});
it("should throw error for unsupported provider", () => {
const config = {};
expect(() => {
VectorStoreFactory.create("unsupported-provider", config);
}).toThrow("Unsupported vector store provider: unsupported-provider");
});
});
});
+287
View File
@@ -0,0 +1,287 @@
/**
* Factory unit tests — EmbedderFactory, LLMFactory, VectorStoreFactory, HistoryManagerFactory.
* Mocks all provider modules to avoid external dependency crashes.
*/
/// <reference types="jest" />
// Mock all provider modules before importing factory
jest.mock("../src/embeddings/openai", () => ({
OpenAIEmbedder: jest
.fn()
.mockImplementation((config) => ({ type: "openai-embedder", config })),
}));
jest.mock("../src/embeddings/ollama", () => ({
OllamaEmbedder: jest
.fn()
.mockImplementation((config) => ({ type: "ollama-embedder", config })),
}));
jest.mock("../src/embeddings/google", () => ({
GoogleEmbedder: jest
.fn()
.mockImplementation((config) => ({ type: "google-embedder", config })),
}));
jest.mock("../src/embeddings/azure", () => ({
AzureOpenAIEmbedder: jest
.fn()
.mockImplementation((config) => ({ type: "azure-embedder", config })),
}));
jest.mock("../src/embeddings/langchain", () => ({
LangchainEmbedder: jest
.fn()
.mockImplementation((config) => ({ type: "langchain-embedder", config })),
}));
jest.mock("../src/embeddings/lmstudio", () => ({
LMStudioEmbedder: jest
.fn()
.mockImplementation((config) => ({ type: "lmstudio-embedder", config })),
}));
jest.mock("../src/llms/openai", () => ({
OpenAILLM: jest
.fn()
.mockImplementation((config) => ({ type: "openai-llm", config })),
}));
jest.mock("../src/llms/openai_structured", () => ({
OpenAIStructuredLLM: jest.fn().mockImplementation((config) => ({
type: "openai-structured-llm",
config,
})),
}));
jest.mock("../src/llms/anthropic", () => ({
AnthropicLLM: jest
.fn()
.mockImplementation((config) => ({ type: "anthropic-llm", config })),
}));
jest.mock("../src/llms/groq", () => ({
GroqLLM: jest
.fn()
.mockImplementation((config) => ({ type: "groq-llm", config })),
}));
jest.mock("../src/llms/ollama", () => ({
OllamaLLM: jest
.fn()
.mockImplementation((config) => ({ type: "ollama-llm", config })),
}));
jest.mock("../src/llms/google", () => ({
GoogleLLM: jest
.fn()
.mockImplementation((config) => ({ type: "google-llm", config })),
}));
jest.mock("../src/llms/azure", () => ({
AzureOpenAILLM: jest
.fn()
.mockImplementation((config) => ({ type: "azure-llm", config })),
}));
jest.mock("../src/llms/mistral", () => ({
MistralLLM: jest
.fn()
.mockImplementation((config) => ({ type: "mistral-llm", config })),
}));
jest.mock("../src/llms/langchain", () => ({
LangchainLLM: jest
.fn()
.mockImplementation((config) => ({ type: "langchain-llm", config })),
}));
jest.mock("../src/llms/lmstudio", () => ({
LMStudioLLM: jest
.fn()
.mockImplementation((config) => ({ type: "lmstudio-llm", config })),
}));
jest.mock("../src/vector_stores/qdrant", () => ({
Qdrant: jest
.fn()
.mockImplementation((config) => ({ type: "qdrant", config })),
}));
jest.mock("../src/vector_stores/redis", () => ({
RedisDB: jest
.fn()
.mockImplementation((config) => ({ type: "redis", config })),
}));
jest.mock("../src/vector_stores/supabase", () => ({
SupabaseDB: jest
.fn()
.mockImplementation((config) => ({ type: "supabase", config })),
}));
jest.mock("../src/vector_stores/langchain", () => ({
LangchainVectorStore: jest
.fn()
.mockImplementation((config) => ({ type: "langchain-vs", config })),
}));
jest.mock("../src/vector_stores/vectorize", () => ({
VectorizeDB: jest
.fn()
.mockImplementation((config) => ({ type: "vectorize", config })),
}));
jest.mock("../src/vector_stores/azure_ai_search", () => ({
AzureAISearch: jest
.fn()
.mockImplementation((config) => ({ type: "azure-ai-search", config })),
}));
jest.mock("../src/storage/SupabaseHistoryManager", () => ({
SupabaseHistoryManager: jest
.fn()
.mockImplementation((config) => ({ type: "supabase-history", config })),
}));
import {
EmbedderFactory,
LLMFactory,
VectorStoreFactory,
HistoryManagerFactory,
} from "../src/utils/factory";
import type {
EmbeddingConfig,
LLMConfig,
VectorStoreConfig,
HistoryStoreConfig,
} from "../src/types";
const dummyEmbedConfig: EmbeddingConfig = { apiKey: "test" };
const dummyLLMConfig: LLMConfig = { apiKey: "test" };
const dummyVSConfig: VectorStoreConfig = {
collectionName: "test",
dimension: 1536,
};
// ─── EmbedderFactory ────────────────────────────────────
describe("EmbedderFactory", () => {
test.each([
["openai"],
["ollama"],
["google"],
["gemini"],
["azure_openai"],
["langchain"],
["lmstudio"],
])("creates embedder for provider '%s'", (provider) => {
expect(() =>
EmbedderFactory.create(provider, dummyEmbedConfig),
).not.toThrow();
});
test("is case-insensitive", () => {
expect(() =>
EmbedderFactory.create("OpenAI", dummyEmbedConfig),
).not.toThrow();
});
test("throws for unsupported provider", () => {
expect(() =>
EmbedderFactory.create("nonexistent", dummyEmbedConfig),
).toThrow("Unsupported embedder provider: nonexistent");
});
test("passes config to created embedder", () => {
const config: EmbeddingConfig = { apiKey: "my-key", model: "my-model" };
const result = EmbedderFactory.create("openai", config) as any;
expect(result.config).toBe(config);
});
});
// ─── LLMFactory ─────────────────────────────────────────
describe("LLMFactory", () => {
test.each([
["openai"],
["openai_structured"],
["anthropic"],
["groq"],
["ollama"],
["google"],
["gemini"],
["azure_openai"],
["mistral"],
["langchain"],
["lmstudio"],
])("creates LLM for provider '%s'", (provider) => {
expect(() => LLMFactory.create(provider, dummyLLMConfig)).not.toThrow();
});
test("is case-insensitive", () => {
expect(() => LLMFactory.create("Anthropic", dummyLLMConfig)).not.toThrow();
});
test("throws for unsupported provider", () => {
expect(() => LLMFactory.create("nonexistent", dummyLLMConfig)).toThrow(
"Unsupported LLM provider: nonexistent",
);
});
test("passes config to created LLM", () => {
const config: LLMConfig = { apiKey: "my-key", model: "gpt-4" };
const result = LLMFactory.create("openai", config) as any;
expect(result.config).toBe(config);
});
});
// ─── VectorStoreFactory ─────────────────────────────────
describe("VectorStoreFactory", () => {
test("creates memory vector store", () => {
// MemoryVectorStore is real (not mocked) — needs valid config
expect(() =>
VectorStoreFactory.create("memory", {
collectionName: "test",
dimension: 4,
}),
).not.toThrow();
});
test.each([
["qdrant"],
["redis"],
["supabase"],
["langchain"],
["vectorize"],
["azure-ai-search"],
])("creates vector store for provider '%s'", (provider) => {
expect(() =>
VectorStoreFactory.create(provider, dummyVSConfig),
).not.toThrow();
});
test("throws for unsupported provider", () => {
expect(() =>
VectorStoreFactory.create("nonexistent", dummyVSConfig),
).toThrow("Unsupported vector store provider: nonexistent");
});
});
// ─── HistoryManagerFactory ──────────────────────────────
describe("HistoryManagerFactory", () => {
test("creates SQLite history manager", () => {
const config: HistoryStoreConfig = {
provider: "sqlite",
config: { historyDbPath: ":memory:" },
};
expect(() => HistoryManagerFactory.create("sqlite", config)).not.toThrow();
});
test("creates supabase history manager", () => {
const config: HistoryStoreConfig = {
provider: "supabase",
config: { supabaseUrl: "http://test", supabaseKey: "key" },
};
expect(() =>
HistoryManagerFactory.create("supabase", config),
).not.toThrow();
});
test("creates memory history manager", () => {
const config: HistoryStoreConfig = {
provider: "memory",
config: {},
};
expect(() => HistoryManagerFactory.create("memory", config)).not.toThrow();
});
test("throws for unsupported provider", () => {
const config: HistoryStoreConfig = { provider: "bad", config: {} };
expect(() => HistoryManagerFactory.create("bad", config)).toThrow(
"Unsupported history store provider: bad",
);
});
});
@@ -61,12 +61,12 @@ describe("LMStudioEmbedder (unit)", () => {
});
it("embedBatch() returns vectors for multiple inputs", async () => {
const mockBatch = [[0.1, 0.2], [0.3, 0.4]];
const mockBatch = [
[0.1, 0.2],
[0.3, 0.4],
];
mockCreate.mockResolvedValueOnce({
data: [
{ embedding: mockBatch[0] },
{ embedding: mockBatch[1] },
],
data: [{ embedding: mockBatch[0] }, { embedding: mockBatch[1] }],
});
const embedder = new LMStudioEmbedder({
@@ -20,17 +20,26 @@ const describeIf = RUN_INTEGRATION ? describe : describe.skip;
jest.setTimeout(120_000);
async function listModels(): Promise<{ embedding: string | null; chat: string | null }> {
async function listModels(): Promise<{
embedding: string | null;
chat: string | null;
}> {
const res = await fetch(`${LMSTUDIO_BASE_URL}/models`);
const body = await res.json();
const models: any[] = body.data || [];
const embedding = models.find((m) => m.id.includes("embed") || m.id.includes("nomic"));
const chat = models.find((m) => !m.id.includes("embed") && !m.id.includes("nomic"));
const embedding = models.find(
(m) => m.id.includes("embed") || m.id.includes("nomic"),
);
const chat = models.find(
(m) => !m.id.includes("embed") && !m.id.includes("nomic"),
);
return { embedding: embedding?.id ?? null, chat: chat?.id ?? null };
}
function cosineSim(a: number[], b: number[]): number {
let dot = 0, normA = 0, normB = 0;
let dot = 0,
normA = 0,
normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
@@ -46,7 +55,10 @@ describeIf("LM Studio Integration", () => {
expect(res.ok).toBe(true);
const body = await res.json();
expect(body.data).toBeDefined();
console.log("Loaded models:", body.data.map((m: any) => m.id));
console.log(
"Loaded models:",
body.data.map((m: any) => m.id),
);
});
// ─── Embedder ────────────────────────────────────────────────────────
@@ -58,7 +70,10 @@ describeIf("LM Studio Integration", () => {
const models = await listModels();
if (!models.embedding) throw new Error("No embedding model loaded");
modelId = models.embedding;
embedder = new LMStudioEmbedder({ baseURL: LMSTUDIO_BASE_URL, model: modelId });
embedder = new LMStudioEmbedder({
baseURL: LMSTUDIO_BASE_URL,
model: modelId,
});
});
it("embed() returns a numeric vector", async () => {
@@ -94,7 +109,9 @@ describeIf("LM Studio Integration", () => {
]);
const simSimilar = cosineSim(v1, v2);
const simDifferent = cosineSim(v1, v3);
console.log(` Similar: ${simSimilar.toFixed(4)}, Different: ${simDifferent.toFixed(4)}`);
console.log(
` Similar: ${simSimilar.toFixed(4)}, Different: ${simDifferent.toFixed(4)}`,
);
expect(Number.isFinite(simSimilar)).toBe(true);
expect(Number.isFinite(simDifferent)).toBe(true);
expect(simSimilar).toBeGreaterThan(simDifferent);
+33 -15
View File
@@ -18,9 +18,15 @@ describe("LMStudioLLM (unit)", () => {
it("generateResponse() returns a text response", async () => {
mockCreate.mockResolvedValueOnce({
choices: [{
message: { content: "Hello, world!", role: "assistant", tool_calls: null },
}],
choices: [
{
message: {
content: "Hello, world!",
role: "assistant",
tool_calls: null,
},
},
],
});
const llm = new LMStudioLLM({ baseURL: "http://localhost:1234/v1" });
@@ -34,15 +40,22 @@ describe("LMStudioLLM (unit)", () => {
it("generateResponse() handles tool calls", async () => {
mockCreate.mockResolvedValueOnce({
choices: [{
message: {
content: "",
role: "assistant",
tool_calls: [{
function: { name: "get_weather", arguments: '{"city": "London"}' },
}],
choices: [
{
message: {
content: "",
role: "assistant",
tool_calls: [
{
function: {
name: "get_weather",
arguments: '{"city": "London"}',
},
},
],
},
},
}],
],
});
const llm = new LMStudioLLM({ baseURL: "http://localhost:1234/v1" });
@@ -71,9 +84,11 @@ describe("LMStudioLLM (unit)", () => {
it("generateChat() returns LLMResponse shape", async () => {
mockCreate.mockResolvedValueOnce({
choices: [{
message: { content: "I can help with that.", role: "assistant" },
}],
choices: [
{
message: { content: "I can help with that.", role: "assistant" },
},
],
});
const llm = new LMStudioLLM({ baseURL: "http://localhost:1234/v1" });
@@ -81,7 +96,10 @@ describe("LMStudioLLM (unit)", () => {
{ role: "user", content: "Help me" },
]);
expect(result).toEqual({ content: "I can help with that.", role: "assistant" });
expect(result).toEqual({
content: "I can help with that.",
role: "assistant",
});
});
it("generateChat() wraps API errors with a clear message", async () => {
+164
View File
@@ -0,0 +1,164 @@
/**
* OSS Memory unit tests — add() with inference, without inference, filter validation, metadata.
* Content-based LLM mock: system-prompt calls → facts, user-only calls → memory actions.
*/
/// <reference types="jest" />
import { Memory } from "../src/memory";
import type { MemoryConfig, MemoryItem, SearchResult } from "../src/types";
jest.setTimeout(15000);
// Mock Google modules to prevent @google/genai crash in CI
jest.mock("../src/embeddings/google", () => ({
GoogleEmbedder: jest.fn(),
}));
jest.mock("../src/llms/google", () => ({
GoogleLLM: jest.fn(),
}));
jest.mock("../src/llms/openai", () => ({
OpenAILLM: jest.fn().mockImplementation(() => ({
generateResponse: jest
.fn()
.mockImplementation(
(messages: Array<{ role: string; content: string }>) => {
const hasSystemRole = messages.some((m) => m.role === "system");
if (hasSystemRole) {
return JSON.stringify({ facts: ["extracted fact from input"] });
}
return JSON.stringify({
memory: [
{
id: "new",
event: "ADD",
text: "extracted fact from input",
old_memory: "",
new_memory: "extracted fact from input",
},
],
});
},
),
})),
}));
jest.mock("../src/embeddings/openai", () => ({
OpenAIEmbedder: jest.fn().mockImplementation(() => ({
embed: jest.fn().mockResolvedValue(new Array(1536).fill(0.1)),
embeddingDims: 1536,
})),
}));
function createMemory(overrides: Partial<MemoryConfig> = {}): Memory {
return new Memory({
version: "v1.1",
embedder: {
provider: "openai",
config: { apiKey: "test-key", model: "text-embedding-3-small" },
},
vectorStore: {
provider: "memory",
config: { collectionName: `test-add-${Date.now()}`, dimension: 1536 },
},
llm: {
provider: "openai",
config: { apiKey: "test-key", model: "gpt-4-turbo-preview" },
},
historyDbPath: ":memory:",
...overrides,
});
}
describe("Memory - add()", () => {
let memory: Memory;
const userId = `add_test_${Date.now()}`;
beforeAll(async () => {
memory = createMemory();
});
afterAll(async () => {
await memory.reset();
});
test("returns SearchResult with results array for string input", async () => {
const result: SearchResult = await memory.add("I am a software engineer", {
userId,
});
expect(Array.isArray(result.results)).toBe(true);
});
test("returns at least one result with an id", async () => {
const result: SearchResult = await memory.add("I am a software engineer", {
userId,
});
expect(result.results.length).toBeGreaterThan(0);
expect(result.results[0].id).toBeDefined();
});
test("result item has a memory string field", async () => {
const result: SearchResult = await memory.add("I am a software engineer", {
userId,
});
expect(typeof result.results[0].memory).toBe("string");
});
test("accepts Message[] input", async () => {
const messages = [
{ role: "user", content: "What is your favorite city?" },
{ role: "assistant", content: "I love Paris." },
];
const result: SearchResult = await memory.add(messages, { userId });
expect(result.results.length).toBeGreaterThan(0);
});
test("works with agentId filter instead of userId", async () => {
const result: SearchResult = await memory.add("test", {
agentId: "agent_1",
});
expect(result.results.length).toBeGreaterThan(0);
});
test("works with runId filter instead of userId", async () => {
const result: SearchResult = await memory.add("test", { runId: "run_1" });
expect(result.results.length).toBeGreaterThan(0);
});
test("throws when no userId/agentId/runId provided", async () => {
await expect(memory.add("test", {} as any)).rejects.toThrow(
"One of the filters: userId, agentId or runId is required!",
);
});
test("passes metadata through to stored memory", async () => {
const result: SearchResult = await memory.add("I love TypeScript", {
userId,
metadata: { source: "chat", tag: "programming" },
});
const stored: MemoryItem | null = await memory.get(result.results[0].id);
expect(stored).not.toBeNull();
expect(stored!.metadata).toEqual(
expect.objectContaining({ source: "chat", tag: "programming" }),
);
});
test("with infer=false skips LLM and stores messages directly", async () => {
const result: SearchResult = await memory.add("Direct storage content", {
userId,
infer: false,
});
expect(result.results.length).toBeGreaterThan(0);
// When infer=false, the literal message text is stored
expect(result.results[0].memory).toBe("Direct storage content");
});
test("with infer=false marks event as ADD in metadata", async () => {
const result: SearchResult = await memory.add("Direct fact", {
userId,
infer: false,
});
expect(result.results[0].metadata).toEqual(
expect.objectContaining({ event: "ADD" }),
);
});
});
+358
View File
@@ -0,0 +1,358 @@
/**
* OSS Memory unit tests — get, update, delete, deleteAll, getAll, search, history.
* Content-based LLM mock. Tests verify real behavior, not mock echoes.
*/
/// <reference types="jest" />
import { Memory } from "../src/memory";
import type { MemoryItem, SearchResult } from "../src/types";
jest.setTimeout(30000);
// Mock Google modules to prevent @google/genai crash in CI
jest.mock("../src/embeddings/google", () => ({
GoogleEmbedder: jest.fn(),
}));
jest.mock("../src/llms/google", () => ({
GoogleLLM: jest.fn(),
}));
jest.mock("../src/llms/openai", () => ({
OpenAILLM: jest.fn().mockImplementation(() => ({
generateResponse: jest
.fn()
.mockImplementation(
(messages: Array<{ role: string; content: string }>) => {
const hasSystemRole = messages.some((m) => m.role === "system");
if (hasSystemRole) {
return JSON.stringify({ facts: ["stored fact"] });
}
return JSON.stringify({
memory: [
{
id: "new",
event: "ADD",
text: "stored fact",
old_memory: "",
new_memory: "stored fact",
},
],
});
},
),
})),
}));
jest.mock("../src/embeddings/openai", () => ({
OpenAIEmbedder: jest.fn().mockImplementation(() => ({
embed: jest.fn().mockResolvedValue(new Array(1536).fill(0.1)),
embeddingDims: 1536,
})),
}));
function createMemory(): Memory {
return new Memory({
version: "v1.1",
embedder: {
provider: "openai",
config: { apiKey: "test-key", model: "text-embedding-3-small" },
},
vectorStore: {
provider: "memory",
config: {
collectionName: `test-crud-${Date.now()}-${Math.random()}`,
dimension: 1536,
},
},
llm: {
provider: "openai",
config: { apiKey: "test-key", model: "gpt-4-turbo-preview" },
},
historyDbPath: ":memory:",
});
}
// ─── get() ───────────────────────────────────────────────
describe("Memory - get()", () => {
let memory: Memory;
const userId = `get_test_${Date.now()}`;
beforeAll(async () => {
memory = createMemory();
});
afterAll(async () => {
await memory.reset();
});
test("returns the memory matching the ID from add()", async () => {
const addResult: SearchResult = await memory.add("I love AI", { userId });
const id = addResult.results[0].id;
const item: MemoryItem | null = await memory.get(id);
expect(item).not.toBeNull();
expect(item!.id).toBe(id);
});
test("returns a string for the memory field", async () => {
const addResult: SearchResult = await memory.add("Testing get", {
userId,
});
const item: MemoryItem | null = await memory.get(addResult.results[0].id);
expect(typeof item!.memory).toBe("string");
});
test("returns null for non-existent ID", async () => {
const item = await memory.get("nonexistent-uuid-12345");
expect(item).toBeNull();
});
test("returns hash and createdAt on stored memory", async () => {
const addResult: SearchResult = await memory.add("Hash test", { userId });
const item: MemoryItem | null = await memory.get(addResult.results[0].id);
expect(typeof item!.hash).toBe("string");
expect(item!.createdAt).toBeDefined();
expect(new Date(item!.createdAt!).toString()).not.toBe("Invalid Date");
});
});
// ─── update() ────────────────────────────────────────────
describe("Memory - update()", () => {
let memory: Memory;
const userId = `update_test_${Date.now()}`;
beforeAll(async () => {
memory = createMemory();
});
afterAll(async () => {
await memory.reset();
});
// Use infer: false for update tests — bypasses LLM, gives us a stable ID
test("returns success message", async () => {
const addResult: SearchResult = await memory.add("Original", {
userId,
infer: false,
});
const id = addResult.results[0].id;
const result = await memory.update(id, "Updated");
expect(result.message).toBe("Memory updated successfully!");
});
test("persists the updated text", async () => {
const addResult: SearchResult = await memory.add("Before update", {
userId,
infer: false,
});
const id = addResult.results[0].id;
await memory.update(id, "After update");
const item: MemoryItem | null = await memory.get(id);
expect(item!.memory).toBe("After update");
});
test("preserves createdAt and sets updatedAt", async () => {
const addResult: SearchResult = await memory.add("Timestamp test", {
userId,
infer: false,
});
const id = addResult.results[0].id;
const before: MemoryItem | null = await memory.get(id);
const originalCreatedAt = before!.createdAt;
await memory.update(id, "New text");
const after: MemoryItem | null = await memory.get(id);
expect(after!.createdAt).toBe(originalCreatedAt);
expect(after!.updatedAt).toBeDefined();
});
test("updates the hash", async () => {
const addResult: SearchResult = await memory.add("Hash change", {
userId,
infer: false,
});
const id = addResult.results[0].id;
const before: MemoryItem | null = await memory.get(id);
await memory.update(id, "Completely different text");
const after: MemoryItem | null = await memory.get(id);
expect(after!.hash).not.toBe(before!.hash);
});
});
// ─── delete() ────────────────────────────────────────────
describe("Memory - delete()", () => {
let memory: Memory;
const userId = `delete_test_${Date.now()}`;
beforeAll(async () => {
memory = createMemory();
});
afterAll(async () => {
await memory.reset();
});
test("returns success message", async () => {
const addResult: SearchResult = await memory.add("Delete me", {
userId,
infer: false,
});
const result = await memory.delete(addResult.results[0].id);
expect(result.message).toBe("Memory deleted successfully!");
});
test("get() returns null after deletion", async () => {
const addResult: SearchResult = await memory.add("Temporary", {
userId,
infer: false,
});
const id = addResult.results[0].id;
await memory.delete(id);
expect(await memory.get(id)).toBeNull();
});
});
// ─── deleteAll() ─────────────────────────────────────────
describe("Memory - deleteAll()", () => {
let memory: Memory;
const userId = `deleteall_test_${Date.now()}`;
beforeAll(async () => {
memory = createMemory();
});
afterAll(async () => {
await memory.reset();
});
test("removes all memories for the user and returns success", async () => {
await memory.add("Fact A", { userId });
await memory.add("Fact B", { userId });
const result = await memory.deleteAll({ userId });
expect(result.message).toBe("Memories deleted successfully!");
const remaining: SearchResult = await memory.getAll({ userId });
expect(remaining.results).toHaveLength(0);
});
test("throws when no filter is provided", async () => {
await expect(memory.deleteAll({} as any)).rejects.toThrow(
"At least one filter is required",
);
});
});
// ─── getAll() ────────────────────────────────────────────
describe("Memory - getAll()", () => {
let memory: Memory;
const userId = `getall_test_${Date.now()}`;
beforeAll(async () => {
memory = createMemory();
});
afterAll(async () => {
await memory.reset();
});
test("returns all stored memories for the user", async () => {
await memory.add("First", { userId });
await memory.add("Second", { userId });
const result: SearchResult = await memory.getAll({ userId });
expect(Array.isArray(result.results)).toBe(true);
expect(result.results.length).toBeGreaterThanOrEqual(2);
});
test("each result has id and memory fields", async () => {
const result: SearchResult = await memory.getAll({ userId });
for (const item of result.results) {
expect(item.id).toBeDefined();
expect(typeof item.memory).toBe("string");
}
});
test("returns empty array when no memories exist", async () => {
const result: SearchResult = await memory.getAll({
userId: "no_such_user",
});
expect(result.results).toHaveLength(0);
});
});
// ─── search() ────────────────────────────────────────────
describe("Memory - search()", () => {
let memory: Memory;
const userId = `search_test_${Date.now()}`;
beforeAll(async () => {
memory = createMemory();
await memory.add("I love TypeScript", { userId });
});
afterAll(async () => {
await memory.reset();
});
test("returns SearchResult with results array", async () => {
const result: SearchResult = await memory.search("TypeScript", { userId });
expect(Array.isArray(result.results)).toBe(true);
});
test("returns results with score field", async () => {
const result: SearchResult = await memory.search("content", { userId });
if (result.results.length > 0) {
expect(typeof result.results[0].score).toBe("number");
}
});
test("throws when no userId/agentId/runId provided", async () => {
await expect(memory.search("query", {} as any)).rejects.toThrow(
"One of the filters: userId, agentId or runId is required!",
);
});
test("returns empty results for user with no memories", async () => {
const result: SearchResult = await memory.search("query", {
userId: "empty_user",
});
expect(result.results).toHaveLength(0);
});
});
// ─── history() ───────────────────────────────────────────
describe("Memory - history()", () => {
let memory: Memory;
const userId = `history_test_${Date.now()}`;
beforeAll(async () => {
memory = createMemory();
});
afterAll(async () => {
await memory.reset();
});
test("records ADD event after add()", async () => {
const addResult: SearchResult = await memory.add("New fact", { userId });
const history = await memory.history(addResult.results[0].id);
expect(Array.isArray(history)).toBe(true);
expect(history.length).toBeGreaterThan(0);
});
test("records additional entry after update()", async () => {
const addResult: SearchResult = await memory.add("Before", { userId });
const id = addResult.results[0].id;
await memory.update(id, "After");
const history = await memory.history(id);
expect(history.length).toBeGreaterThanOrEqual(2);
});
test("returns empty array for non-existent memory ID", async () => {
const history = await memory.history("nonexistent-id");
expect(history).toHaveLength(0);
});
});
+292
View File
@@ -0,0 +1,292 @@
/**
* OSS Memory E2E tests — exercises full add/get/search/update/delete flow with mocked LLM/embedder.
* Skipped by default. Run with: MEM0_RUN_E2E=1 npx jest memory.e2e.test.ts
*/
/// <reference types="jest" />
import { Memory } from "../src/memory";
import { MemoryItem, SearchResult } from "../src/types";
const describeOrSkip = process.env.MEM0_RUN_E2E ? describe : describe.skip;
jest.setTimeout(30000);
// Mock LLM and embedder so tests run without API keys.
// Content-based mock: system-prompt calls → facts, user-only calls → memory actions.
jest.mock("../src/embeddings/google", () => ({ GoogleEmbedder: jest.fn() }));
jest.mock("../src/llms/google", () => ({ GoogleLLM: jest.fn() }));
jest.mock("../src/llms/openai", () => ({
OpenAILLM: jest.fn().mockImplementation(() => ({
generateResponse: jest
.fn()
.mockImplementation(
(messages: Array<{ role: string; content: string }>) => {
const hasSystemRole = messages.some((m) => m.role === "system");
if (hasSystemRole) {
return JSON.stringify({
facts: ["John is a software engineer"],
});
}
return JSON.stringify({
memory: [
{
id: "new",
event: "ADD",
text: "John is a software engineer",
old_memory: "",
new_memory: "John is a software engineer",
},
],
});
},
),
})),
}));
jest.mock("../src/embeddings/openai", () => ({
OpenAIEmbedder: jest.fn().mockImplementation(() => ({
embed: jest.fn().mockResolvedValue(new Array(1536).fill(0.1)),
embeddingDims: 1536,
})),
}));
describeOrSkip("Memory Class (E2E)", () => {
let memory: Memory;
const userId =
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
beforeEach(async () => {
memory = new Memory({
version: "v1.1",
embedder: {
provider: "openai",
config: { apiKey: "test-key", model: "text-embedding-3-small" },
},
vectorStore: {
provider: "memory",
config: { collectionName: "test-memories", dimension: 1536 },
},
llm: {
provider: "openai",
config: { apiKey: "test-key", model: "gpt-4-turbo-preview" },
},
historyDbPath: ":memory:",
});
await memory.reset();
});
afterEach(async () => {
await memory.reset();
});
describe("add() single memory", () => {
let result: SearchResult;
beforeEach(async () => {
result = (await memory.add(
"Hi, my name is John and I am a software engineer.",
{ userId },
)) as SearchResult;
});
it("returns a defined result", () => {
expect(result).toBeDefined();
});
it("returns results array", () => {
expect(Array.isArray(result.results)).toBe(true);
});
it("returns at least one result", () => {
expect(result.results.length).toBeGreaterThan(0);
});
it("returns result with an id", () => {
expect(result.results[0]?.id).toBeDefined();
});
});
describe("add() multiple messages", () => {
let result: SearchResult;
beforeEach(async () => {
const messages = [
{ role: "user", content: "What is your favorite city?" },
{ role: "assistant", content: "I love Paris, it is my favorite city." },
];
result = (await memory.add(messages, { userId })) as SearchResult;
});
it("returns results array", () => {
expect(Array.isArray(result.results)).toBe(true);
});
it("returns at least one result", () => {
expect(result.results.length).toBeGreaterThan(0);
});
});
describe("get() single memory", () => {
let memoryItem: MemoryItem;
let memoryId: string;
beforeEach(async () => {
const addResult = (await memory.add(
"I am a big advocate of using AI to make the world a better place",
{ userId },
)) as SearchResult;
memoryId = addResult.results[0].id;
memoryItem = (await memory.get(memoryId)) as MemoryItem;
});
it("returns the correct id", () => {
expect(memoryItem.id).toBe(memoryId);
});
it("returns a string memory", () => {
expect(typeof memoryItem.memory).toBe("string");
});
});
describe("update() memory", () => {
let memoryId: string;
beforeEach(async () => {
const addResult = (await memory.add(
"I love speaking foreign languages especially Spanish",
{ userId },
)) as SearchResult;
memoryId = addResult.results[0].id;
});
it("returns success message", async () => {
const result = await memory.update(memoryId, "Updated content");
expect(result.message).toBe("Memory updated successfully!");
});
it("persists the updated content", async () => {
await memory.update(memoryId, "Updated content");
const updated = (await memory.get(memoryId)) as MemoryItem;
expect(updated.memory).toBe("Updated content");
});
});
describe("getAll() memories for user", () => {
let result: SearchResult;
beforeEach(async () => {
await memory.add("I love visiting new places in the winters", { userId });
await memory.add("I like to rule the world", { userId });
result = (await memory.getAll({ userId })) as SearchResult;
});
it("returns results array", () => {
expect(Array.isArray(result.results)).toBe(true);
});
it("returns at least two results", () => {
expect(result.results.length).toBeGreaterThanOrEqual(2);
});
});
describe("search() memories", () => {
let result: SearchResult;
beforeEach(async () => {
await memory.add("I love programming in Python", { userId });
await memory.add("JavaScript is my favorite language", { userId });
result = (await memory.search("What programming languages do I know?", {
userId,
})) as SearchResult;
});
it("returns results array", () => {
expect(Array.isArray(result.results)).toBe(true);
});
it("returns at least one result", () => {
expect(result.results.length).toBeGreaterThan(0);
});
});
describe("history() of a memory", () => {
let history: unknown[];
beforeEach(async () => {
const addResult = (await memory.add("I like swimming in warm water", {
userId,
})) as SearchResult;
const memoryId = addResult.results[0].id;
await memory.update(memoryId, "Updated content");
history = await memory.history(memoryId);
});
it("returns an array", () => {
expect(Array.isArray(history)).toBe(true);
});
it("returns at least one entry", () => {
expect(history.length).toBeGreaterThan(0);
});
});
describe("delete() a memory", () => {
it("returns null after deletion", async () => {
const addResult = (await memory.add("I love to drink vodka in summers", {
userId,
})) as SearchResult;
const memoryId = addResult.results[0].id;
await memory.delete(memoryId);
const result = await memory.get(memoryId);
expect(result).toBeNull();
});
});
describe("Memory with Custom Configuration", () => {
let customMemory: Memory;
beforeEach(() => {
customMemory = new Memory({
version: "v1.1",
embedder: {
provider: "openai",
config: { apiKey: "test-key", model: "text-embedding-3-small" },
},
vectorStore: {
provider: "memory",
config: { collectionName: "test-memories", dimension: 1536 },
},
llm: {
provider: "openai",
config: { apiKey: "test-key", model: "gpt-4-turbo-preview" },
},
historyDbPath: ":memory:",
});
});
afterEach(async () => {
await customMemory.reset();
});
it("add() returns results with custom config", async () => {
const result = (await customMemory.add("I love programming in Python", {
userId,
})) as SearchResult;
expect(result.results.length).toBeGreaterThan(0);
});
it("search() returns results with custom config", async () => {
await customMemory.add("The weather in London is rainy today", {
userId,
});
await customMemory.add("The temperature in Paris is 25 degrees", {
userId,
});
const result = (await customMemory.search("What is the weather like?", {
userId,
})) as SearchResult;
expect(result.results.length).toBeGreaterThan(0);
});
});
});
+125
View File
@@ -0,0 +1,125 @@
/**
* OSS Memory unit tests — constructor, initialization, config validation, reset.
* Mocks LLM/Embedder at module level. No API keys needed.
*/
/// <reference types="jest" />
import { Memory } from "../src/memory";
import type { MemoryConfig, SearchResult } from "../src/types";
jest.setTimeout(15000);
// Mock Google modules to prevent @google/genai crash in CI
jest.mock("../src/embeddings/google", () => ({
GoogleEmbedder: jest.fn(),
}));
jest.mock("../src/llms/google", () => ({
GoogleLLM: jest.fn(),
}));
// ─── Content-based LLM mock (reviewer #9) ────────────────
// Returns facts for system-prompt calls, memory actions for user-only calls.
jest.mock("../src/llms/openai", () => ({
OpenAILLM: jest.fn().mockImplementation(() => ({
generateResponse: jest
.fn()
.mockImplementation(
(messages: Array<{ role: string; content: string }>) => {
const hasSystemRole = messages.some((m) => m.role === "system");
if (hasSystemRole) {
return JSON.stringify({ facts: ["test fact"] });
}
return JSON.stringify({
memory: [
{
id: "new",
event: "ADD",
text: "test fact",
old_memory: "",
new_memory: "test fact",
},
],
});
},
),
})),
}));
jest.mock("../src/embeddings/openai", () => ({
OpenAIEmbedder: jest.fn().mockImplementation(() => ({
embed: jest.fn().mockResolvedValue(new Array(1536).fill(0.1)),
embeddingDims: 1536,
})),
}));
function createMemory(overrides: Partial<MemoryConfig> = {}): Memory {
return new Memory({
version: "v1.1",
embedder: {
provider: "openai",
config: { apiKey: "test-key", model: "text-embedding-3-small" },
},
vectorStore: {
provider: "memory",
config: { collectionName: "test-init", dimension: 1536 },
},
llm: {
provider: "openai",
config: { apiKey: "test-key", model: "gpt-4-turbo-preview" },
},
historyDbPath: ":memory:",
...overrides,
});
}
describe("Memory - Initialization", () => {
test("constructs without throwing with valid config", () => {
expect(() => createMemory()).not.toThrow();
});
test("fromConfig creates instance from config dict", () => {
const config = {
version: "v1.1",
embedder: {
provider: "openai",
config: { apiKey: "test-key", model: "text-embedding-3-small" },
},
vectorStore: {
provider: "memory",
config: { collectionName: "test", dimension: 1536 },
},
llm: {
provider: "openai",
config: { apiKey: "test-key", model: "gpt-4" },
},
};
const mem = Memory.fromConfig(config);
expect(mem).toBeInstanceOf(Memory);
});
test("fromConfig throws on invalid config", () => {
expect(() => Memory.fromConfig({ invalid: true } as any)).toThrow();
});
test("disableHistory=true uses DummyHistoryManager (no crash on history)", async () => {
const mem = createMemory({ disableHistory: true });
// If DummyHistoryManager is used, history returns [] without error
const result = await mem.history("nonexistent-id");
expect(Array.isArray(result)).toBe(true);
});
});
describe("Memory - reset()", () => {
test("reset clears all stored memories", async () => {
const mem = createMemory();
const userId = `reset_test_${Date.now()}`;
await mem.add("Remember this fact", { userId });
const before: SearchResult = await mem.getAll({ userId });
expect(before.results.length).toBeGreaterThan(0);
await mem.reset();
const after: SearchResult = await mem.getAll({ userId });
expect(after.results).toHaveLength(0);
});
});
-256
View File
@@ -1,256 +0,0 @@
/// <reference types="jest" />
import { Memory } from "../src";
import { MemoryItem, SearchResult } from "../src/types";
import dotenv from "dotenv";
dotenv.config();
jest.setTimeout(30000); // Increase timeout to 30 seconds
describe("Memory Class", () => {
let memory: Memory;
const userId =
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
beforeEach(async () => {
// Initialize with default configuration
memory = new Memory({
version: "v1.1",
embedder: {
provider: "openai",
config: {
apiKey: process.env.OPENAI_API_KEY || "",
model: "text-embedding-3-small",
},
},
vectorStore: {
provider: "memory",
config: {
collectionName: "test-memories",
dimension: 1536,
},
},
llm: {
provider: "openai",
config: {
apiKey: process.env.OPENAI_API_KEY || "",
model: "gpt-4-turbo-preview",
},
},
historyDbPath: ":memory:", // Use in-memory SQLite for tests
});
// Reset all memories before each test
await memory.reset();
});
afterEach(async () => {
// Clean up after each test
await memory.reset();
});
describe("Basic Memory Operations", () => {
it("should add a single memory", async () => {
const result = (await memory.add(
"Hi, my name is John and I am a software engineer.",
userId,
)) as SearchResult;
expect(result).toBeDefined();
expect(result.results).toBeDefined();
expect(Array.isArray(result.results)).toBe(true);
expect(result.results.length).toBeGreaterThan(0);
expect(result.results[0]?.id).toBeDefined();
});
it("should add multiple messages", async () => {
const messages = [
{ role: "user", content: "What is your favorite city?" },
{ role: "assistant", content: "I love Paris, it is my favorite city." },
];
const result = (await memory.add(messages, userId)) as SearchResult;
expect(result).toBeDefined();
expect(result.results).toBeDefined();
expect(Array.isArray(result.results)).toBe(true);
expect(result.results.length).toBeGreaterThan(0);
});
it("should get a single memory", async () => {
// First add a memory
const addResult = (await memory.add(
"I am a big advocate of using AI to make the world a better place",
userId,
)) as SearchResult;
if (!addResult.results?.[0]?.id) {
throw new Error("Failed to create test memory");
}
const memoryId = addResult.results[0].id;
const result = (await memory.get(memoryId)) as MemoryItem;
expect(result).toBeDefined();
expect(result.id).toBe(memoryId);
expect(result.memory).toBeDefined();
expect(typeof result.memory).toBe("string");
});
it("should update a memory", async () => {
// First add a memory
const addResult = (await memory.add(
"I love speaking foreign languages especially Spanish",
userId,
)) as SearchResult;
if (!addResult.results?.[0]?.id) {
throw new Error("Failed to create test memory");
}
const memoryId = addResult.results[0].id;
const updatedContent = "Updated content";
const result = await memory.update(memoryId, updatedContent);
expect(result).toBeDefined();
expect(result.message).toBe("Memory updated successfully!");
// Verify the update by getting the memory
const updatedMemory = (await memory.get(memoryId)) as MemoryItem;
expect(updatedMemory.memory).toBe(updatedContent);
});
it("should get all memories for a user", async () => {
// Add a few memories
await memory.add("I love visiting new places in the winters", userId);
await memory.add("I like to rule the world", userId);
const result = (await memory.getAll(userId)) as SearchResult;
expect(result).toBeDefined();
expect(Array.isArray(result.results)).toBe(true);
expect(result.results.length).toBeGreaterThanOrEqual(2);
});
it("should search memories", async () => {
// Add some test memories
await memory.add("I love programming in Python", userId);
await memory.add("JavaScript is my favorite language", userId);
const result = (await memory.search(
"What programming languages do I know?",
userId,
)) as SearchResult;
expect(result).toBeDefined();
expect(Array.isArray(result.results)).toBe(true);
expect(result.results.length).toBeGreaterThan(0);
});
it("should get memory history", async () => {
// Add and update a memory to create history
const addResult = (await memory.add(
"I like swimming in warm water",
userId,
)) as SearchResult;
if (!addResult.results?.[0]?.id) {
throw new Error("Failed to create test memory");
}
const memoryId = addResult.results[0].id;
await memory.update(memoryId, "Updated content");
const history = await memory.history(memoryId);
expect(history).toBeDefined();
expect(Array.isArray(history)).toBe(true);
expect(history.length).toBeGreaterThan(0);
});
it("should delete a memory", async () => {
// First add a memory
const addResult = (await memory.add(
"I love to drink vodka in summers",
userId,
)) as SearchResult;
if (!addResult.results?.[0]?.id) {
throw new Error("Failed to create test memory");
}
const memoryId = addResult.results[0].id;
// Delete the memory
await memory.delete(memoryId);
// Try to get the deleted memory - should throw or return null
const result = await memory.get(memoryId);
expect(result).toBeNull();
});
});
describe("Memory with Custom Configuration", () => {
let customMemory: Memory;
beforeEach(() => {
customMemory = new Memory({
version: "v1.1",
embedder: {
provider: "openai",
config: {
apiKey: process.env.OPENAI_API_KEY || "",
model: "text-embedding-3-small",
},
},
vectorStore: {
provider: "memory",
config: {
collectionName: "test-memories",
dimension: 1536,
},
},
llm: {
provider: "openai",
config: {
apiKey: process.env.OPENAI_API_KEY || "",
model: "gpt-4-turbo-preview",
},
},
historyDbPath: ":memory:", // Use in-memory SQLite for tests
});
});
afterEach(async () => {
await customMemory.reset();
});
it("should work with custom configuration", async () => {
const result = (await customMemory.add(
"I love programming in Python",
userId,
)) as SearchResult;
expect(result).toBeDefined();
expect(result.results).toBeDefined();
expect(Array.isArray(result.results)).toBe(true);
expect(result.results.length).toBeGreaterThan(0);
});
it("should perform semantic search with custom embeddings", async () => {
// Add test memories
await customMemory.add("The weather in London is rainy today", userId);
await customMemory.add("The temperature in Paris is 25 degrees", userId);
const result = (await customMemory.search(
"What is the weather like?",
userId,
)) as SearchResult;
expect(result).toBeDefined();
expect(Array.isArray(result.results)).toBe(true);
// Results should be ordered by relevance
expect(result.results.length).toBeGreaterThan(0);
});
});
});
+228
View File
@@ -0,0 +1,228 @@
/**
* Storage manager unit tests — SQLiteManager, DummyHistoryManager.
* Uses real in-memory SQLite, no external dependencies.
*/
/// <reference types="jest" />
import { SQLiteManager } from "../src/storage/SQLiteManager";
import { DummyHistoryManager } from "../src/storage/DummyHistoryManager";
import { MemoryHistoryManager } from "../src/storage/MemoryHistoryManager";
// ─── SQLiteManager ──────────────────────────────────────
describe("SQLiteManager", () => {
let db: SQLiteManager;
beforeEach(() => {
db = new SQLiteManager(":memory:");
});
afterEach(() => {
db.close();
});
test("constructs without throwing", () => {
expect(db).toBeDefined();
});
test("addHistory inserts a record retrievable by getHistory", async () => {
await db.addHistory(
"mem1",
null,
"new value",
"ADD",
"2026-01-01T00:00:00Z",
);
const history = await db.getHistory("mem1");
expect(history).toHaveLength(1);
expect(history[0].memory_id).toBe("mem1");
expect(history[0].new_value).toBe("new value");
expect(history[0].action).toBe("ADD");
});
test("getHistory returns records in reverse chronological order", async () => {
await db.addHistory("mem1", null, "first", "ADD", "2026-01-01");
await db.addHistory("mem1", "first", "second", "UPDATE", "2026-01-02");
await db.addHistory("mem1", "second", "third", "UPDATE", "2026-01-03");
const history = await db.getHistory("mem1");
expect(history).toHaveLength(3);
// DESC order by id: most recent first
expect(history[0].new_value).toBe("third");
expect(history[2].new_value).toBe("first");
});
test("getHistory returns empty array for non-existent memory", async () => {
const history = await db.getHistory("nonexistent");
expect(history).toHaveLength(0);
});
test("addHistory stores previous_value for UPDATE", async () => {
await db.addHistory("mem1", "old text", "new text", "UPDATE");
const history = await db.getHistory("mem1");
expect(history[0].previous_value).toBe("old text");
expect(history[0].new_value).toBe("new text");
});
test("addHistory stores null new_value for DELETE", async () => {
await db.addHistory(
"mem1",
"deleted text",
null,
"DELETE",
undefined,
undefined,
1,
);
const history = await db.getHistory("mem1");
expect(history[0].action).toBe("DELETE");
expect(history[0].new_value).toBeNull();
expect(history[0].is_deleted).toBe(1);
});
test("reset clears all history and recreates table", async () => {
await db.addHistory("mem1", null, "data", "ADD");
await db.addHistory("mem2", null, "data", "ADD");
await db.reset();
expect(await db.getHistory("mem1")).toHaveLength(0);
expect(await db.getHistory("mem2")).toHaveLength(0);
// Table still works after reset
await db.addHistory("mem3", null, "after reset", "ADD");
expect(await db.getHistory("mem3")).toHaveLength(1);
});
test("stores createdAt and updatedAt timestamps", async () => {
const created = "2026-03-17T10:00:00Z";
const updated = "2026-03-17T11:00:00Z";
await db.addHistory("mem1", null, "data", "ADD", created, updated);
const history = await db.getHistory("mem1");
expect(history[0].created_at).toBe(created);
expect(history[0].updated_at).toBe(updated);
});
test("handles multiple memories independently", async () => {
await db.addHistory("mem1", null, "data1", "ADD");
await db.addHistory("mem2", null, "data2", "ADD");
expect(await db.getHistory("mem1")).toHaveLength(1);
expect(await db.getHistory("mem2")).toHaveLength(1);
});
});
// ─── DummyHistoryManager ────────────────────────────────
describe("DummyHistoryManager", () => {
let dummy: DummyHistoryManager;
beforeEach(() => {
dummy = new DummyHistoryManager();
});
test("constructs without throwing", () => {
expect(dummy).toBeDefined();
});
test("addHistory is a no-op that resolves", async () => {
await expect(
dummy.addHistory("id", null, "val", "ADD"),
).resolves.toBeUndefined();
});
test("getHistory returns empty array", async () => {
const result = await dummy.getHistory("any-id");
expect(result).toEqual([]);
});
test("reset resolves without throwing", async () => {
await expect(dummy.reset()).resolves.toBeUndefined();
});
test("close does not throw", () => {
expect(() => dummy.close()).not.toThrow();
});
});
// ─── MemoryHistoryManager ───────────────────────────────
describe("MemoryHistoryManager", () => {
let mgr: MemoryHistoryManager;
beforeEach(() => {
mgr = new MemoryHistoryManager();
});
test("constructs without throwing", () => {
expect(mgr).toBeDefined();
});
test("addHistory + getHistory round-trips correctly", async () => {
await mgr.addHistory(
"mem1",
null,
"new value",
"ADD",
"2026-01-01T00:00:00Z",
);
const history = await mgr.getHistory("mem1");
expect(history).toHaveLength(1);
expect(history[0].memory_id).toBe("mem1");
expect(history[0].new_value).toBe("new value");
expect(history[0].action).toBe("ADD");
});
test("getHistory returns entries sorted by date descending", async () => {
await mgr.addHistory("mem1", null, "first", "ADD", "2026-01-01T00:00:00Z");
await mgr.addHistory(
"mem1",
"first",
"second",
"UPDATE",
"2026-01-02T00:00:00Z",
);
await mgr.addHistory(
"mem1",
"second",
"third",
"UPDATE",
"2026-01-03T00:00:00Z",
);
const history = await mgr.getHistory("mem1");
expect(history).toHaveLength(3);
expect(history[0].new_value).toBe("third");
expect(history[2].new_value).toBe("first");
});
test("getHistory returns empty array for non-existent memory", async () => {
expect(await mgr.getHistory("nonexistent")).toHaveLength(0);
});
test("getHistory caps at 100 entries", async () => {
for (let i = 0; i < 110; i++) {
await mgr.addHistory(
"mem1",
null,
`entry-${i}`,
"ADD",
`2026-01-01T00:${String(i).padStart(2, "0")}:00Z`,
);
}
const history = await mgr.getHistory("mem1");
expect(history).toHaveLength(100);
});
test("reset clears all entries", async () => {
await mgr.addHistory("mem1", null, "data", "ADD");
await mgr.addHistory("mem2", null, "data", "ADD");
await mgr.reset();
expect(await mgr.getHistory("mem1")).toHaveLength(0);
expect(await mgr.getHistory("mem2")).toHaveLength(0);
});
test("close does not throw", () => {
expect(() => mgr.close()).not.toThrow();
});
test("isolates history by memory_id", async () => {
await mgr.addHistory("mem1", null, "d1", "ADD");
await mgr.addHistory("mem2", null, "d2", "ADD");
expect(await mgr.getHistory("mem1")).toHaveLength(1);
expect(await mgr.getHistory("mem2")).toHaveLength(1);
});
});
@@ -0,0 +1,198 @@
/**
* MemoryVectorStore unit tests — insert, search, get, update, delete, list, cosine similarity.
* Uses real SQLite in-memory DB, no external dependencies.
*/
/// <reference types="jest" />
import { MemoryVectorStore } from "../src/vector_stores/memory";
import type { VectorStoreResult } from "../src/types";
const DIM = 4; // Small dimension for fast tests
function createStore(): MemoryVectorStore {
return new MemoryVectorStore({
collectionName: "test",
dimension: DIM,
dbPath: ":memory:",
});
}
function vec(values: number[]): number[] {
return values;
}
describe("MemoryVectorStore - insert + get", () => {
let store: MemoryVectorStore;
beforeAll(() => {
store = createStore();
});
test("inserts and retrieves a vector by ID", async () => {
await store.insert(
[vec([1, 0, 0, 0])],
["id1"],
[{ data: "hello", userId: "u1" }],
);
const result: VectorStoreResult | null = await store.get("id1");
expect(result).not.toBeNull();
expect(result!.id).toBe("id1");
expect(result!.payload.data).toBe("hello");
});
test("returns null for non-existent ID", async () => {
const result = await store.get("nonexistent");
expect(result).toBeNull();
});
test("throws on dimension mismatch during insert", async () => {
await expect(
store.insert([vec([1, 0, 0])], ["bad"], [{ data: "x" }]),
).rejects.toThrow("Vector dimension mismatch");
});
});
describe("MemoryVectorStore - search", () => {
let store: MemoryVectorStore;
beforeAll(async () => {
store = createStore();
await store.insert(
[vec([1, 0, 0, 0]), vec([0, 1, 0, 0]), vec([0.9, 0.1, 0, 0])],
["a", "b", "c"],
[
{ data: "north", userId: "u1" },
{ data: "east", userId: "u1" },
{ data: "north-ish", userId: "u2" },
],
);
});
test("returns results sorted by cosine similarity descending", async () => {
const results: VectorStoreResult[] = await store.search(
vec([1, 0, 0, 0]),
10,
);
expect(results.length).toBeGreaterThan(0);
expect(results[0].id).toBe("a"); // exact match
// scores should be descending
for (let i = 1; i < results.length; i++) {
expect(results[i - 1].score!).toBeGreaterThanOrEqual(results[i].score!);
}
});
test("respects limit parameter", async () => {
const results = await store.search(vec([1, 0, 0, 0]), 1);
expect(results).toHaveLength(1);
});
test("filters by userId", async () => {
const results = await store.search(vec([1, 0, 0, 0]), 10, { userId: "u2" });
expect(results.every((r) => r.payload.userId === "u2")).toBe(true);
});
test("returns empty when filter matches nothing", async () => {
const results = await store.search(vec([1, 0, 0, 0]), 10, {
userId: "nobody",
});
expect(results).toHaveLength(0);
});
test("throws on query dimension mismatch", async () => {
await expect(store.search(vec([1, 0]), 10)).rejects.toThrow(
"Query dimension mismatch",
);
});
});
describe("MemoryVectorStore - update", () => {
let store: MemoryVectorStore;
beforeAll(async () => {
store = createStore();
await store.insert([vec([1, 0, 0, 0])], ["upd1"], [{ data: "original" }]);
});
test("updates payload and vector", async () => {
await store.update("upd1", vec([0, 1, 0, 0]), { data: "updated" });
const result = await store.get("upd1");
expect(result!.payload.data).toBe("updated");
});
test("throws on dimension mismatch during update", async () => {
await expect(
store.update("upd1", vec([1, 0]), { data: "bad" }),
).rejects.toThrow("Vector dimension mismatch");
});
});
describe("MemoryVectorStore - delete + deleteCol", () => {
test("delete removes a vector", async () => {
const store = createStore();
await store.insert([vec([1, 0, 0, 0])], ["del1"], [{ data: "bye" }]);
await store.delete("del1");
expect(await store.get("del1")).toBeNull();
});
test("deleteCol clears all vectors", async () => {
const store = createStore();
await store.insert(
[vec([1, 0, 0, 0]), vec([0, 1, 0, 0])],
["x", "y"],
[{ data: "a" }, { data: "b" }],
);
await store.deleteCol();
const [results] = await store.list();
expect(results).toHaveLength(0);
});
});
describe("MemoryVectorStore - list", () => {
let store: MemoryVectorStore;
beforeAll(async () => {
store = createStore();
await store.insert(
[vec([1, 0, 0, 0]), vec([0, 1, 0, 0]), vec([0, 0, 1, 0])],
["l1", "l2", "l3"],
[
{ data: "a", userId: "u1" },
{ data: "b", userId: "u1" },
{ data: "c", userId: "u2" },
],
);
});
test("returns all vectors without filter", async () => {
const [results, count] = await store.list();
expect(count).toBe(3);
expect(results).toHaveLength(3);
});
test("filters by userId", async () => {
const [results, count] = await store.list({ userId: "u1" });
expect(count).toBe(2);
expect(results.every((r) => r.payload.userId === "u1")).toBe(true);
});
test("respects limit", async () => {
const [results] = await store.list(undefined, 1);
expect(results).toHaveLength(1);
});
});
describe("MemoryVectorStore - userId tracking", () => {
test("getUserId generates and persists a random ID", async () => {
const store = createStore();
const id = await store.getUserId();
expect(typeof id).toBe("string");
expect(id.length).toBeGreaterThan(0);
// Calling again returns same ID
expect(await store.getUserId()).toBe(id);
});
test("setUserId overrides the stored ID", async () => {
const store = createStore();
await store.setUserId("custom-id");
expect(await store.getUserId()).toBe("custom-id");
});
});