254 lines
8.5 KiB
TypeScript
254 lines
8.5 KiB
TypeScript
/**
|
|
* 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");
|
|
});
|
|
});
|
|
|
|
// ─── 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 single ────────────────────────────────────────
|
|
// NOTE: Delete tests run last to avoid race conditions with
|
|
// other tests that depend on the seeded memories.
|
|
describe("delete memory", () => {
|
|
test("deletes a single memory by ID", async () => {
|
|
const memoryId = memoryIds[0];
|
|
expect(memoryId).toBeDefined();
|
|
|
|
const result = await client.delete(memoryId);
|
|
expect(result).toBeDefined();
|
|
expect(typeof result.message).toBe("string");
|
|
});
|
|
|
|
test("getting deleted memory throws MemoryError", async () => {
|
|
const memoryId = memoryIds[0];
|
|
await expect(client.get(memoryId)).rejects.toThrow(MemoryError);
|
|
});
|
|
});
|
|
|
|
// ─── Delete all + delete user ─────────────────────────────
|
|
describe("cleanup operations", () => {
|
|
test("deletes all memories for test user", async () => {
|
|
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.");
|
|
});
|
|
});
|
|
});
|