Merge branch 'main' into fix/deflake-ts-integration-seeding

This commit is contained in:
kartik-mem0
2026-08-14 17:13:38 +05:30
135 changed files with 3932 additions and 1190 deletions
+56
View File
@@ -0,0 +1,56 @@
# TypeScript SDK (`mem0-ts/`)
The `mem0ai` package on npm. Hosted client plus self-hosted OSS memory.
## Commands
```bash
pnpm install
pnpm run build # tsup (CJS + ESM)
pnpm run test # jest, all tests
pnpm run test:unit # jest --coverage
pnpm run test:integration # jest, needs MEM0_API_KEY
pnpm run test:ci # jest --coverage --ci
pnpm run test:watch
pnpm run typecheck # tsc --noEmit
```
pnpm only. Never npm, never yarn.
## Conventions
- **Node 20 and 22** are the CI-tested versions.
- **Build:** tsup, dual CJS + ESM output.
- **Formatter:** Prettier. No linter is configured here; `cli/node/` uses Biome and `integrations/vercel-ai-sdk/` uses ESLint, so do not assume a shared setup.
- **Tests:** jest. `cli/node/` and `integrations/openclaw/` use vitest instead.
- **TypeScript strict mode.**
- ES module `import` syntax only. Never `require()`.
- Source files are `snake_case.ts`, tests are `<module>.test.ts`.
Run `pnpm run typecheck` after every change.
## Layout
```
mem0-ts/src/
├── client/ MemoryClient (hosted platform)
└── oss/ Memory (self-hosted)
├── llms/
├── embeddings/
├── vector_stores/
└── graphs/
```
## Public API
| Export | Purpose | Import |
| -------------- | ---------------------- | ---------------------------------------------- |
| `MemoryClient` | Hosted platform client | `import { MemoryClient } from 'mem0ai'` |
| `Memory` | Self-hosted OSS memory | `import { Memory } from 'mem0ai/oss'` |
| Providers | OSS building blocks | `import { OpenAIEmbedding } from 'mem0ai/oss'` |
The method surface mirrors the Python SDK: `add`, `search`, `get`, `getAll`, `update`, `delete`, `deleteAll`, `history`. Changing any public signature means updating `docs/` in the same PR.
## Releasing
Tag prefix `ts-v*` triggers `ts-sdk-cd.yml`, which publishes to npm over OIDC. Bump the version in `package.json` first.
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+4 -4
View File
@@ -404,10 +404,10 @@ export default class MemoryClient {
...(filters && { filters }),
};
let url = `${this.host}/v3/memories/`;
if (page && pageSize) {
url += `?page=${page}&page_size=${pageSize}`;
}
const queryParams: string[] = [];
if (page !== undefined) queryParams.push(`page=${page}`);
if (pageSize !== undefined) queryParams.push(`page_size=${pageSize}`);
const url = `${this.host}/v3/memories/${queryParams.length ? `?${queryParams.join("&")}` : ""}`;
const response = await this._fetchWithErrorHandling(url, {
method: "POST",
@@ -373,3 +373,56 @@ describe("MemoryClient - getAll() entity param rejection", () => {
expect(getFetchBody(call!).show_expired).toBe(true);
});
});
describe("MemoryClient - getAll() page/pageSize query params", () => {
test("sends no query params when neither page nor pageSize is provided", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v3/memories/", { status: 200, body: { results: [] } });
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.getAll({ filters: { user_id: "u1" } });
const call = findFetchCall(mock, "/v3/memories/", "POST");
expect(call![0]).not.toContain("?");
});
test("sends page alone as a query param", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v3/memories/", { status: 200, body: { results: [] } });
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.getAll({ filters: { user_id: "u1" }, page: 2 });
const call = findFetchCall(mock, "/v3/memories/", "POST");
expect(call![0]).toContain("?page=2");
expect(call![0]).not.toContain("page_size");
});
test("sends pageSize alone as a query param", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v3/memories/", { status: 200, body: { results: [] } });
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.getAll({ filters: { user_id: "u1" }, pageSize: 50 });
const call = findFetchCall(mock, "/v3/memories/", "POST");
expect(call![0]).toContain("?page_size=50");
expect(call![0]).not.toContain("?page=50");
});
test("sends both page and pageSize when provided, including falsy 0 values", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v3/memories/", { status: 200, body: { results: [] } });
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.getAll({ filters: { user_id: "u1" }, page: 0, pageSize: 0 });
const call = findFetchCall(mock, "/v3/memories/", "POST");
expect(call![0]).toContain("page=0");
expect(call![0]).toContain("page_size=0");
});
});
+13 -2
View File
@@ -29,7 +29,18 @@ const PROVIDERS = [
* Extract the model-family provider from a Bedrock model id
* (e.g. `anthropic.claude-3-sonnet-...` -> `anthropic`).
*/
export function extractProvider(model: string): string {
export function extractProvider(
model: string,
providerOverride?: string,
): string {
if (providerOverride) {
if (!PROVIDERS.includes(providerOverride)) {
throw new Error(
`Unknown providerOverride '${providerOverride}'. Valid providers: ${PROVIDERS.join(", ")}`,
);
}
return providerOverride;
}
for (const provider of PROVIDERS) {
const re = new RegExp(
`\\b${provider.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
@@ -79,7 +90,7 @@ export class AWSBedrockLLM implements LLM {
this.model =
(typeof config.model === "string" && config.model) ||
"anthropic.claude-3-5-sonnet-20240620-v1:0";
this.provider = extractProvider(this.model);
this.provider = extractProvider(this.model, config.providerOverride);
this.temperature = config.temperature ?? 0.1;
this.maxTokens = config.maxTokens ?? 2000;
this.topP = config.topP;
+29 -44
View File
@@ -105,6 +105,18 @@ const ENTITY_PARAMS = [
// actor_id has no camelCase alias.
const IDENTITY_KEYS = [...ENTITY_PARAMS, "actor_id"];
const PAYLOAD_METADATA_EXCLUDED_KEYS = new Set([
"user_id",
"agent_id",
"run_id",
"hash",
"data",
"createdAt",
"updatedAt",
"textLemmatized",
"attributedTo",
]);
// Caller metadata must not overwrite or inject an identity scope (#6342 / #6367 / #6371).
function stripIdentityKeys(
metadata: Record<string, any> = {},
@@ -1000,7 +1012,8 @@ export class Memory {
for (const mem of extractedMemories) {
const text = mem.text;
if (!text || !(text in embedMap)) continue;
if (!text || !Object.prototype.hasOwnProperty.call(embedMap, text))
continue;
const memHash = createHash("md5").update(text).digest("hex");
if (existingHashes.has(memHash) || seenHashes.has(memHash)) {
@@ -1297,20 +1310,8 @@ export class Memory {
metadata: {},
};
// Add additional metadata
const excludedKeys = new Set([
"userId",
"agentId",
"runId",
"hash",
"data",
"createdAt",
"updatedAt",
"textLemmatized",
"attributedTo",
]);
for (const [key, value] of Object.entries(memory.payload)) {
if (!excludedKeys.has(key)) {
if (!PAYLOAD_METADATA_EXCLUDED_KEYS.has(key)) {
memoryItem.metadata![key] = value;
}
}
@@ -1566,18 +1567,6 @@ export class Memory {
);
// Step 9: Format results
const excludedKeys = new Set([
"user_id",
"agent_id",
"run_id",
"hash",
"data",
"createdAt",
"updatedAt",
"textLemmatized",
"attributedTo",
]);
const results = scoredResults
.filter((scored) => scored.payload?.data)
.map((scored) => {
@@ -1590,7 +1579,7 @@ export class Memory {
updatedAt: payload.updatedAt,
score: scored.score,
metadata: Object.entries(payload)
.filter(([key]) => !excludedKeys.has(key))
.filter(([key]) => !PAYLOAD_METADATA_EXCLUDED_KEYS.has(key))
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
...(payload.user_id && { user_id: payload.user_id }),
...(payload.agent_id && { agent_id: payload.agent_id }),
@@ -1896,17 +1885,6 @@ export class Memory {
? memories
: memories.filter((mem) => !payloadIsExpired(mem.payload));
const excludedKeys = new Set([
"user_id",
"agent_id",
"run_id",
"hash",
"data",
"createdAt",
"updatedAt",
"textLemmatized",
"attributedTo",
]);
const results = visibleMemories.slice(0, topK).map((mem) => ({
id: mem.id,
memory: mem.payload.data,
@@ -1914,7 +1892,7 @@ export class Memory {
createdAt: mem.payload.createdAt,
updatedAt: mem.payload.updatedAt,
metadata: Object.entries(mem.payload)
.filter(([key]) => !excludedKeys.has(key))
.filter(([key]) => !PAYLOAD_METADATA_EXCLUDED_KEYS.has(key))
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
...(mem.payload.user_id && { user_id: mem.payload.user_id }),
...(mem.payload.agent_id && { agent_id: mem.payload.agent_id }),
@@ -1943,8 +1921,12 @@ export class Memory {
metadata: Record<string, any>,
): Promise<string> {
const memoryId = uuidv4();
const embedding =
existingEmbeddings[data] || (await this.embedder.embed(data, "add"));
const embedding = Object.prototype.hasOwnProperty.call(
existingEmbeddings,
data,
)
? existingEmbeddings[data]
: await this.embedder.embed(data, "add");
const memoryMetadata = {
...metadata,
@@ -1987,9 +1969,12 @@ export class Memory {
}
const textChanged = newData !== prevValue;
const embedding =
existingEmbeddings[newData] ||
(await this.embedder.embed(newData, "update"));
const embedding = Object.prototype.hasOwnProperty.call(
existingEmbeddings,
newData,
)
? existingEmbeddings[newData]
: await this.embedder.embed(newData, "update");
const sanitizedMetadata = stripIdentityKeys(metadata);
@@ -68,6 +68,29 @@ describe("extractProvider", () => {
/Unknown provider/,
);
});
const ARN =
"arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123xyz";
it("throws for an application inference profile ARN without providerOverride", () => {
expect(() => extractProvider(ARN)).toThrow(/Unknown provider in model/);
});
it("resolves an application inference profile ARN via providerOverride", () => {
expect(extractProvider(ARN, "anthropic")).toBe("anthropic");
});
it("lets providerOverride take precedence over regex detection", () => {
expect(
extractProvider("anthropic.claude-3-5-sonnet-20240620-v1:0", "amazon"),
).toBe("amazon");
});
it("throws on a misspelled providerOverride", () => {
expect(() => extractProvider(ARN, "anthorpic")).toThrow(
/Unknown providerOverride 'anthorpic'/,
);
});
});
describe("AWSBedrockLLM", () => {
@@ -176,6 +199,28 @@ describe("AWSBedrockLLM", () => {
expect(res).toEqual({ content: "hello from bedrock", role: "assistant" });
});
it("uses providerOverride to resolve inference profile ARNs (topP omitted like anthropic)", async () => {
const arn =
"arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123xyz";
const client = new FakeBedrockClient(textResponse);
const llm = makeLLM(client, {
model: arn,
providerOverride: "anthropic",
topP: 0.9,
});
await llm.generateResponse([{ role: "user", content: "hi" }]);
expect(client.lastInput.modelId).toBe(arn);
expect(client.lastInput.inferenceConfig.topP).toBeUndefined();
});
it("throws when constructed with an inference profile ARN and no providerOverride", () => {
const arn =
"arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abc123xyz";
expect(() => new AWSBedrockLLM({ model: arn })).toThrow(
/Unknown provider in model/,
);
});
it("does not construct the Bedrock client until the first request", () => {
const before = mockClientConstructions;
// No injected client: the old constructor eagerly built a real one.
+1
View File
@@ -78,6 +78,7 @@ export interface LLMConfig {
awsAccessKeyId?: string;
awsSecretAccessKey?: string;
awsSessionToken?: string;
providerOverride?: string;
// Optional pre-constructed client (e.g. BedrockRuntimeClient) for DI/testing.
client?: any;
}
+22
View File
@@ -0,0 +1,22 @@
function toCamelCase(obj: Record<string, any>): Record<string, any> {
if (typeof obj !== "object" || obj === null) return obj;
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => [
key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()),
value,
]),
);
}
export function toCamelCasePreservingIds(
payload: Record<string, any>,
): Record<string, any> {
const { agent_id, run_id, user_id, ...rest } = payload;
return {
...toCamelCase(rest),
...(agent_id !== undefined && { agent_id }),
...(run_id !== undefined && { run_id }),
...(user_id !== undefined && { user_id }),
};
}
+4 -15
View File
@@ -8,6 +8,7 @@ import type {
import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
import { loadPeer } from "../utils/load_peer";
import { toCamelCasePreservingIds } from "../utils/casing";
/**
* Escape RediSearch TAG filter special characters. Any punctuation in the
@@ -145,18 +146,6 @@ export function buildRedisFilterExpr(filters?: SearchFilters): string {
return conditions.length > 0 ? conditions.join(" ") : "*";
}
// Utility function to convert object keys to camelCase
function toCamelCase(obj: Record<string, any>): Record<string, any> {
if (typeof obj !== "object" || obj === null) return obj;
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => [
key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()),
value,
]),
);
}
export class RedisDB implements VectorStore {
private client!: RedisClientType<
RedisDefaultModules & RedisModules & RedisFunctions & RedisScripts
@@ -468,7 +457,7 @@ export class RedisDB implements VectorStore {
return {
id: doc.value.memory_id,
payload: toCamelCase(resultPayload),
payload: toCamelCasePreservingIds(resultPayload),
score: Math.max(0, 1 - (Number(doc.value.__vector_score) ?? 0)),
};
});
@@ -573,7 +562,7 @@ export class RedisDB implements VectorStore {
return {
id: vectorId,
payload: toCamelCase(payload),
payload: toCamelCasePreservingIds(payload),
};
} catch (error) {
console.error("Error getting vector:", error);
@@ -680,7 +669,7 @@ export class RedisDB implements VectorStore {
const items = results.documents.map((doc) => ({
id: doc.value.memory_id,
payload: toCamelCase({
payload: toCamelCasePreservingIds({
hash: doc.value.hash,
data: doc.value.memory,
created_at: new Date(parseInt(doc.value.created_at)).toISOString(),
+2 -11
View File
@@ -2,6 +2,7 @@ import { VectorStore } from "./base";
import { SearchFilters, VectorStoreResult } from "../types";
import { ValkeyConfig } from "../types/valkey";
import { loadPeer } from "../utils/load_peer";
import { toCamelCasePreservingIds } from "../utils/casing";
interface ValkeyClient {
call: (...args: (string | number | Buffer)[]) => Promise<unknown>;
@@ -45,16 +46,6 @@ function toSnakeCase(obj: Record<string, any>): Record<string, any> {
);
}
function toCamelCase(obj: Record<string, any>): Record<string, any> {
if (typeof obj !== "object" || obj === null) return obj;
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => [
key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()),
value,
]),
);
}
interface ValkeySearchDoc {
memory_id?: string;
hash?: string;
@@ -388,7 +379,7 @@ export class ValkeyDB implements VectorStore {
return {
id: doc.memory_id ?? "",
payload: toCamelCase(resultPayload),
payload: toCamelCasePreservingIds(resultPayload),
score,
};
}
+2 -8
View File
@@ -269,9 +269,7 @@ describe("Memory - add()", () => {
"runId",
"actor_id",
]) {
if (key !== canonicalKey) {
expect(stored!.metadata).not.toHaveProperty(key);
}
expect(stored!.metadata).not.toHaveProperty(key);
}
},
);
@@ -307,10 +305,7 @@ describe("Memory - add()", () => {
}
}
expect(stored!.metadata).toEqual(
expect.objectContaining({
[filterKey]: filterValue,
ordinary: "preserved",
}),
expect.objectContaining({ ordinary: "preserved" }),
);
for (const key of [
"user_id",
@@ -321,7 +316,6 @@ describe("Memory - add()", () => {
"runId",
"actor_id",
]) {
if (key === filterKey) continue;
expect(stored!.metadata).not.toHaveProperty(key);
}
},
+10
View File
@@ -128,6 +128,16 @@ describe("Memory - get()", () => {
expect(item!.createdAt).toBeDefined();
expect(new Date(item!.createdAt!).toString()).not.toBe("Invalid Date");
});
test("does not duplicate the entity id inside metadata", async () => {
const addResult: SearchResult = await memory.add("Metadata leak test", {
userId,
});
const item: any = await memory.get(addResult.results[0].id);
expect(item.user_id).toBe(userId);
expect(item.metadata).not.toHaveProperty("user_id");
expect(item.metadata).not.toHaveProperty("userId");
});
});
// ─── update() ────────────────────────────────────────────
@@ -0,0 +1,125 @@
/// <reference types="jest" />
/** add() with infer=false must embed Object.prototype-colliding text instead of resolving it off the prototype chain. */
import { Memory } from "../src/memory";
import { MemoryVectorStore } from "../src/vector_stores/memory";
import type { SearchResult } from "../src/types";
const mockEmbedding = new Array(1536).fill(0.1);
const mockEmbed = jest.fn().mockResolvedValue(mockEmbedding);
jest.mock("../src/embeddings/openai", () => ({
OpenAIEmbedder: jest.fn().mockImplementation(() => ({
embed: mockEmbed,
embedBatch: jest.fn(),
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-proto-${Date.now()}-${Math.random()}`,
dimension: 1536,
dbPath: ":memory:",
},
},
llm: {
provider: "openai",
config: { apiKey: "test-key", model: "gpt-5-mini" },
},
historyDbPath: ":memory:",
});
}
describe("add() with infer=false and Object.prototype-colliding text", () => {
let memory: Memory;
let insertSpy: jest.SpyInstance;
beforeEach(() => {
memory = createMemory();
mockEmbed.mockClear();
insertSpy = jest.spyOn(MemoryVectorStore.prototype, "insert");
});
afterEach(async () => {
insertSpy.mockRestore();
await memory.reset();
});
test.each([
"constructor",
"toString",
"valueOf",
"hasOwnProperty",
"__proto__",
])(
'embeds "%s" instead of resolving it off Object.prototype',
async (text) => {
const result: SearchResult = await memory.add(text, {
userId: "u1",
infer: false,
});
expect(mockEmbed).toHaveBeenCalledWith(text, "add");
const [storedVectors] =
insertSpy.mock.calls[insertSpy.mock.calls.length - 1];
expect(Array.isArray(storedVectors[0])).toBe(true);
expect(storedVectors[0]).toEqual(mockEmbedding);
expect(
storedVectors[0].every((n: unknown) => typeof n === "number"),
).toBe(true);
expect(result.results[0].memory).toBe(text);
},
);
});
describe("update() metadata-only on Object.prototype-colliding text (#6323)", () => {
let memory: Memory;
let updateSpy: jest.SpyInstance;
beforeEach(() => {
memory = createMemory();
updateSpy = jest.spyOn(MemoryVectorStore.prototype, "update");
});
afterEach(async () => {
updateSpy.mockRestore();
await memory.reset();
});
test.each([
"constructor",
"toString",
"valueOf",
"hasOwnProperty",
"__proto__",
])(
're-embeds "%s" on a metadata-only update instead of resolving it off Object.prototype',
async (text) => {
const added: SearchResult = await memory.add(text, {
userId: "u1",
infer: false,
});
const memoryId = added.results[0].id!;
mockEmbed.mockClear();
await memory.update(memoryId, { metadata: { pinned: true } });
expect(mockEmbed).toHaveBeenCalledWith(text, "update");
const [, storedVector] =
updateSpy.mock.calls[updateSpy.mock.calls.length - 1];
expect(Array.isArray(storedVector)).toBe(true);
expect(storedVector).toEqual(mockEmbedding);
},
);
});
+84
View File
@@ -125,4 +125,88 @@ describe("RedisDB – entity payload handling", () => {
expect(entry.created_at).toBeGreaterThan(0);
expect(Number.isNaN(entry.created_at)).toBe(false);
});
test("search returns entity ids as snake_case", async () => {
mockClient.ft.search.mockResolvedValue({
total: 1,
documents: [
{
id: "mem0:test:mem-1",
value: {
memory_id: "mem-1",
hash: "h1",
memory: "likes coffee",
created_at: "1700000000000",
agent_id: "agent-1",
run_id: "run-1",
user_id: "user-1",
metadata: "{}",
__vector_score: 0.1,
},
},
],
});
const results = await store.search([0.1, 0.2, 0.3, 0.4], 5);
expect(results[0].payload.user_id).toBe("user-1");
expect(results[0].payload.agent_id).toBe("agent-1");
expect(results[0].payload.run_id).toBe("run-1");
expect(results[0].payload).not.toHaveProperty("userId");
expect(results[0].payload).not.toHaveProperty("agentId");
expect(results[0].payload).not.toHaveProperty("runId");
});
test("get returns entity ids as snake_case", async () => {
mockClient.exists.mockResolvedValue(1);
mockClient.hGetAll.mockResolvedValue({
memory_id: "mem-1",
hash: "h1",
memory: "likes coffee",
created_at: "1700000000000",
agent_id: "agent-1",
run_id: "run-1",
user_id: "user-1",
metadata: "{}",
});
const result = await store.get("mem-1");
expect(result?.payload.user_id).toBe("user-1");
expect(result?.payload.agent_id).toBe("agent-1");
expect(result?.payload.run_id).toBe("run-1");
expect(result?.payload).not.toHaveProperty("userId");
expect(result?.payload).not.toHaveProperty("agentId");
expect(result?.payload).not.toHaveProperty("runId");
});
test("list returns entity ids as snake_case", async () => {
mockClient.ft.search.mockResolvedValue({
total: 1,
documents: [
{
id: "mem0:test:mem-1",
value: {
memory_id: "mem-1",
hash: "h1",
memory: "likes coffee",
created_at: "1700000000000",
agent_id: "agent-1",
run_id: "run-1",
user_id: "user-1",
metadata: "{}",
},
},
],
});
const [items] = await store.list();
expect(items[0].payload.user_id).toBe("user-1");
expect(items[0].payload.agent_id).toBe("agent-1");
expect(items[0].payload.run_id).toBe("run-1");
expect(items[0].payload).not.toHaveProperty("userId");
expect(items[0].payload).not.toHaveProperty("agentId");
expect(items[0].payload).not.toHaveProperty("runId");
});
});
+86 -1
View File
@@ -166,7 +166,7 @@ describe("Valkey – mocked iovalkey client", () => {
const result = await store.get("mem-1");
expect(result?.id).toBe("mem-1");
expect(result?.payload.data).toBe("hello valkey");
expect(result?.payload.userId).toBe("alice");
expect(result?.payload.user_id).toBe("alice");
// created_at is persisted as unix seconds and rendered back to its ISO instant.
expect(result?.payload.createdAt).toBe("2024-01-01T00:00:00.000Z");
});
@@ -244,6 +244,91 @@ describe("Valkey – mocked iovalkey client", () => {
expect(searchCall[2]).toContain("@user_id:{a\\|b\\ c}");
});
it("get returns entity ids as snake_case", async () => {
const store = new ValkeyDB({
collectionName: "test",
embeddingModelDims: 4,
valkeyUrl: "valkey://localhost:6379",
});
await store.initialize();
await store.insert(
[[0.1, 0.2, 0.3, 0.4]],
["mem-ids"],
[
{
data: "likes tea",
user_id: "user-1",
agent_id: "agent-1",
run_id: "run-1",
},
],
);
const result = await store.get("mem-ids");
expect(result?.payload.user_id).toBe("user-1");
expect(result?.payload.agent_id).toBe("agent-1");
expect(result?.payload.run_id).toBe("run-1");
expect(result?.payload).not.toHaveProperty("userId");
expect(result?.payload).not.toHaveProperty("agentId");
expect(result?.payload).not.toHaveProperty("runId");
});
it("search and list return entity ids as snake_case", async () => {
const store = new ValkeyDB({
collectionName: "test",
embeddingModelDims: 4,
valkeyUrl: "valkey://localhost:6379",
});
await store.initialize();
const iovalkey = require("iovalkey");
const mockClient = iovalkey.__mockClient;
mockClient.call.mockImplementation(async (...args: any[]) => {
if (args[0] === "FT.SEARCH") {
return [
1,
"mem0:test:mem-search",
[
"memory_id",
"mem-search",
"hash",
"h1",
"memory",
"likes tea",
"created_at",
"1700000000",
"agent_id",
"agent-1",
"run_id",
"run-1",
"user_id",
"user-1",
"metadata",
"{}",
"vector_score",
"0.1",
],
];
}
return "OK";
});
const results = await store.search([0.1, 0.2, 0.3, 0.4], 5);
expect(results[0].payload.user_id).toBe("user-1");
expect(results[0].payload.agent_id).toBe("agent-1");
expect(results[0].payload.run_id).toBe("run-1");
expect(results[0].payload).not.toHaveProperty("userId");
expect(results[0].payload).not.toHaveProperty("agentId");
expect(results[0].payload).not.toHaveProperty("runId");
const [listed] = await store.list();
expect(listed[0].payload.user_id).toBe("user-1");
expect(listed[0].payload.agent_id).toBe("agent-1");
expect(listed[0].payload.run_id).toBe("run-1");
expect(listed[0].payload).not.toHaveProperty("userId");
});
it("does not raise an unhandled rejection when initialization fails", async () => {
const iovalkey = require("iovalkey");
iovalkey.__mockClient.call.mockImplementationOnce(async () => {