Files
mem0/integrations/eve/tests/messages.test.ts
T
Himanshu-Sangshetti 75e9efa261 fix(eve): address review — async-write status, reasoning filter, turn-bounded capture, best-effort dedup
Review by @kartik-mem0. Root cause across all four: Mem0's hosted add with
infer:true is async and returns a PENDING event before the memory is visible.

- store.ts/tools.ts: add() returns AddResult{status,eventId} via parseAddResult;
  the v3 add path returns {status:"PENDING", event_id}, so remember reports
  {status:"queued", eventId} for async writes and {status:"saved"} only when
  resolved. FAILED/unknown status maps to completed, not queued.
- messages.ts: drop typed non-text parts (reasoning no longer leaks into memory);
  bound the captured assistant reply to the current turn and collect all of its
  text segments (text->tool->text) instead of a stale prior answer.
- capture.ts + README + eve.mdx: document operationId dedup as best-effort
  (lookup+add is not atomic; pending/concurrent windows can double-write);
  removed the "restart replays cannot write twice" claim.

Tests: 68 pass (+15). Added reasoning, tool-only/cross-turn, multi-segment,
pending/concurrent capture replay, and PENDING/FAILED add-result cases.
Typecheck + build clean.
2026-09-12 00:36:29 +09:00

154 lines
4.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
completedTurnMessages,
conversationMessages,
extractText,
lastUserText,
} from "../src/messages.js";
describe("extractText", () => {
it("reads a string", () => {
expect(extractText(" hello ")).toBe("hello");
});
it("joins text parts", () => {
expect(
extractText([
{ type: "text", text: "I like" },
{ type: "text", text: "tea" },
]),
).toBe("I like\ntea");
});
it("reads a single text object", () => {
expect(extractText({ type: "text", text: " solo " })).toBe("solo");
});
it("ignores non-text parts", () => {
expect(extractText([{ type: "image", url: "x" }])).toBe("");
});
it("ignores reasoning parts and keeps only the final text", () => {
expect(
extractText([
{ type: "reasoning", text: "speculation" },
{ type: "text", text: "Noted." },
]),
).toBe("Noted.");
});
it("still reads an untyped text object", () => {
expect(extractText({ text: " bare " })).toBe("bare");
});
});
describe("lastUserText", () => {
it("returns the last user message", () => {
expect(
lastUserText([
{ role: "user", content: "first" },
{ role: "assistant", content: "ok" },
{ role: "user", content: "second" },
]),
).toBe("second");
});
it("returns empty when there is no user text", () => {
expect(lastUserText([{ role: "assistant", content: "hi" }])).toBe("");
});
});
describe("conversationMessages", () => {
it("keeps user and assistant text only", () => {
expect(
conversationMessages([
{ role: "system", content: "ignore" },
{ role: "user", content: "hello" },
{ role: "assistant", content: "hi" },
{ role: "user", content: "" },
]),
).toEqual([
{ role: "user", content: "hello" },
{ role: "assistant", content: "hi" },
]);
});
});
describe("completedTurnMessages", () => {
it("pairs the current user turn with the latest assistant reply", () => {
expect(
completedTurnMessages({
turnInput: [{ role: "user", content: "I like tea" }],
messages: [
{ role: "user", content: "older" },
{ role: "assistant", content: "older reply" },
{ role: "user", content: "I like tea" },
{ role: "assistant", content: "Noted." },
],
}),
).toEqual([
{ role: "user", content: "I like tea" },
{ role: "assistant", content: "Noted." },
]);
});
it("skips capture when the turn has no user text", () => {
expect(
completedTurnMessages({
turnInput: [{ role: "assistant", content: "hello" }],
messages: [{ role: "assistant", content: "hello" }],
}),
).toEqual([]);
});
it("does not attach a previous turn's answer to a tool-only turn", () => {
// Older Q&A, then a new user message whose only assistant output is a
// tool call (no text). The stale "older reply" must not be captured.
expect(
completedTurnMessages({
turnInput: [{ role: "user", content: "new question" }],
messages: [
{ role: "user", content: "older question" },
{ role: "assistant", content: "older reply" },
{ role: "user", content: "new question" },
{ role: "assistant", content: [{ type: "tool-call", id: "t1" }] },
],
}),
).toEqual([{ role: "user", content: "new question" }]);
});
it("captures all assistant text in a text/tool-call/text turn", () => {
expect(
completedTurnMessages({
turnInput: [{ role: "user", content: "Q" }],
messages: [
{ role: "user", content: "Q" },
{ role: "assistant", content: "part A" },
{ role: "assistant", content: [{ type: "tool-call", id: "t1" }] },
{ role: "assistant", content: "part B" },
],
}),
).toEqual([
{ role: "user", content: "Q" },
{ role: "assistant", content: "part A\npart B" },
]);
});
it("captures this turn's assistant reply, not an earlier one", () => {
expect(
completedTurnMessages({
turnInput: [{ role: "user", content: "new question" }],
messages: [
{ role: "user", content: "older question" },
{ role: "assistant", content: "older reply" },
{ role: "user", content: "new question" },
{ role: "assistant", content: "new reply" },
],
}),
).toEqual([
{ role: "user", content: "new question" },
{ role: "assistant", content: "new reply" },
]);
});
});