Files
mem0/mem0-ts/src/oss/tests/memory.validation.test.ts
T
kartik-mem0 ac76eb7568 fix(ts-oss): accept camelCase entity ids inside search/getAll filters
Both methods reject top-level entity params with "Use filters: { userId:
"..." } instead", and docs/open-source/node-quickstart.mdx teaches that same
shape. But search() and getAll() only read filters.user_id / .agent_id /
.run_id, so following either one throws a second, contradictory error:
"filters must contain at least one of: user_id, agent_id, run_id".

Normalize the three camelCase aliases to snake_case before validation in both
methods, so the shape the SDK tells callers to use actually works. An explicit
snake_case value wins if both are supplied, and the camelCase key is dropped
rather than passed through to the vector store as a stray metadata field.
2026-08-13 18:24:40 +05:30

432 lines
14 KiB
TypeScript

/**
* Unit tests for OSS SDK input validation.
*
* Validates fixes for:
* - Undefined/null message handling in add()
* - Threshold bounds validation (must be 0-1) in search()
* - TopK validation (must be non-negative) in search() and getAll()
* - Whitespace-only entity ID rejection in add(), search(), getAll()
*/
/// <reference types="jest" />
import { Memory } from "../src/memory";
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().mockResolvedValue(
JSON.stringify({
memory: [{ id: "0", text: "test memory", attributed_to: "user" }],
}),
),
})),
}));
const mockEmbedding = new Array(1536).fill(0.1);
jest.mock("../src/embeddings/openai", () => ({
OpenAIEmbedder: jest.fn().mockImplementation(() => ({
embed: jest.fn().mockResolvedValue(mockEmbedding),
embedBatch: jest.fn().mockResolvedValue([mockEmbedding]),
})),
}));
describe("Memory Input Validation", () => {
let memory: Memory;
const testUserId = "test-user-validation";
beforeAll(async () => {
memory = new Memory({
version: "v1.1",
embedder: {
provider: "openai",
config: { apiKey: "test-key", model: "text-embedding-3-small" },
},
llm: {
provider: "openai",
config: { apiKey: "test-key", model: "gpt-5-mini" },
},
vectorStore: {
provider: "memory",
config: { collectionName: "validation-test" },
},
});
// Wait for initialization
await new Promise((resolve) => setTimeout(resolve, 2000));
});
afterAll(async () => {
try {
await memory.reset();
} catch (e) {
// ignore cleanup errors
}
});
describe("add() message validation", () => {
it("should throw error when messages is undefined", async () => {
await expect(
// @ts-ignore - intentionally passing undefined
memory.add(undefined, { userId: testUserId }),
).rejects.toThrow("messages is required");
});
it("should throw error when messages is null", async () => {
await expect(
// @ts-ignore - intentionally passing null
memory.add(null, { userId: testUserId }),
).rejects.toThrow("messages is required");
});
it("should throw error when messages is an empty array", async () => {
await expect(memory.add([], { userId: testUserId })).rejects.toThrow(
"messages array cannot be empty",
);
});
it("should throw error when messages array contains only blank content", async () => {
await expect(
memory.add([{ role: "user", content: " " }], { userId: testUserId }),
).rejects.toThrow("messages array cannot contain only blank content");
});
it("should throw error when messages is an empty string", async () => {
await expect(memory.add(" ", { userId: testUserId })).rejects.toThrow(
"messages string cannot be empty",
);
});
});
describe("search() threshold validation", () => {
it("should throw error when threshold > 1.0", async () => {
await expect(
memory.search("test query", {
filters: { user_id: testUserId },
threshold: 1.5,
}),
).rejects.toThrow("Invalid threshold");
});
it("should throw error when threshold = 1.1", async () => {
await expect(
memory.search("test query", {
filters: { user_id: testUserId },
threshold: 1.1,
}),
).rejects.toThrow("Invalid threshold");
});
it("should throw error when threshold is negative", async () => {
await expect(
memory.search("test query", {
filters: { user_id: testUserId },
threshold: -0.5,
}),
).rejects.toThrow("Invalid threshold");
});
it("should throw error when threshold = -0.1", async () => {
await expect(
memory.search("test query", {
filters: { user_id: testUserId },
threshold: -0.1,
}),
).rejects.toThrow("Invalid threshold");
});
it("should accept threshold = 0 (edge case)", async () => {
const result = await memory.search("test query", {
filters: { user_id: testUserId },
threshold: 0,
});
expect(result).toBeDefined();
expect(result.results).toBeDefined();
});
it("should accept threshold = 1.0 (edge case)", async () => {
const result = await memory.search("test query", {
filters: { user_id: testUserId },
threshold: 1.0,
});
expect(result).toBeDefined();
expect(result.results).toBeDefined();
});
it("should accept threshold = 0.5 (normal valid value)", async () => {
const result = await memory.search("test query", {
filters: { user_id: testUserId },
threshold: 0.5,
});
expect(result).toBeDefined();
expect(result.results).toBeDefined();
});
});
describe("search() topK validation", () => {
it("should throw error when topK is negative", async () => {
await expect(
memory.search("test query", {
filters: { user_id: testUserId },
topK: -5,
}),
).rejects.toThrow("Invalid topK");
});
it("should throw error when topK = -1", async () => {
await expect(
memory.search("test query", {
filters: { user_id: testUserId },
topK: -1,
}),
).rejects.toThrow("Invalid topK");
});
it("should accept topK = 0 (returns empty)", async () => {
const result = await memory.search("test query", {
filters: { user_id: testUserId },
topK: 0,
});
expect(result).toBeDefined();
expect(result.results).toBeDefined();
});
it("should accept topK = 20 (normal value)", async () => {
const result = await memory.search("test query", {
filters: { user_id: testUserId },
topK: 20,
});
expect(result).toBeDefined();
expect(result.results).toBeDefined();
});
});
describe("add() entity ID validation", () => {
it("should throw error when userId is whitespace-only", async () => {
await expect(
memory.add("test message", { userId: " " }),
).rejects.toThrow("Invalid userId");
});
it("should throw error when userId is tabs and newlines", async () => {
await expect(
memory.add("test message", { userId: "\t\n\t" }),
).rejects.toThrow("Invalid userId");
});
it("should throw error when agentId is whitespace-only", async () => {
await expect(
memory.add("test message", { agentId: " " }),
).rejects.toThrow("Invalid agentId");
});
it("should throw error when runId is whitespace-only", async () => {
await expect(
memory.add("test message", { runId: " " }),
).rejects.toThrow("Invalid runId");
});
it("should throw error when userId contains internal whitespace", async () => {
await expect(
memory.add("test message", { userId: "user 123" }),
).rejects.toThrow("Invalid userId: cannot contain whitespace");
});
it("should throw error when userId contains tab character", async () => {
await expect(
memory.add("test message", { userId: "user\t123" }),
).rejects.toThrow("Invalid userId: cannot contain whitespace");
});
it("should accept userId with leading/trailing whitespace (trimmed)", async () => {
// Should not throw - leading/trailing whitespace is trimmed
const result = await memory.add("test message", {
userId: " valid-user ",
});
expect(result).toBeDefined();
});
});
describe("non-string entity ID coercion", () => {
it("coerces an integer user_id in getAll filters to its string form", async () => {
const listSpy = jest
.spyOn((memory as any).vectorStore, "list")
.mockResolvedValue([[], 0]);
await memory.getAll({ filters: { user_id: 42 as any } });
const passedFilters = listSpy.mock.calls[0][0] as Record<string, any>;
expect(passedFilters.user_id).toBe("42");
listSpy.mockRestore();
});
it("coerces an integer user_id in search filters to its string form", async () => {
const searchSpy = jest
.spyOn((memory as any).vectorStore, "search")
.mockResolvedValue([]);
await memory.search("q", { filters: { user_id: 42 as any } });
const passedFilters = searchSpy.mock.calls[0][2] as Record<string, any>;
expect(passedFilters.user_id).toBe("42");
searchSpy.mockRestore();
});
});
describe("camelCase entity IDs in filters", () => {
it("maps camelCase filter keys to snake_case in search", async () => {
const searchSpy = jest
.spyOn((memory as any).vectorStore, "search")
.mockResolvedValue([]);
await memory.search("q", {
filters: { userId: "alice", agentId: "bot", runId: "run-1" },
});
const passedFilters = searchSpy.mock.calls[0][2] as Record<string, any>;
expect(passedFilters).toMatchObject({
user_id: "alice",
agent_id: "bot",
run_id: "run-1",
});
expect(passedFilters.userId).toBeUndefined();
expect(passedFilters.agentId).toBeUndefined();
expect(passedFilters.runId).toBeUndefined();
searchSpy.mockRestore();
});
it("maps camelCase filter keys to snake_case in getAll", async () => {
const listSpy = jest
.spyOn((memory as any).vectorStore, "list")
.mockResolvedValue([[], 0]);
await memory.getAll({ filters: { userId: "alice" } });
const passedFilters = listSpy.mock.calls[0][0] as Record<string, any>;
expect(passedFilters.user_id).toBe("alice");
expect(passedFilters.userId).toBeUndefined();
listSpy.mockRestore();
});
it("prefers an explicit snake_case value over its camelCase alias", async () => {
const listSpy = jest
.spyOn((memory as any).vectorStore, "list")
.mockResolvedValue([[], 0]);
await memory.getAll({
filters: { user_id: "snake", userId: "camel" },
});
const passedFilters = listSpy.mock.calls[0][0] as Record<string, any>;
expect(passedFilters.user_id).toBe("snake");
expect(passedFilters.userId).toBeUndefined();
listSpy.mockRestore();
});
it("validates camelCase filter values like their snake_case counterparts", async () => {
await expect(
memory.search("q", { filters: { userId: "user 123" } }),
).rejects.toThrow("Invalid user_id: cannot contain whitespace");
});
});
describe("search() filter entity ID validation", () => {
it("should throw error when user_id in filters is whitespace-only", async () => {
await expect(
memory.search("test query", {
filters: { user_id: " " },
}),
).rejects.toThrow("Invalid user_id");
});
it("should throw error when agent_id in filters is whitespace-only", async () => {
await expect(
memory.search("test query", {
filters: { agent_id: " " },
}),
).rejects.toThrow("Invalid agent_id");
});
it("should throw error when user_id contains internal whitespace", async () => {
await expect(
memory.search("test query", {
filters: { user_id: "user 123" },
}),
).rejects.toThrow("Invalid user_id: cannot contain whitespace");
});
it("should accept user_id with leading/trailing whitespace (trimmed)", async () => {
const result = await memory.search("test query", {
filters: { user_id: " valid-user " },
});
expect(result).toBeDefined();
expect(result.results).toBeDefined();
});
});
describe("getAll() validation", () => {
it("should throw error when user_id is whitespace-only", async () => {
await expect(
memory.getAll({ filters: { user_id: " " } }),
).rejects.toThrow("Invalid user_id");
});
it("should throw error when topK is negative", async () => {
await expect(
memory.getAll({ filters: { user_id: testUserId }, topK: -1 }),
).rejects.toThrow("Invalid topK");
});
it("should throw error when user_id contains internal whitespace", async () => {
await expect(
memory.getAll({ filters: { user_id: "user 123" } }),
).rejects.toThrow("Invalid user_id: cannot contain whitespace");
});
it("should accept user_id with leading/trailing whitespace (trimmed)", async () => {
const result = await memory.getAll({
filters: { user_id: " valid-user " },
});
expect(result).toBeDefined();
expect(result.results).toBeDefined();
});
});
describe("deleteAll() entity ID validation", () => {
it("should throw error when userId is whitespace-only", async () => {
await expect(memory.deleteAll({ userId: " " })).rejects.toThrow(
"Invalid userId",
);
});
it("should throw error when userId contains internal whitespace", async () => {
await expect(memory.deleteAll({ userId: "user 123" })).rejects.toThrow(
"Invalid userId: cannot contain whitespace",
);
});
it("should trim userId before listing memories", async () => {
const listSpy = jest.spyOn(memory["vectorStore"], "list");
listSpy.mockResolvedValue([[], null]);
await memory.deleteAll({ userId: " alice " });
expect(listSpy).toHaveBeenCalledWith(
{ user_id: "alice" },
expect.any(Number),
);
});
});
});