feat(oss): port v3 pipeline with hybrid search, entity extraction, and additive scoring (#4805)
Co-authored-by: Soumil Rathi <soumilrathi@gmail.com> Co-authored-by: Saket Aryan <saketaryan2002@gmail.com> Co-authored-by: chaithanyak42 <chaithanya.kumar42a@gmail.com> Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
This commit is contained in:
co-authored by
Soumil Rathi
Saket Aryan
chaithanyak42
kartik-mem0
parent
57f944e18a
commit
a488e19044
@@ -3,7 +3,6 @@ import type * as MemoryTypes from "./mem0.types";
|
||||
|
||||
// Re-export all types from mem0.types
|
||||
export type {
|
||||
EntityOptions,
|
||||
AddMemoryOptions,
|
||||
SearchMemoryOptions,
|
||||
GetAllMemoryOptions,
|
||||
|
||||
+56
-10
@@ -23,6 +23,37 @@ import { captureClientEvent, generateHash } from "./telemetry";
|
||||
import { camelToSnake, camelToSnakeKeys, snakeToCamelKeys } from "./utils";
|
||||
import { createExceptionFromResponse, MemoryError } from "../common/exceptions";
|
||||
|
||||
// Entity params that must be passed via filters - check both snake_case and camelCase
|
||||
const ENTITY_PARAMS = [
|
||||
"user_id",
|
||||
"agent_id",
|
||||
"app_id",
|
||||
"run_id",
|
||||
"userId",
|
||||
"agentId",
|
||||
"appId",
|
||||
"runId",
|
||||
];
|
||||
|
||||
/**
|
||||
* Validates that no top-level entity parameters are passed.
|
||||
* @throws Error if entity params are found at top level
|
||||
*/
|
||||
function rejectTopLevelEntityParams(
|
||||
options: Record<string, any> | undefined,
|
||||
methodName: string,
|
||||
): void {
|
||||
const invalidKeys = Object.keys(options ?? {}).filter((k) =>
|
||||
ENTITY_PARAMS.includes(k),
|
||||
);
|
||||
if (invalidKeys.length > 0) {
|
||||
throw new Error(
|
||||
`Top-level entity parameters [${invalidKeys.join(", ")}] are not supported in ${methodName}(). ` +
|
||||
`Use filters: { user_id: "..." } instead.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class APIError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
@@ -190,7 +221,7 @@ export default class MemoryClient {
|
||||
this._captureEvent("add", [payloadKeys]);
|
||||
|
||||
const response = await this._fetchWithErrorHandling(
|
||||
`${this.host}/v1/memories/`,
|
||||
`${this.host}/v3/memories/`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: this.headers,
|
||||
@@ -254,13 +285,17 @@ export default class MemoryClient {
|
||||
}
|
||||
|
||||
async getAll(options?: GetAllMemoryOptions): Promise<Array<Memory>> {
|
||||
// Reject top-level entity params - must use filters instead
|
||||
rejectTopLevelEntityParams(options as Record<string, any>, "getAll");
|
||||
|
||||
if (this.telemetryId === "") await this.ping();
|
||||
const payloadKeys = Object.keys(options || {});
|
||||
this._captureEvent("get_all", [payloadKeys]);
|
||||
const { page, pageSize, ...rest } = options ?? {};
|
||||
const { page, pageSize, filters, ...rest } = options ?? {};
|
||||
const body: Record<string, any> = {
|
||||
output_format: "v1.1",
|
||||
...camelToSnakeKeys(rest),
|
||||
...(filters && { filters }),
|
||||
};
|
||||
|
||||
let url = `${this.host}/v2/memories/`;
|
||||
@@ -273,33 +308,36 @@ export default class MemoryClient {
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
// Unwrap v1.1 format: { results: [...] } → [...]
|
||||
return Array.isArray(response) ? response : (response?.results ?? response);
|
||||
}
|
||||
|
||||
async search(
|
||||
query: string,
|
||||
options?: SearchMemoryOptions,
|
||||
): Promise<Array<Memory>> {
|
||||
): Promise<{ results: Array<Memory> }> {
|
||||
// Reject top-level entity params - must use filters instead
|
||||
rejectTopLevelEntityParams(options as Record<string, any>, "search");
|
||||
|
||||
if (this.telemetryId === "") await this.ping();
|
||||
const payloadKeys = Object.keys(options || {});
|
||||
this._captureEvent("search", [payloadKeys]);
|
||||
const { filters, ...rest } = options ?? {};
|
||||
const payload: Record<string, any> = {
|
||||
query,
|
||||
output_format: "v1.1",
|
||||
...camelToSnakeKeys(options ?? {}),
|
||||
...camelToSnakeKeys(rest),
|
||||
...(filters && { filters }),
|
||||
};
|
||||
|
||||
const response = await this._fetchWithErrorHandling(
|
||||
`${this.host}/v2/memories/search/`,
|
||||
`${this.host}/v3/memories/search/`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(payload),
|
||||
},
|
||||
);
|
||||
// Unwrap v1.1 format: { results: [...] } → [...]
|
||||
return Array.isArray(response) ? response : (response?.results ?? response);
|
||||
return response;
|
||||
}
|
||||
|
||||
async delete(memoryId: string): Promise<{ message: string }> {
|
||||
@@ -614,12 +652,16 @@ export default class MemoryClient {
|
||||
throw new Error("Missing filters or schema");
|
||||
}
|
||||
|
||||
const { filters, ...rest } = data;
|
||||
const response = await this._fetchWithErrorHandling(
|
||||
`${this.host}/v1/exports/`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(camelToSnakeKeys(data)),
|
||||
body: JSON.stringify({
|
||||
...camelToSnakeKeys(rest),
|
||||
filters,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -636,12 +678,16 @@ export default class MemoryClient {
|
||||
throw new Error("Missing memoryExportId or filters");
|
||||
}
|
||||
|
||||
const { filters, ...rest } = data;
|
||||
const response = await this._fetchWithErrorHandling(
|
||||
`${this.host}/v1/exports/get/`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(camelToSnakeKeys(data)),
|
||||
body: JSON.stringify({
|
||||
...camelToSnakeKeys(rest),
|
||||
...(filters && { filters }),
|
||||
}),
|
||||
},
|
||||
);
|
||||
return response;
|
||||
|
||||
@@ -51,17 +51,13 @@ describeIntegration("MemoryClient Integration — CRUD", () => {
|
||||
},
|
||||
];
|
||||
|
||||
const result = await client.add(messages, { userId: TEST_USER_ID });
|
||||
const result = await client.add(messages, {
|
||||
userId: 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("eventId");
|
||||
}
|
||||
// v3 API processes memories asynchronously — returns PENDING
|
||||
expect(result).toHaveProperty("eventId");
|
||||
expect(result).toHaveProperty("status");
|
||||
});
|
||||
|
||||
test("adds a second batch of messages", async () => {
|
||||
@@ -76,8 +72,10 @@ describeIntegration("MemoryClient Integration — CRUD", () => {
|
||||
},
|
||||
];
|
||||
|
||||
const result = await client.add(messages, { userId: TEST_USER_ID });
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
const result = await client.add(messages, {
|
||||
userId: TEST_USER_ID,
|
||||
});
|
||||
expect(result).toHaveProperty("eventId");
|
||||
});
|
||||
|
||||
test("memories become available after async processing", async () => {
|
||||
@@ -123,7 +121,7 @@ describeIntegration("MemoryClient Integration — CRUD", () => {
|
||||
describe("get all memories", () => {
|
||||
test("returns all memories for test user", async () => {
|
||||
const memories = await client.getAll({
|
||||
filters: { userId: TEST_USER_ID },
|
||||
filters: { user_id: TEST_USER_ID },
|
||||
});
|
||||
|
||||
expect(Array.isArray(memories)).toBe(true);
|
||||
@@ -137,7 +135,7 @@ describeIntegration("MemoryClient Integration — CRUD", () => {
|
||||
|
||||
test("returns paginated results with page and page_size", async () => {
|
||||
const page1 = await client.getAll({
|
||||
filters: { userId: TEST_USER_ID },
|
||||
filters: { user_id: TEST_USER_ID },
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
});
|
||||
@@ -196,13 +194,12 @@ describeIntegration("MemoryClient Integration — CRUD", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
expect(result).toHaveProperty("eventId");
|
||||
});
|
||||
|
||||
test("getAll for non-existent user returns empty array", async () => {
|
||||
const memories = await client.getAll({
|
||||
filters: { userId: `nonexistent-user-${randomUUID()}` },
|
||||
filters: { user_id: `nonexistent-user-${randomUUID()}` },
|
||||
});
|
||||
|
||||
expect(Array.isArray(memories)).toBe(true);
|
||||
@@ -241,7 +238,9 @@ describeIntegration("MemoryClient Integration — CRUD", () => {
|
||||
// ─── Delete all + delete user ─────────────────────────────
|
||||
describe("cleanup operations", () => {
|
||||
test("deletes all memories for test user", async () => {
|
||||
const result = await client.deleteAll({ userId: TEST_USER_ID });
|
||||
const result = await client.deleteAll({
|
||||
userId: TEST_USER_ID,
|
||||
});
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result.message).toBe("string");
|
||||
});
|
||||
|
||||
@@ -64,7 +64,7 @@ export async function waitForMemories(
|
||||
): Promise<Memory[]> {
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
const memories = await withRetry(() =>
|
||||
client.getAll({ filters: { userId } }),
|
||||
client.getAll({ filters: { user_id: userId } }),
|
||||
);
|
||||
if (Array.isArray(memories) && memories.length >= minCount) {
|
||||
return memories;
|
||||
@@ -92,8 +92,9 @@ export async function waitForSearchResults(
|
||||
maxRetries = 4,
|
||||
): Promise<Memory[]> {
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
const results = await withRetry(() => client.search(query, options));
|
||||
if (Array.isArray(results) && results.length > 0) {
|
||||
const response = await withRetry(() => client.search(query, options));
|
||||
const results = response?.results ?? [];
|
||||
if (results.length > 0) {
|
||||
return results;
|
||||
}
|
||||
if (attempt < maxRetries) {
|
||||
|
||||
@@ -43,7 +43,7 @@ describeIntegration("MemoryClient Integration — Search & History", () => {
|
||||
const results = await waitForSearchResults(
|
||||
client,
|
||||
"What is my favorite color?",
|
||||
{ filters: { userId: TEST_USER_ID } },
|
||||
{ filters: { user_id: TEST_USER_ID } },
|
||||
);
|
||||
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
@@ -64,7 +64,7 @@ describeIntegration("MemoryClient Integration — Search & History", () => {
|
||||
client,
|
||||
"What do you know about me?",
|
||||
{
|
||||
filters: { OR: [{ userId: TEST_USER_ID }] },
|
||||
filters: { OR: [{ user_id: TEST_USER_ID }] },
|
||||
},
|
||||
);
|
||||
|
||||
@@ -108,24 +108,24 @@ describeIntegration("MemoryClient Integration — Search & History", () => {
|
||||
// ─── Edge cases ─────────────────────────────────────────
|
||||
describe("edge cases", () => {
|
||||
test("search for non-existent user returns empty results", async () => {
|
||||
const results = await client.search("anything", {
|
||||
filters: { userId: `nonexistent-user-${randomUUID()}` },
|
||||
const response = await client.search("test search query", {
|
||||
filters: { user_id: `nonexistent-user-${randomUUID()}` },
|
||||
});
|
||||
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
expect(results.length).toBe(0);
|
||||
expect(response).toHaveProperty("results");
|
||||
expect(response.results).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("search with top_k param does not throw", async () => {
|
||||
const results = await client.search(
|
||||
const response = await client.search(
|
||||
"Tell me about integration test user",
|
||||
{
|
||||
filters: { userId: TEST_USER_ID },
|
||||
filters: { user_id: TEST_USER_ID },
|
||||
topK: 1,
|
||||
},
|
||||
);
|
||||
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
expect(response).toHaveProperty("results");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,33 +21,33 @@ installConsoleSuppression();
|
||||
// ─── add() ───────────────────────────────────────────────
|
||||
|
||||
describe("MemoryClient - add()", () => {
|
||||
test("sends POST to /v1/memories/", async () => {
|
||||
test("sends POST to /v3/memories/", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/", { status: 200, body: [createMockMemory()] });
|
||||
extra.set("/v3/memories/", { status: 200, body: [createMockMemory()] });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.add([{ role: "user", content: "Hello" }], { userId: "u1" });
|
||||
|
||||
expect(findFetchCall(mock, "/v1/memories/", "POST")).toBeDefined();
|
||||
expect(findFetchCall(mock, "/v3/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()] });
|
||||
extra.set("/v3/memories/", { status: 200, body: [createMockMemory()] });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.add(messages, { userId: "u1" });
|
||||
|
||||
const call = findFetchCall(mock, "/v1/memories/", "POST");
|
||||
const call = findFetchCall(mock, "/v3/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()] });
|
||||
extra.set("/v3/memories/", { status: 200, body: [createMockMemory()] });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
@@ -55,19 +55,19 @@ describe("MemoryClient - add()", () => {
|
||||
user_id: "user_1",
|
||||
});
|
||||
|
||||
const call = findFetchCall(mock, "/v1/memories/", "POST");
|
||||
const call = findFetchCall(mock, "/v3/memories/", "POST");
|
||||
expect(getFetchBody(call!).user_id).toBe("user_1");
|
||||
});
|
||||
|
||||
test("sends empty messages array without crashing", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v1/memories/", { status: 200, body: [] });
|
||||
extra.set("/v3/memories/", { status: 200, body: [] });
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.add([], { userId: "u1" });
|
||||
|
||||
const call = findFetchCall(mock, "/v1/memories/", "POST");
|
||||
const call = findFetchCall(mock, "/v3/memories/", "POST");
|
||||
expect(getFetchBody(call!).messages).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* MemoryClient unit tests — search (v2 default, filters).
|
||||
* MemoryClient unit tests — search (v3 endpoint, filters).
|
||||
* Tests verify request construction, not mock response echo.
|
||||
*/
|
||||
import { MemoryClient } from "../mem0";
|
||||
@@ -15,48 +15,60 @@ import {
|
||||
installConsoleSuppression();
|
||||
|
||||
describe("MemoryClient - search()", () => {
|
||||
test("sends POST to /v2/memories/search/ by default", async () => {
|
||||
test("sends POST to /v3/memories/search/ by default", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/memories/search/", { status: 200, body: [] });
|
||||
extra.set("/v3/memories/search/", {
|
||||
status: 200,
|
||||
body: { results: [] },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.search("What is my name?", {
|
||||
filters: { userId: "u1" },
|
||||
filters: { user_id: "u1" },
|
||||
});
|
||||
|
||||
expect(findFetchCall(mock, "/v2/memories/search/", "POST")).toBeDefined();
|
||||
expect(findFetchCall(mock, "/v3/memories/search/", "POST")).toBeDefined();
|
||||
});
|
||||
|
||||
test("includes query in request body", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/memories/search/", { status: 200, body: [] });
|
||||
extra.set("/v3/memories/search/", {
|
||||
status: 200,
|
||||
body: { results: [] },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.search("What is my name?", {
|
||||
filters: { userId: "u1" },
|
||||
filters: { user_id: "u1" },
|
||||
});
|
||||
|
||||
const call = findFetchCall(mock, "/v2/memories/search/", "POST");
|
||||
const call = findFetchCall(mock, "/v3/memories/search/", "POST");
|
||||
expect(getFetchBody(call!).query).toBe("What is my name?");
|
||||
});
|
||||
|
||||
test("passes filters through to the API body", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/memories/search/", { status: 200, body: [] });
|
||||
extra.set("/v3/memories/search/", {
|
||||
status: 200,
|
||||
body: { results: [] },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.search("test", { filters: { userId: "u1" } });
|
||||
await client.search("test", { filters: { user_id: "u1" } });
|
||||
|
||||
const call = findFetchCall(mock, "/v2/memories/search/", "POST");
|
||||
const call = findFetchCall(mock, "/v3/memories/search/", "POST");
|
||||
expect(getFetchBody(call!).filters).toEqual({ user_id: "u1" });
|
||||
});
|
||||
|
||||
test("passes complex OR filters through to the API body", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/memories/search/", { status: 200, body: [] });
|
||||
extra.set("/v3/memories/search/", {
|
||||
status: 200,
|
||||
body: { results: [] },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
@@ -64,32 +76,192 @@ describe("MemoryClient - search()", () => {
|
||||
filters: { OR: [{ user_id: "u1" }, { agent_id: "a1" }] },
|
||||
});
|
||||
|
||||
const call = findFetchCall(mock, "/v2/memories/search/", "POST");
|
||||
const call = findFetchCall(mock, "/v3/memories/search/", "POST");
|
||||
const body = getFetchBody(call!);
|
||||
expect(body.filters).toEqual({
|
||||
OR: [{ user_id: "u1" }, { agent_id: "a1" }],
|
||||
});
|
||||
});
|
||||
|
||||
test("passes complex AND filters through to the API body", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v3/memories/search/", {
|
||||
status: 200,
|
||||
body: { results: [] },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.search("query", {
|
||||
filters: {
|
||||
AND: [
|
||||
{ user_id: "u1" },
|
||||
{ created_at: { gte: "2024-01-01T00:00:00Z" } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const call = findFetchCall(mock, "/v3/memories/search/", "POST");
|
||||
const body = getFetchBody(call!);
|
||||
expect(body.filters).toEqual({
|
||||
AND: [{ user_id: "u1" }, { created_at: { gte: "2024-01-01T00:00:00Z" } }],
|
||||
});
|
||||
});
|
||||
|
||||
test("passes NOT filters through to the API body", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v3/memories/search/", {
|
||||
status: 200,
|
||||
body: { results: [] },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.search("query", {
|
||||
filters: {
|
||||
AND: [
|
||||
{ user_id: "u1" },
|
||||
{ NOT: { categories: { in: ["spam", "test"] } } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const call = findFetchCall(mock, "/v3/memories/search/", "POST");
|
||||
const body = getFetchBody(call!);
|
||||
expect(body.filters).toEqual({
|
||||
AND: [
|
||||
{ user_id: "u1" },
|
||||
{ NOT: { categories: { in: ["spam", "test"] } } },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("passes complex nested AND/OR/NOT filters through to the API body", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v3/memories/search/", {
|
||||
status: 200,
|
||||
body: { results: [] },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
const complexFilter = {
|
||||
AND: [
|
||||
{ user_id: "u1" },
|
||||
{ created_at: { gte: "2024-01-01T00:00:00Z" } },
|
||||
{
|
||||
NOT: {
|
||||
OR: [
|
||||
{ categories: { in: ["spam"] } },
|
||||
{ categories: { in: ["test"] } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
await client.search("query", { filters: complexFilter });
|
||||
|
||||
const call = findFetchCall(mock, "/v3/memories/search/", "POST");
|
||||
const body = getFetchBody(call!);
|
||||
expect(body.filters).toEqual(complexFilter);
|
||||
});
|
||||
|
||||
test("does not crash when called without options", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/memories/search/", { status: 200, body: [] });
|
||||
extra.set("/v3/memories/search/", {
|
||||
status: 200,
|
||||
body: { results: [] },
|
||||
});
|
||||
setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
const result: Memory[] = await client.search("query");
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
const result = await client.search("query");
|
||||
expect(result).toHaveProperty("results");
|
||||
});
|
||||
|
||||
test("handles empty results array", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/memories/search/", { status: 200, body: [] });
|
||||
extra.set("/v3/memories/search/", {
|
||||
status: 200,
|
||||
body: { results: [] },
|
||||
});
|
||||
setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
const result: Memory[] = await client.search("nonexistent query", {
|
||||
filters: { userId: "u1" },
|
||||
const result = await client.search("nonexistent query", {
|
||||
filters: { AND: [{ user_id: "u1" }] },
|
||||
});
|
||||
expect(result).toHaveLength(0);
|
||||
expect(result.results).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MemoryClient - search() entity param rejection", () => {
|
||||
test("rejects user_id at top level", async () => {
|
||||
setupMockFetch();
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await expect(
|
||||
client.search("query", { user_id: "u1" } as any),
|
||||
).rejects.toThrow(/filters/);
|
||||
});
|
||||
|
||||
test("rejects agent_id at top level", async () => {
|
||||
setupMockFetch();
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await expect(
|
||||
client.search("query", { agent_id: "a1" } as any),
|
||||
).rejects.toThrow(/filters/);
|
||||
});
|
||||
|
||||
test("rejects app_id at top level", async () => {
|
||||
setupMockFetch();
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await expect(
|
||||
client.search("query", { app_id: "app1" } as any),
|
||||
).rejects.toThrow(/filters/);
|
||||
});
|
||||
|
||||
test("accepts filters with user_id", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v3/memories/search/", {
|
||||
status: 200,
|
||||
body: { results: [] },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
// Should not throw
|
||||
await client.search("query", { filters: { AND: [{ user_id: "u1" }] } });
|
||||
expect(findFetchCall(mock, "/v3/memories/search/", "POST")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("MemoryClient - getAll() entity param rejection", () => {
|
||||
test("rejects user_id at top level", async () => {
|
||||
setupMockFetch();
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await expect(client.getAll({ user_id: "u1" } as any)).rejects.toThrow(
|
||||
/filters/,
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects agent_id at top level", async () => {
|
||||
setupMockFetch();
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await expect(client.getAll({ agent_id: "a1" } as any)).rejects.toThrow(
|
||||
/filters/,
|
||||
);
|
||||
});
|
||||
|
||||
test("accepts filters with user_id", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/memories/", {
|
||||
status: 200,
|
||||
body: { results: [] },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.getAll({ filters: { user_id: "u1" } });
|
||||
expect(findFetchCall(mock, "/v2/memories/", "POST")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user