From f4ccef51573c6844ee0667759e5ab6b314e57d92 Mon Sep 17 00:00:00 2001 From: Hrushikesh Yadav <136978914+HrushiYadav@users.noreply.github.com> Date: Fri, 26 Jun 2026 19:01:58 +0530 Subject: [PATCH] fix(ts/redis): use nullish coalescing for hash/timestamps in insert/update (#5860) Co-authored-by: Claude Opus 4.6 --- mem0-ts/src/oss/src/vector_stores/redis.ts | 23 ++-- mem0-ts/src/oss/tests/redis.unit.test.ts | 128 +++++++++++++++++++++ 2 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 mem0-ts/src/oss/tests/redis.unit.test.ts diff --git a/mem0-ts/src/oss/src/vector_stores/redis.ts b/mem0-ts/src/oss/src/vector_stores/redis.ts index 6cb29c44e..deae5e3ac 100644 --- a/mem0-ts/src/oss/src/vector_stores/redis.ts +++ b/mem0-ts/src/oss/src/vector_stores/redis.ts @@ -337,11 +337,14 @@ export class RedisDB implements VectorStore { const id = ids[idx]; // Create entry with required fields + const createdAt = payload.created_at + ? new Date(payload.created_at).getTime() + : 0; const entry: Record = { memory_id: id, - hash: payload.hash, - memory: payload.data, - created_at: new Date(payload.created_at).getTime(), + hash: payload.hash ?? "", + memory: payload.data ?? "", + created_at: createdAt, embedding: new Float32Array(vector).buffer, }; @@ -561,12 +564,18 @@ export class RedisDB implements VectorStore { payload: Record, ): Promise { const snakePayload = toSnakeCase(payload); + const createdAt = snakePayload.created_at + ? new Date(snakePayload.created_at).getTime() + : 0; + const updatedAt = snakePayload.updated_at + ? new Date(snakePayload.updated_at).getTime() + : 0; const entry: Record = { memory_id: vectorId, - hash: snakePayload.hash, - memory: snakePayload.data, - created_at: new Date(snakePayload.created_at).getTime(), - updated_at: new Date(snakePayload.updated_at).getTime(), + hash: snakePayload.hash ?? "", + memory: snakePayload.data ?? "", + created_at: createdAt, + updated_at: updatedAt, embedding: Buffer.from(new Float32Array(vector).buffer), }; diff --git a/mem0-ts/src/oss/tests/redis.unit.test.ts b/mem0-ts/src/oss/tests/redis.unit.test.ts new file mode 100644 index 000000000..cf3d47174 --- /dev/null +++ b/mem0-ts/src/oss/tests/redis.unit.test.ts @@ -0,0 +1,128 @@ +import { RedisDB } from "../src/vector_stores/redis"; + +jest.mock("redis", () => ({ + createClient: jest.fn(() => ({ + connect: jest.fn(), + on: jest.fn(), + moduleList: jest.fn().mockResolvedValue([{ name: "search", ver: 20800 }]), + ft: { + create: jest.fn(), + search: jest.fn(), + info: jest.fn().mockRejectedValue(new Error("Unknown index")), + _list: jest.fn().mockResolvedValue([]), + }, + hSet: jest.fn(), + hGetAll: jest.fn(), + del: jest.fn(), + exists: jest.fn(), + quit: jest.fn(), + })), +})); + +function createStore(): RedisDB { + return new RedisDB({ + redisUrl: "redis://localhost:6379", + collectionName: "test", + embeddingModelDims: 4, + }); +} + +describe("RedisDB – entity payload handling", () => { + let store: RedisDB; + let mockClient: any; + + beforeAll(async () => { + store = createStore(); + await store.initialize(); + mockClient = (store as any).client; + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test("insert with entity payload (no hash/created_at) does not produce NaN", async () => { + const entityPayload = { + data: "OpenAI", + entityType: "organization", + linkedMemoryIds: ["mem-1"], + userId: "test_user", + }; + + await store.insert([[0.1, 0.2, 0.3, 0.4]], ["entity-1"], [entityPayload]); + + expect(mockClient.hSet).toHaveBeenCalledTimes(1); + const call = mockClient.hSet.mock.calls[0]; + const entry = call[1]; + + expect(entry.memory_id).toBe("entity-1"); + expect(entry.memory).toBe("OpenAI"); + expect(entry.hash).toBe(""); + expect(entry.created_at).toBe(0); + expect(Number.isNaN(entry.created_at)).toBe(false); + }); + + test("update with entity payload (no hash/created_at/updated_at) does not produce NaN", async () => { + const entityPayload = { + data: "OpenAI", + entityType: "organization", + linkedMemoryIds: ["mem-1"], + userId: "test_user", + }; + + await store.update("entity-1", [0.1, 0.2, 0.3, 0.4], entityPayload); + + expect(mockClient.hSet).toHaveBeenCalledTimes(1); + const call = mockClient.hSet.mock.calls[0]; + const entry = call[1]; + + expect(entry.memory_id).toBe("entity-1"); + expect(entry.memory).toBe("OpenAI"); + expect(entry.hash).toBe(""); + expect(entry.created_at).toBe(0); + expect(entry.updated_at).toBe(0); + expect(Number.isNaN(entry.created_at)).toBe(false); + expect(Number.isNaN(entry.updated_at)).toBe(false); + }); + + test("update with normal payload preserves timestamps", async () => { + const normalPayload = { + data: "likes coffee", + hash: "abc123", + createdAt: "2026-06-25T10:00:00.000Z", + updatedAt: "2026-06-25T12:00:00.000Z", + userId: "test_user", + }; + + await store.update("mem-1", [0.1, 0.2, 0.3, 0.4], normalPayload); + + const call = mockClient.hSet.mock.calls[0]; + const entry = call[1]; + + expect(entry.hash).toBe("abc123"); + expect(entry.memory).toBe("likes coffee"); + expect(entry.created_at).toBeGreaterThan(0); + expect(entry.updated_at).toBeGreaterThan(0); + expect(Number.isNaN(entry.created_at)).toBe(false); + expect(Number.isNaN(entry.updated_at)).toBe(false); + }); + + test("insert with normal payload preserves timestamp", async () => { + const normalPayload = { + data: "likes coffee", + hash: "abc123", + createdAt: "2026-06-25T10:00:00.000Z", + userId: "test_user", + }; + + await store.insert([[0.1, 0.2, 0.3, 0.4]], ["mem-1"], [normalPayload]); + + const call = mockClient.hSet.mock.calls[0]; + const entry = call[1]; + + expect(entry.hash).toBe("abc123"); + expect(entry.memory).toBe("likes coffee"); + expect(entry.created_at).toBeGreaterThan(0); + expect(Number.isNaN(entry.created_at)).toBe(false); + }); +});