Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 137cdd6704 | |||
| ba8b6616bb | |||
| 6bcfd3fad8 | |||
| 2633b51303 | |||
| 27c05c765f | |||
| 205fffa3c7 | |||
| 388b61a099 | |||
| 1b7c904b98 | |||
| a14bc1ff24 | |||
| 2131da5101 | |||
| 277c4e9738 | |||
| dfa5b6d9f0 | |||
| 8c80b2e7c8 |
@@ -57,9 +57,9 @@ jobs:
|
|||||||
working-directory: mem0-ts
|
working-directory: mem0-ts
|
||||||
run: pnpm run build
|
run: pnpm run build
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run unit tests
|
||||||
working-directory: mem0-ts
|
working-directory: mem0-ts
|
||||||
run: pnpm run test:ci
|
run: pnpm run test:unit
|
||||||
|
|
||||||
- name: Verify package exports
|
- name: Verify package exports
|
||||||
working-directory: mem0-ts
|
working-directory: mem0-ts
|
||||||
@@ -73,3 +73,38 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: coverage-report
|
name: coverage-report
|
||||||
path: mem0-ts/coverage/
|
path: mem0-ts/coverage/
|
||||||
|
|
||||||
|
integration_ts_sdk:
|
||||||
|
needs: build_ts_sdk
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
max-parallel: 1
|
||||||
|
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: Build
|
||||||
|
working-directory: mem0-ts
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Run integration tests (with cleanup)
|
||||||
|
working-directory: mem0-ts
|
||||||
|
env:
|
||||||
|
MEM0_API_KEY: ${{ secrets.MEM0_API_KEY }}
|
||||||
|
run: pnpm run test:integration
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
|
module.exports = {
|
||||||
|
...require("./jest.config"),
|
||||||
|
testMatch: ["**/integration/**/*.test.ts"],
|
||||||
|
globalSetup: "<rootDir>/src/client/tests/integration/global-setup.ts",
|
||||||
|
globalTeardown: "<rootDir>/src/client/tests/integration/global-teardown.ts",
|
||||||
|
};
|
||||||
@@ -38,6 +38,8 @@
|
|||||||
"example": "ts-node src/oss/examples/vector-stores/index.ts",
|
"example": "ts-node src/oss/examples/vector-stores/index.ts",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:ci": "jest --coverage --ci",
|
"test:ci": "jest --coverage --ci",
|
||||||
|
"test:unit": "jest --coverage --ci --testPathIgnorePatterns='/node_modules/' '/dist/' 'integration'",
|
||||||
|
"test:integration": "jest --config jest.integration.config.js --forceExit",
|
||||||
"test:ts": "jest --config jest.config.js",
|
"test:ts": "jest --config jest.config.js",
|
||||||
"test:watch": "jest --config jest.config.js --watch",
|
"test:watch": "jest --config jest.config.js --watch",
|
||||||
"format": "npm run clean && prettier --write .",
|
"format": "npm run clean && prettier --write .",
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Integration tests: Batch operations.
|
||||||
|
*
|
||||||
|
* Tests batch update and batch delete against the real API.
|
||||||
|
*
|
||||||
|
* Run: MEM0_API_KEY=your-key npx jest batch.test.ts --forceExit
|
||||||
|
*/
|
||||||
|
import { MemoryClient } from "../../mem0";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
import {
|
||||||
|
describeIntegration,
|
||||||
|
createTestClient,
|
||||||
|
suppressTelemetryNoise,
|
||||||
|
seedTestMemories,
|
||||||
|
cleanupTestUser,
|
||||||
|
} from "./helpers";
|
||||||
|
|
||||||
|
jest.setTimeout(120_000);
|
||||||
|
|
||||||
|
const TEST_USER_ID = `integration-batch-${randomUUID()}`;
|
||||||
|
|
||||||
|
describeIntegration("MemoryClient Integration — Batch Operations", () => {
|
||||||
|
let client: MemoryClient;
|
||||||
|
let cleanup: () => void;
|
||||||
|
let memoryIds: string[] = [];
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
cleanup = suppressTelemetryNoise();
|
||||||
|
client = createTestClient();
|
||||||
|
memoryIds = await seedTestMemories(client, TEST_USER_ID);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanupTestUser(client, TEST_USER_ID);
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("batch updates memories", async () => {
|
||||||
|
expect(memoryIds.length).toBeGreaterThanOrEqual(1);
|
||||||
|
|
||||||
|
const batchPayload = memoryIds
|
||||||
|
.slice(0, Math.min(2, memoryIds.length))
|
||||||
|
.map((id) => ({
|
||||||
|
memoryId: id,
|
||||||
|
text: `Batch updated content for ${id}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await client.batchUpdate(batchPayload);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
|
||||||
|
// Verify the update took effect on at least one memory
|
||||||
|
const updated = await client.get(memoryIds[0]);
|
||||||
|
expect(typeof updated.memory).toBe("string");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("batch deletes memories that exist", async () => {
|
||||||
|
// Use one of the seeded memory IDs that we know exists
|
||||||
|
expect(memoryIds.length).toBeGreaterThanOrEqual(1);
|
||||||
|
|
||||||
|
const toDelete = [memoryIds[memoryIds.length - 1]];
|
||||||
|
const result = await client.batchDelete(toDelete);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
/**
|
||||||
|
* Integration tests: Memory CRUD operations.
|
||||||
|
*
|
||||||
|
* Tests the full lifecycle: add → get → getAll → update → delete.
|
||||||
|
* Validates response shapes against the real API.
|
||||||
|
*
|
||||||
|
* Run: MEM0_API_KEY=your-key npx jest crud.test.ts --forceExit
|
||||||
|
*/
|
||||||
|
import { MemoryClient } from "../../mem0";
|
||||||
|
import { MemoryError } from "../../../common/exceptions";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
import {
|
||||||
|
describeIntegration,
|
||||||
|
createTestClient,
|
||||||
|
suppressTelemetryNoise,
|
||||||
|
waitForMemories,
|
||||||
|
cleanupTestUser,
|
||||||
|
} from "./helpers";
|
||||||
|
|
||||||
|
jest.setTimeout(120_000);
|
||||||
|
|
||||||
|
const TEST_USER_ID = `integration-crud-${randomUUID()}`;
|
||||||
|
|
||||||
|
describeIntegration("MemoryClient Integration — CRUD", () => {
|
||||||
|
let client: MemoryClient;
|
||||||
|
let cleanup: () => void;
|
||||||
|
let memoryIds: string[] = [];
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
cleanup = suppressTelemetryNoise();
|
||||||
|
client = createTestClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanupTestUser(client, TEST_USER_ID);
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Add ──────────────────────────────────────────────────
|
||||||
|
describe("add memories", () => {
|
||||||
|
test("add returns a pending response with event_id", async () => {
|
||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
role: "user" as const,
|
||||||
|
content: "Hi, I'm integration-test-user. My favorite color is blue.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant" as const,
|
||||||
|
content:
|
||||||
|
"Nice to meet you! I'll remember that your favorite color is blue.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await client.add(messages, { user_id: TEST_USER_ID });
|
||||||
|
|
||||||
|
// API processes memories asynchronously — returns PENDING
|
||||||
|
expect(Array.isArray(result)).toBe(true);
|
||||||
|
expect(result.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Validate response shape
|
||||||
|
for (const item of result) {
|
||||||
|
expect(item).toHaveProperty("status");
|
||||||
|
expect(item).toHaveProperty("event_id");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("adds a second batch of messages", async () => {
|
||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
role: "user" as const,
|
||||||
|
content: "I work as a software engineer at Acme Corp.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant" as const,
|
||||||
|
content: "Got it, you're a software engineer at Acme Corp!",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await client.add(messages, { user_id: TEST_USER_ID });
|
||||||
|
expect(Array.isArray(result)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("memories become available after async processing", async () => {
|
||||||
|
const memories = await waitForMemories(client, TEST_USER_ID, 1);
|
||||||
|
|
||||||
|
expect(memories.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Store IDs for later tests
|
||||||
|
memoryIds = memories.map((m) => m.id);
|
||||||
|
expect(memoryIds.length).toBeGreaterThan(0);
|
||||||
|
expect(typeof memoryIds[0]).toBe("string");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Get by ID ────────────────────────────────────────────
|
||||||
|
describe("get memory by ID", () => {
|
||||||
|
test("retrieves a specific memory with correct shape", async () => {
|
||||||
|
const memoryId = memoryIds[0];
|
||||||
|
expect(memoryId).toBeDefined();
|
||||||
|
|
||||||
|
const memory = await client.get(memoryId);
|
||||||
|
|
||||||
|
expect(memory.id).toBe(memoryId);
|
||||||
|
expect(typeof memory.memory).toBe("string");
|
||||||
|
expect(memory.memory!.length).toBeGreaterThan(0);
|
||||||
|
expect(typeof memory.user_id).toBe("string");
|
||||||
|
expect(
|
||||||
|
memory.metadata === null || typeof memory.metadata === "object",
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
Array.isArray(memory.categories) || memory.categories === null,
|
||||||
|
).toBe(true);
|
||||||
|
expect(new Date(memory.created_at || "").toString()).not.toBe(
|
||||||
|
"Invalid Date",
|
||||||
|
);
|
||||||
|
expect(new Date(memory.updated_at || "").toString()).not.toBe(
|
||||||
|
"Invalid Date",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Get all ──────────────────────────────────────────────
|
||||||
|
describe("get all memories", () => {
|
||||||
|
test("returns all memories for test user", async () => {
|
||||||
|
const memories = await client.getAll({ user_id: TEST_USER_ID });
|
||||||
|
|
||||||
|
expect(Array.isArray(memories)).toBe(true);
|
||||||
|
expect(memories.length).toBeGreaterThanOrEqual(memoryIds.length);
|
||||||
|
|
||||||
|
for (const mem of memories) {
|
||||||
|
expect(typeof mem.id).toBe("string");
|
||||||
|
expect(typeof mem.memory).toBe("string");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns paginated results with page and page_size", async () => {
|
||||||
|
const page1 = await client.getAll({
|
||||||
|
user_id: TEST_USER_ID,
|
||||||
|
page: 1,
|
||||||
|
page_size: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Paginated response is an object with results array
|
||||||
|
expect(page1).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Update ───────────────────────────────────────────────
|
||||||
|
describe("update memory", () => {
|
||||||
|
test("updates memory text and verifies the content changed", async () => {
|
||||||
|
const memoryId = memoryIds[0];
|
||||||
|
|
||||||
|
// Read original text before update
|
||||||
|
const original = await client.get(memoryId);
|
||||||
|
const originalText = original.memory;
|
||||||
|
|
||||||
|
await client.update(memoryId, {
|
||||||
|
text: "My favorite color is green (updated)",
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = await client.get(memoryId);
|
||||||
|
expect(typeof updated.memory).toBe("string");
|
||||||
|
expect(updated.memory).not.toBe(originalText);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("updates memory metadata", async () => {
|
||||||
|
const memoryId = memoryIds[0];
|
||||||
|
|
||||||
|
await client.update(memoryId, {
|
||||||
|
metadata: { source: "integration-test", priority: "high" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = await client.get(memoryId);
|
||||||
|
expect(updated.metadata).toBeDefined();
|
||||||
|
expect(updated.metadata.source).toBe("integration-test");
|
||||||
|
expect(updated.metadata.priority).toBe("high");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Delete single ────────────────────────────────────────
|
||||||
|
describe("delete memory", () => {
|
||||||
|
test("deletes a single memory by ID", async () => {
|
||||||
|
const memoryId = memoryIds[0];
|
||||||
|
expect(memoryId).toBeDefined();
|
||||||
|
|
||||||
|
const result = await client.delete(memoryId);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(typeof result.message).toBe("string");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getting deleted memory throws MemoryError", async () => {
|
||||||
|
const memoryId = memoryIds[0];
|
||||||
|
await expect(client.get(memoryId)).rejects.toThrow(MemoryError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Edge cases ──────────────────────────────────────────
|
||||||
|
describe("edge cases", () => {
|
||||||
|
test("add with metadata attaches metadata to the memory", async () => {
|
||||||
|
const result = await client.add(
|
||||||
|
[
|
||||||
|
{ role: "user" as const, content: "I prefer dark mode in all apps." },
|
||||||
|
{
|
||||||
|
role: "assistant" as const,
|
||||||
|
content: "Noted, dark mode preference saved!",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
user_id: TEST_USER_ID,
|
||||||
|
metadata: { source: "integration-test", category: "preferences" },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(Array.isArray(result)).toBe(true);
|
||||||
|
expect(result.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getAll for non-existent user returns empty array", async () => {
|
||||||
|
const memories = await client.getAll({
|
||||||
|
user_id: `nonexistent-user-${randomUUID()}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(Array.isArray(memories)).toBe(true);
|
||||||
|
expect(memories.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("deleteAll for non-existent user does not throw", async () => {
|
||||||
|
const result = await client.deleteAll({
|
||||||
|
user_id: `nonexistent-user-${randomUUID()}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(typeof result.message).toBe("string");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Delete all + delete user ─────────────────────────────
|
||||||
|
describe("cleanup operations", () => {
|
||||||
|
test("deletes all memories for test user", async () => {
|
||||||
|
const result = await client.deleteAll({ user_id: TEST_USER_ID });
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(typeof result.message).toBe("string");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("deletes the test user entity", async () => {
|
||||||
|
const result = await client.deleteUsers({ user_id: TEST_USER_ID });
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.message).toBe("Entity deleted successfully.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Jest global setup for integration tests.
|
||||||
|
*
|
||||||
|
* Runs a full project cleanup before any integration test starts,
|
||||||
|
* then waits 10 seconds for the async cleanup to propagate.
|
||||||
|
*/
|
||||||
|
import { MemoryClient } from "../../mem0";
|
||||||
|
|
||||||
|
export default async function globalSetup() {
|
||||||
|
const apiKey = process.env.MEM0_API_KEY;
|
||||||
|
if (!apiKey) return; // skip if no key — tests will be skipped too
|
||||||
|
|
||||||
|
const client = new MemoryClient({ apiKey });
|
||||||
|
await client.ping();
|
||||||
|
|
||||||
|
console.log("[integration] Running pre-test cleanup...");
|
||||||
|
|
||||||
|
// Full project wipe — all four filters set explicitly
|
||||||
|
try {
|
||||||
|
await client.deleteAll({
|
||||||
|
user_id: "*",
|
||||||
|
agent_id: "*",
|
||||||
|
app_id: "*",
|
||||||
|
run_id: "*",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// ignore — may 404 if no data exists
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.deleteUsers();
|
||||||
|
} catch {
|
||||||
|
// ignore — may throw "No entities to delete"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait 10 seconds for async cleanup to propagate
|
||||||
|
console.log("[integration] Waiting 10s for cleanup to propagate...");
|
||||||
|
await new Promise((r) => setTimeout(r, 10_000));
|
||||||
|
console.log("[integration] Pre-test cleanup done.");
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Jest global teardown for integration tests.
|
||||||
|
*
|
||||||
|
* Runs a full project cleanup after all integration tests complete
|
||||||
|
* so no test data is left behind.
|
||||||
|
*/
|
||||||
|
import { MemoryClient } from "../../mem0";
|
||||||
|
|
||||||
|
export default async function globalTeardown() {
|
||||||
|
const apiKey = process.env.MEM0_API_KEY;
|
||||||
|
if (!apiKey) return;
|
||||||
|
|
||||||
|
const client = new MemoryClient({ apiKey });
|
||||||
|
await client.ping();
|
||||||
|
|
||||||
|
console.log("[integration] Running post-test cleanup...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.deleteAll({
|
||||||
|
user_id: "*",
|
||||||
|
agent_id: "*",
|
||||||
|
app_id: "*",
|
||||||
|
run_id: "*",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.deleteUsers();
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[integration] Post-test cleanup done.");
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
/**
|
||||||
|
* Shared helpers for MemoryClient real integration tests.
|
||||||
|
*
|
||||||
|
* Provides environment gating, client factory, polling helpers,
|
||||||
|
* and console suppression for telemetry noise.
|
||||||
|
*
|
||||||
|
* All helpers use only the SDK's public API — no internal method access.
|
||||||
|
*/
|
||||||
|
import { MemoryClient } from "../../mem0";
|
||||||
|
import type { Memory } from "../../mem0.types";
|
||||||
|
import { NetworkError, RateLimitError } from "../../../common/exceptions";
|
||||||
|
|
||||||
|
// ─── Environment gate ────────────────────────────────────
|
||||||
|
export const API_KEY = process.env.MEM0_API_KEY;
|
||||||
|
export const describeIntegration = API_KEY ? describe : describe.skip;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a MemoryClient with the real API key.
|
||||||
|
* Call this inside beforeAll — not at module scope — so it only
|
||||||
|
* runs when the suite is not skipped.
|
||||||
|
*/
|
||||||
|
export function createTestClient(): MemoryClient {
|
||||||
|
return new MemoryClient({ apiKey: API_KEY! });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry an async SDK call on transient errors (NetworkError, RateLimitError).
|
||||||
|
* Use this to wrap any SDK call that may flake in CI.
|
||||||
|
*/
|
||||||
|
export async function withRetry<T>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
maxRetries = 2,
|
||||||
|
): Promise<T> {
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} catch (error: any) {
|
||||||
|
const isTransient =
|
||||||
|
error instanceof NetworkError || error instanceof RateLimitError;
|
||||||
|
if (isTransient && attempt < maxRetries) {
|
||||||
|
await new Promise((r) => setTimeout(r, 3_000 * attempt));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("withRetry: unreachable");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Poll getAll until memories appear for a user.
|
||||||
|
* The Mem0 API processes memories asynchronously — after add()
|
||||||
|
* we need to wait for them to be available.
|
||||||
|
*/
|
||||||
|
export async function waitForMemories(
|
||||||
|
client: MemoryClient,
|
||||||
|
userId: string,
|
||||||
|
minCount: number,
|
||||||
|
maxWaitMs = 60_000,
|
||||||
|
): Promise<Memory[]> {
|
||||||
|
const start = Date.now();
|
||||||
|
while (Date.now() - start < maxWaitMs) {
|
||||||
|
const memories = await withRetry(() => client.getAll({ user_id: userId }));
|
||||||
|
if (Array.isArray(memories) && memories.length >= minCount) {
|
||||||
|
return memories;
|
||||||
|
}
|
||||||
|
await new Promise((r) => setTimeout(r, 3_000));
|
||||||
|
}
|
||||||
|
return await client.getAll({ user_id: userId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Poll search until results appear. Only used by search tests —
|
||||||
|
* other test files should NOT call this to avoid wasting API credits.
|
||||||
|
*/
|
||||||
|
export async function waitForSearchResults(
|
||||||
|
client: MemoryClient,
|
||||||
|
query: string,
|
||||||
|
options: Record<string, any>,
|
||||||
|
maxWaitMs = 60_000,
|
||||||
|
): Promise<Memory[]> {
|
||||||
|
const start = Date.now();
|
||||||
|
while (Date.now() - start < maxWaitMs) {
|
||||||
|
const results = await withRetry(() => client.search(query, options));
|
||||||
|
if (Array.isArray(results) && results.length > 0) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
await new Promise((r) => setTimeout(r, 3_000));
|
||||||
|
}
|
||||||
|
return await client.search(query, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suppress telemetry console noise during tests.
|
||||||
|
* Returns a cleanup function to call in afterAll.
|
||||||
|
*/
|
||||||
|
export function suppressTelemetryNoise(): () => void {
|
||||||
|
const originalError = console.error;
|
||||||
|
const originalWarn = console.warn;
|
||||||
|
|
||||||
|
jest.spyOn(console, "error").mockImplementation((...args: unknown[]) => {
|
||||||
|
if (
|
||||||
|
String(args[0] ?? "").match(
|
||||||
|
/Telemetry|Failed to initialize|Failed to capture/,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
originalError(...args);
|
||||||
|
});
|
||||||
|
jest.spyOn(console, "warn").mockImplementation((...args: unknown[]) => {
|
||||||
|
if (String(args[0] ?? "").match(/telemetry|Telemetry/)) return;
|
||||||
|
originalWarn(...args);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => jest.restoreAllMocks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add test memories and wait for them to be processed.
|
||||||
|
* Returns the memory IDs once available via getAll.
|
||||||
|
*
|
||||||
|
* NOTE: This only waits for the listing index. If your test needs
|
||||||
|
* search results, call waitForSearchResults() separately.
|
||||||
|
*/
|
||||||
|
export async function seedTestMemories(
|
||||||
|
client: MemoryClient,
|
||||||
|
userId: string,
|
||||||
|
): Promise<string[]> {
|
||||||
|
await withRetry(() =>
|
||||||
|
client.add(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
role: "user" as const,
|
||||||
|
content: "Hi, I'm integration-test-user. My favorite color is blue.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant" as const,
|
||||||
|
content:
|
||||||
|
"Nice to meet you! I'll remember that your favorite color is blue.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{ user_id: userId },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await withRetry(() =>
|
||||||
|
client.add(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
role: "user" as const,
|
||||||
|
content: "I work as a software engineer at Acme Corp.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant" as const,
|
||||||
|
content: "Got it, you're a software engineer at Acme Corp!",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{ user_id: userId },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const memories = await waitForMemories(client, userId, 1);
|
||||||
|
return memories.map((m) => m.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up all test data for a user. Best-effort — ignores errors.
|
||||||
|
*/
|
||||||
|
export async function cleanupTestUser(
|
||||||
|
client: MemoryClient,
|
||||||
|
userId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await client.deleteAll({ user_id: userId });
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await client.deleteUsers({ user_id: userId });
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full project wipe — deletes all memories and all entities.
|
||||||
|
* Equivalent to Python SDK's:
|
||||||
|
* client.delete_all(user_id="*", agent_id="*", app_id="*", run_id="*")
|
||||||
|
*
|
||||||
|
* Used as cleanup before and after integration test runs so tests
|
||||||
|
* start from a clean slate and don't leave data behind.
|
||||||
|
*/
|
||||||
|
export async function fullProjectCleanup(client: MemoryClient): Promise<void> {
|
||||||
|
// Delete all memories — all four filters set explicitly
|
||||||
|
try {
|
||||||
|
await client.deleteAll({
|
||||||
|
user_id: "*",
|
||||||
|
agent_id: "*",
|
||||||
|
app_id: "*",
|
||||||
|
run_id: "*",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// ignore — may 404 if no data exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all entities (users, agents, apps, runs)
|
||||||
|
try {
|
||||||
|
await client.deleteUsers();
|
||||||
|
} catch {
|
||||||
|
// ignore — may throw "No entities to delete"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* Integration tests: Client initialization and error handling.
|
||||||
|
*
|
||||||
|
* Tests ping, org/project resolution, and invalid credentials.
|
||||||
|
* These tests do NOT need pre-seeded memories.
|
||||||
|
*
|
||||||
|
* Run: MEM0_API_KEY=your-key npx jest initialization.test.ts --forceExit
|
||||||
|
*/
|
||||||
|
import { MemoryClient } from "../../mem0";
|
||||||
|
import {
|
||||||
|
MemoryError,
|
||||||
|
MemoryNotFoundError,
|
||||||
|
ValidationError,
|
||||||
|
} from "../../../common/exceptions";
|
||||||
|
import {
|
||||||
|
describeIntegration,
|
||||||
|
createTestClient,
|
||||||
|
suppressTelemetryNoise,
|
||||||
|
} from "./helpers";
|
||||||
|
|
||||||
|
jest.setTimeout(60_000);
|
||||||
|
|
||||||
|
describeIntegration("MemoryClient Integration — Initialization", () => {
|
||||||
|
let client: MemoryClient;
|
||||||
|
let cleanup: () => void;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
cleanup = suppressTelemetryNoise();
|
||||||
|
client = createTestClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => cleanup());
|
||||||
|
|
||||||
|
test("client pings successfully and resolves org/project", async () => {
|
||||||
|
await client.ping();
|
||||||
|
expect(client.organizationId).toBeTruthy();
|
||||||
|
expect(client.projectId).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("get with invalid ID throws ValidationError", async () => {
|
||||||
|
// Non-UUID string triggers a 400 ValidationError, not a 404
|
||||||
|
await expect(client.get("nonexistent-memory-id-12345")).rejects.toThrow(
|
||||||
|
ValidationError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("get with non-existent UUID throws MemoryNotFoundError", async () => {
|
||||||
|
await expect(
|
||||||
|
client.get("00000000-0000-0000-0000-000000000000"),
|
||||||
|
).rejects.toThrow(MemoryNotFoundError);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("all SDK exceptions are MemoryError subclasses", async () => {
|
||||||
|
await expect(client.get("nonexistent-memory-id-12345")).rejects.toThrow(
|
||||||
|
MemoryError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("invalid API key throws on ping", async () => {
|
||||||
|
const badClient = new MemoryClient({ apiKey: "invalid-key-12345" });
|
||||||
|
await expect(badClient.ping()).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* Integration tests: User management and project configuration.
|
||||||
|
*
|
||||||
|
* Tests users(), getProject(), and updateProject() against the real API.
|
||||||
|
*
|
||||||
|
* Note: Webhook tests (createWebhook, updateWebhook) are excluded because
|
||||||
|
* the SDK has a known bug where it sends camelCase keys (eventTypes) instead
|
||||||
|
* of snake_case (event_types). These will be added once the SDK is fixed.
|
||||||
|
*
|
||||||
|
* Run: MEM0_API_KEY=your-key npx jest management.test.ts --forceExit
|
||||||
|
*/
|
||||||
|
import { MemoryClient } from "../../mem0";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
import {
|
||||||
|
describeIntegration,
|
||||||
|
createTestClient,
|
||||||
|
suppressTelemetryNoise,
|
||||||
|
seedTestMemories,
|
||||||
|
cleanupTestUser,
|
||||||
|
} from "./helpers";
|
||||||
|
|
||||||
|
jest.setTimeout(120_000);
|
||||||
|
|
||||||
|
const TEST_USER_ID = `integration-mgmt-${randomUUID()}`;
|
||||||
|
|
||||||
|
describeIntegration("MemoryClient Integration — Users & Project", () => {
|
||||||
|
let client: MemoryClient;
|
||||||
|
let cleanup: () => void;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
cleanup = suppressTelemetryNoise();
|
||||||
|
client = createTestClient();
|
||||||
|
await seedTestMemories(client, TEST_USER_ID);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanupTestUser(client, TEST_USER_ID);
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Users ────────────────────────────────────────────────
|
||||||
|
describe("user management", () => {
|
||||||
|
test("lists users and finds test user", async () => {
|
||||||
|
const allUsers = await client.users();
|
||||||
|
|
||||||
|
expect(typeof allUsers.count).toBe("number");
|
||||||
|
expect(Array.isArray(allUsers.results)).toBe(true);
|
||||||
|
|
||||||
|
if (allUsers.results.length > 0) {
|
||||||
|
const user = allUsers.results[0];
|
||||||
|
expect(typeof user.id).toBe("string");
|
||||||
|
expect(typeof user.name).toBe("string");
|
||||||
|
expect(typeof user.type).toBe("string");
|
||||||
|
}
|
||||||
|
|
||||||
|
const testUser = allUsers.results.find((u) => u.name === TEST_USER_ID);
|
||||||
|
expect(testUser).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Project ──────────────────────────────────────────────
|
||||||
|
describe("project management", () => {
|
||||||
|
let originalInstructions: string | undefined;
|
||||||
|
|
||||||
|
test("gets project with custom_instructions field", async () => {
|
||||||
|
const project = await client.getProject({
|
||||||
|
fields: ["custom_instructions"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(project).toBeDefined();
|
||||||
|
expect(typeof project).toBe("object");
|
||||||
|
expect("custom_instructions" in project).toBe(true);
|
||||||
|
|
||||||
|
originalInstructions = project.custom_instructions;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("updates project custom_instructions via updateProject()", async () => {
|
||||||
|
const testInstruction = `integration-test-${randomUUID().slice(0, 8)}`;
|
||||||
|
|
||||||
|
const result = await client.updateProject({
|
||||||
|
custom_instructions: testInstruction,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
|
||||||
|
// Verify the update took effect
|
||||||
|
const project = await client.getProject({
|
||||||
|
fields: ["custom_instructions"],
|
||||||
|
});
|
||||||
|
expect(project.custom_instructions).toBe(testInstruction);
|
||||||
|
|
||||||
|
// Restore original
|
||||||
|
await client.updateProject({
|
||||||
|
custom_instructions: originalInstructions || "",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* Integration tests: Search and history operations.
|
||||||
|
*
|
||||||
|
* Tests search v1, search v2, and memory history against the real API.
|
||||||
|
*
|
||||||
|
* Run: MEM0_API_KEY=your-key npx jest search.test.ts --forceExit
|
||||||
|
*/
|
||||||
|
import { MemoryClient } from "../../mem0";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
import {
|
||||||
|
describeIntegration,
|
||||||
|
createTestClient,
|
||||||
|
suppressTelemetryNoise,
|
||||||
|
seedTestMemories,
|
||||||
|
cleanupTestUser,
|
||||||
|
waitForSearchResults,
|
||||||
|
} from "./helpers";
|
||||||
|
|
||||||
|
jest.setTimeout(120_000);
|
||||||
|
|
||||||
|
const TEST_USER_ID = `integration-search-${randomUUID()}`;
|
||||||
|
|
||||||
|
describeIntegration("MemoryClient Integration — Search & History", () => {
|
||||||
|
let client: MemoryClient;
|
||||||
|
let cleanup: () => void;
|
||||||
|
let memoryIds: string[] = [];
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
cleanup = suppressTelemetryNoise();
|
||||||
|
client = createTestClient();
|
||||||
|
memoryIds = await seedTestMemories(client, TEST_USER_ID);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanupTestUser(client, TEST_USER_ID);
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Search v1 ────────────────────────────────────────────
|
||||||
|
describe("search v1", () => {
|
||||||
|
test("searches memories by user_id and returns results with scores", async () => {
|
||||||
|
// Search index may lag behind listing index — poll until ready
|
||||||
|
const results = await waitForSearchResults(
|
||||||
|
client,
|
||||||
|
"What is my favorite color?",
|
||||||
|
{ user_id: TEST_USER_ID },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
expect(results.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const first = results[0];
|
||||||
|
expect(typeof first.id).toBe("string");
|
||||||
|
expect(typeof first.memory).toBe("string");
|
||||||
|
expect(typeof first.score).toBe("number");
|
||||||
|
expect(first.score).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Search v2 ────────────────────────────────────────────
|
||||||
|
describe("search v2", () => {
|
||||||
|
test("searches with OR filters and returns results", async () => {
|
||||||
|
const results = await waitForSearchResults(
|
||||||
|
client,
|
||||||
|
"What do you know about me?",
|
||||||
|
{
|
||||||
|
filters: { OR: [{ user_id: TEST_USER_ID }] },
|
||||||
|
api_version: "v2",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
expect(results.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const first = results[0];
|
||||||
|
expect(typeof first.id).toBe("string");
|
||||||
|
expect(typeof first.memory).toBe("string");
|
||||||
|
expect(typeof first.score).toBe("number");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── History ──────────────────────────────────────────────
|
||||||
|
describe("memory history", () => {
|
||||||
|
test("returns history with at least an ADD event", async () => {
|
||||||
|
const memoryId = memoryIds[0];
|
||||||
|
const history = await client.history(memoryId);
|
||||||
|
|
||||||
|
expect(Array.isArray(history)).toBe(true);
|
||||||
|
expect(history.length).toBeGreaterThanOrEqual(1);
|
||||||
|
|
||||||
|
const entry = history[0];
|
||||||
|
expect(typeof entry.id).toBe("string");
|
||||||
|
expect(typeof entry.memory_id).toBe("string");
|
||||||
|
expect(["ADD", "UPDATE", "DELETE", "NOOP"]).toContain(entry.event);
|
||||||
|
expect(new Date(entry.created_at).toString()).not.toBe("Invalid Date");
|
||||||
|
expect(new Date(entry.updated_at).toString()).not.toBe("Invalid Date");
|
||||||
|
expect(
|
||||||
|
entry.new_memory === null || typeof entry.new_memory === "string",
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
entry.old_memory === null || typeof entry.old_memory === "string",
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
const events = history.map((h) => h.event);
|
||||||
|
expect(events).toContain("ADD");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Edge cases ─────────────────────────────────────────
|
||||||
|
describe("edge cases", () => {
|
||||||
|
test("search for non-existent user returns empty results", async () => {
|
||||||
|
const results = await client.search("anything", {
|
||||||
|
user_id: `nonexistent-user-${randomUUID()}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
expect(results.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("search with limit param does not throw", async () => {
|
||||||
|
const results = await client.search(
|
||||||
|
"Tell me about integration test user",
|
||||||
|
{
|
||||||
|
user_id: TEST_USER_ID,
|
||||||
|
limit: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user