feat(cli): add event commands, --json/--agent flag, agent output sanitization, and edge-case hardening for CLI SDKs (#4649)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -96,6 +96,43 @@ def mock_backend():
|
||||
{"name": "alice", "count": 5},
|
||||
{"name": "bob", "count": 3},
|
||||
]
|
||||
backend.list_events.return_value = [
|
||||
{
|
||||
"id": "evt-abc-123-def-456",
|
||||
"event_type": "ADD",
|
||||
"status": "SUCCEEDED",
|
||||
"graph_status": None,
|
||||
"latency": 1234.5,
|
||||
"created_at": "2026-04-01T10:00:00Z",
|
||||
"updated_at": "2026-04-01T10:00:01Z",
|
||||
},
|
||||
{
|
||||
"id": "evt-def-456-ghi-789",
|
||||
"event_type": "SEARCH",
|
||||
"status": "PENDING",
|
||||
"graph_status": None,
|
||||
"latency": None,
|
||||
"created_at": "2026-04-01T10:01:00Z",
|
||||
"updated_at": "2026-04-01T10:01:00Z",
|
||||
},
|
||||
]
|
||||
backend.get_event.return_value = {
|
||||
"id": "evt-abc-123-def-456",
|
||||
"event_type": "ADD",
|
||||
"status": "SUCCEEDED",
|
||||
"graph_status": "SUCCEEDED",
|
||||
"latency": 1234.5,
|
||||
"created_at": "2026-04-01T10:00:00Z",
|
||||
"updated_at": "2026-04-01T10:00:01Z",
|
||||
"results": [
|
||||
{
|
||||
"id": "mem-abc-123",
|
||||
"event": "ADD",
|
||||
"user_id": "alice",
|
||||
"data": {"memory": "User prefers dark mode"},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
return backend
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import typing
|
||||
from io import StringIO
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -16,6 +17,7 @@ from mem0_cli.commands.config_cmd import (
|
||||
cmd_config_show,
|
||||
)
|
||||
from mem0_cli.commands.entities import cmd_entities_delete, cmd_entities_list
|
||||
from mem0_cli.commands.events_cmd import cmd_event_list, cmd_event_status
|
||||
from mem0_cli.commands.memory import (
|
||||
cmd_add,
|
||||
cmd_delete,
|
||||
@@ -176,30 +178,28 @@ class TestAddCommand:
|
||||
def test_add_no_content_exits(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
# Patch stdin.isatty to return True so it doesn't try to read stdin
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
patch("mem0_cli.commands.memory.sys") as mock_sys,
|
||||
patch("mem0_cli.commands.memory._stdin_is_piped", return_value=False),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
):
|
||||
mock_sys.stdin.isatty.return_value = True
|
||||
with pytest.raises((SystemExit, ClickExit)):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
None,
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
messages=None,
|
||||
file=None,
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
categories=None,
|
||||
output="text",
|
||||
)
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
None,
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
messages=None,
|
||||
file=None,
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
categories=None,
|
||||
output="text",
|
||||
)
|
||||
|
||||
def test_add_invalid_metadata_json(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
@@ -279,6 +279,66 @@ class TestAddCommand:
|
||||
mock_backend.add.assert_called_once()
|
||||
|
||||
|
||||
class TestAddDeduplicatesPending:
|
||||
"""Ensure duplicate PENDING entries with the same event_id are collapsed."""
|
||||
|
||||
DUPLICATE_PENDING: typing.ClassVar[dict] = {
|
||||
"results": [
|
||||
{"status": "PENDING", "event_id": "evt-dup"},
|
||||
{"status": "PENDING", "event_id": "evt-dup"},
|
||||
]
|
||||
}
|
||||
|
||||
def _run_add(self, mock_backend, output):
|
||||
mock_backend.add.return_value = self.DUPLICATE_PENDING
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
"test",
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
messages=None,
|
||||
file=None,
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
categories=None,
|
||||
output=output,
|
||||
)
|
||||
return buf.getvalue()
|
||||
|
||||
def test_text_shows_one_pending(self, mock_backend):
|
||||
raw = self._run_add(mock_backend, "text")
|
||||
assert raw.count("Queued") == 1
|
||||
|
||||
def test_json_shows_one_pending(self, mock_backend):
|
||||
raw = self._run_add(mock_backend, "json")
|
||||
data = json.loads(raw)
|
||||
results = data.get("results", data)
|
||||
pending = [r for r in results if r.get("status") == "PENDING"]
|
||||
assert len(pending) == 1
|
||||
|
||||
def test_agent_shows_one_pending(self, mock_backend):
|
||||
from mem0_cli.state import set_agent_mode
|
||||
|
||||
set_agent_mode(True)
|
||||
try:
|
||||
raw = self._run_add(mock_backend, "agent")
|
||||
finally:
|
||||
set_agent_mode(False)
|
||||
data = json.loads(raw)
|
||||
assert data["count"] == 1
|
||||
assert len(data["data"]) == 1
|
||||
|
||||
|
||||
class TestSearchCommand:
|
||||
def test_search_text(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
@@ -618,28 +678,6 @@ class TestDeleteAllCommand:
|
||||
run_id="*",
|
||||
)
|
||||
|
||||
def test_delete_all_project_wide_dry_run(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_delete_all(
|
||||
mock_backend,
|
||||
force=True,
|
||||
all_=True,
|
||||
dry_run=True,
|
||||
user_id=None,
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
output="text",
|
||||
)
|
||||
output = buf.getvalue()
|
||||
assert "project-wide" in output.lower()
|
||||
mock_backend.delete.assert_not_called()
|
||||
|
||||
def test_delete_all_project_wide_async_response(self, mock_backend):
|
||||
mock_backend.delete.return_value = {"message": "Memories deletion started..."}
|
||||
console, buf = _make_console()
|
||||
@@ -888,6 +926,28 @@ class TestEntitiesDeleteCommand:
|
||||
output = buf.getvalue()
|
||||
assert "deleted" in output.lower()
|
||||
|
||||
def test_delete_entity_agent_id(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.entities.console", console),
|
||||
patch("mem0_cli.commands.entities.err_console", err_console),
|
||||
):
|
||||
cmd_entities_delete(
|
||||
mock_backend,
|
||||
user_id=None,
|
||||
agent_id="bot1",
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
force=True,
|
||||
output="text",
|
||||
)
|
||||
mock_backend.delete_entities.assert_called_once_with(
|
||||
user_id=None, agent_id="bot1", app_id=None, run_id=None
|
||||
)
|
||||
output = buf.getvalue()
|
||||
assert "deleted" in output.lower()
|
||||
|
||||
def test_delete_entity_no_id_exits(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
@@ -1024,3 +1084,388 @@ class TestEnableGraph:
|
||||
)
|
||||
call_kwargs = mock_backend.list_memories.call_args
|
||||
assert call_kwargs.kwargs.get("enable_graph") is True
|
||||
|
||||
|
||||
class TestEventCommands:
|
||||
def test_event_list_table(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.events_cmd.console", console),
|
||||
patch("mem0_cli.commands.events_cmd.err_console", err_console),
|
||||
):
|
||||
cmd_event_list(mock_backend, output="table")
|
||||
out = buf.getvalue()
|
||||
assert "evt-abc-" in out
|
||||
assert "ADD" in out
|
||||
assert "SUCCEEDED" in out
|
||||
|
||||
def test_event_list_json(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.events_cmd.console", console),
|
||||
patch("mem0_cli.commands.events_cmd.err_console", err_console),
|
||||
):
|
||||
cmd_event_list(mock_backend, output="json")
|
||||
out = buf.getvalue()
|
||||
assert "evt-abc-123-def-456" in out
|
||||
assert "evt-def-456-ghi-789" in out
|
||||
|
||||
def test_event_list_empty(self, mock_backend):
|
||||
mock_backend.list_events.return_value = []
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.events_cmd.console", console),
|
||||
patch("mem0_cli.commands.events_cmd.err_console", err_console),
|
||||
):
|
||||
cmd_event_list(mock_backend, output="table")
|
||||
out = buf.getvalue()
|
||||
assert "No events" in out
|
||||
|
||||
def test_event_status_text(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.events_cmd.console", console),
|
||||
patch("mem0_cli.commands.events_cmd.err_console", err_console),
|
||||
):
|
||||
cmd_event_status(mock_backend, "evt-abc-123-def-456", output="text")
|
||||
out = buf.getvalue()
|
||||
assert "evt-abc-123-def-456" in out
|
||||
assert "SUCCEEDED" in out
|
||||
|
||||
def test_event_status_json(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.events_cmd.console", console),
|
||||
patch("mem0_cli.commands.events_cmd.err_console", err_console),
|
||||
):
|
||||
cmd_event_status(mock_backend, "evt-abc-123-def-456", output="json")
|
||||
out = buf.getvalue()
|
||||
assert "evt-abc-123-def-456" in out
|
||||
assert "ADD" in out
|
||||
|
||||
|
||||
class TestAgentMode:
|
||||
"""Tests for --json/--agent mode: structured JSON envelope output."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Enable agent mode before each test."""
|
||||
from mem0_cli.state import set_agent_mode
|
||||
|
||||
set_agent_mode(True)
|
||||
|
||||
def teardown_method(self):
|
||||
"""Reset agent mode after each test."""
|
||||
from mem0_cli.state import set_agent_mode
|
||||
|
||||
set_agent_mode(False)
|
||||
|
||||
# ── add ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_add_agent_mode_envelope(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
"I prefer dark mode",
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
messages=None,
|
||||
file=None,
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
categories=None,
|
||||
output="text", # will be overridden to "agent"
|
||||
)
|
||||
raw = buf.getvalue()
|
||||
data = json.loads(raw)
|
||||
assert data["status"] == "success"
|
||||
assert data["command"] == "add"
|
||||
assert "data" in data
|
||||
assert isinstance(data["data"], list)
|
||||
assert data["count"] == 1
|
||||
assert set(data["data"][0].keys()) == {"id", "memory", "event"}
|
||||
|
||||
def test_add_agent_mode_scope(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
"test",
|
||||
user_id="bob",
|
||||
agent_id="agent1",
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
messages=None,
|
||||
file=None,
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
categories=None,
|
||||
output="text",
|
||||
)
|
||||
data = json.loads(buf.getvalue())
|
||||
assert data["scope"]["user_id"] == "bob"
|
||||
assert data["scope"]["agent_id"] == "agent1"
|
||||
|
||||
# ── search ───────────────────────────────────────────────────────────────
|
||||
|
||||
def test_search_agent_mode_envelope(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_search(
|
||||
mock_backend,
|
||||
"dark mode",
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
top_k=10,
|
||||
threshold=0.3,
|
||||
rerank=False,
|
||||
keyword=False,
|
||||
filter_json=None,
|
||||
fields=None,
|
||||
output="text",
|
||||
)
|
||||
data = json.loads(buf.getvalue())
|
||||
assert data["status"] == "success"
|
||||
assert data["command"] == "search"
|
||||
assert isinstance(data["data"], list)
|
||||
assert data["count"] == 2
|
||||
assert "duration_ms" in data
|
||||
assert set(data["data"][0].keys()) == {"id", "memory", "score", "created_at", "categories"}
|
||||
|
||||
# ── list ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_list_agent_mode_envelope(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_list(
|
||||
mock_backend,
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
page=1,
|
||||
page_size=100,
|
||||
category=None,
|
||||
after=None,
|
||||
before=None,
|
||||
output="table", # will be overridden to "agent"
|
||||
)
|
||||
data = json.loads(buf.getvalue())
|
||||
assert data["status"] == "success"
|
||||
assert data["command"] == "list"
|
||||
assert isinstance(data["data"], list)
|
||||
assert data["count"] == 2
|
||||
assert data["scope"]["user_id"] == "alice"
|
||||
assert set(data["data"][0].keys()) == {"id", "memory", "created_at", "categories"}
|
||||
|
||||
# ── get ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_get_agent_mode_envelope(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_get(mock_backend, "abc-123-def-456", output="text")
|
||||
data = json.loads(buf.getvalue())
|
||||
assert data["status"] == "success"
|
||||
assert data["command"] == "get"
|
||||
assert isinstance(data["data"], dict)
|
||||
assert data["data"]["id"] == "abc-123-def-456"
|
||||
assert "memory" in data["data"]
|
||||
assert set(data["data"].keys()) >= {"id", "memory"}
|
||||
|
||||
# ── update ───────────────────────────────────────────────────────────────
|
||||
|
||||
def test_update_agent_mode_envelope(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_update(mock_backend, "abc-123", "Updated content", metadata=None, output="text")
|
||||
data = json.loads(buf.getvalue())
|
||||
assert data["status"] == "success"
|
||||
assert data["command"] == "update"
|
||||
assert isinstance(data["data"], dict)
|
||||
assert "memory" in data["data"]
|
||||
assert "duration_ms" in data
|
||||
|
||||
# ── delete ───────────────────────────────────────────────────────────────
|
||||
|
||||
def test_delete_agent_mode_envelope(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_delete(mock_backend, "abc-123-def-456", output="text")
|
||||
data = json.loads(buf.getvalue())
|
||||
assert data["status"] == "success"
|
||||
assert data["command"] == "delete"
|
||||
assert data["data"]["id"] == "abc-123-def-456"
|
||||
assert data["data"]["deleted"] is True
|
||||
assert "duration_ms" in data
|
||||
|
||||
# ── event list ───────────────────────────────────────────────────────────
|
||||
|
||||
def test_event_list_agent_mode_envelope(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.events_cmd.console", console),
|
||||
patch("mem0_cli.commands.events_cmd.err_console", err_console),
|
||||
):
|
||||
cmd_event_list(mock_backend, output="table")
|
||||
data = json.loads(buf.getvalue())
|
||||
assert data["status"] == "success"
|
||||
assert data["command"] == "event list"
|
||||
assert isinstance(data["data"], list)
|
||||
assert data["count"] == 2
|
||||
assert "duration_ms" in data
|
||||
assert set(data["data"][0].keys()) == {
|
||||
"id",
|
||||
"event_type",
|
||||
"status",
|
||||
"latency",
|
||||
"created_at",
|
||||
}
|
||||
|
||||
# ── event status ─────────────────────────────────────────────────────────
|
||||
|
||||
def test_event_status_agent_mode_envelope(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.events_cmd.console", console),
|
||||
patch("mem0_cli.commands.events_cmd.err_console", err_console),
|
||||
):
|
||||
cmd_event_status(mock_backend, "evt-abc-123-def-456", output="text")
|
||||
data = json.loads(buf.getvalue())
|
||||
assert data["status"] == "success"
|
||||
assert data["command"] == "event status"
|
||||
assert isinstance(data["data"], dict)
|
||||
assert data["data"]["id"] == "evt-abc-123-def-456"
|
||||
assert "duration_ms" in data
|
||||
assert set(data["data"]["results"][0].keys()) == {"id", "event", "user_id", "memory"}
|
||||
assert "data" not in data["data"]["results"][0]
|
||||
|
||||
# ── error handling ───────────────────────────────────────────────────────
|
||||
|
||||
def test_error_in_agent_mode_produces_json_to_stdout(self, mock_backend):
|
||||
"""Errors in agent mode must emit a JSON envelope to stdout, not stderr."""
|
||||
from io import StringIO
|
||||
|
||||
mock_backend.get.side_effect = Exception("Memory not found")
|
||||
console, _buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
|
||||
captured_stdout = StringIO()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
patch("sys.stdout", captured_stdout),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
):
|
||||
cmd_get(mock_backend, "bad-id", output="text")
|
||||
|
||||
stdout_output = captured_stdout.getvalue()
|
||||
# The error JSON envelope must be on stdout
|
||||
error_data = json.loads(stdout_output)
|
||||
assert error_data["status"] == "error"
|
||||
assert "error" in error_data
|
||||
assert error_data["data"] is None
|
||||
|
||||
def test_branding_suppressed_in_agent_mode(self, mock_backend):
|
||||
"""Scope line and success message must be absent in agent mode output."""
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
"branding test",
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
messages=None,
|
||||
file=None,
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
categories=None,
|
||||
output="text",
|
||||
)
|
||||
output = buf.getvalue()
|
||||
# Must be valid JSON only — no human-readable branding
|
||||
data = json.loads(output)
|
||||
assert data["status"] == "success"
|
||||
# "Scope:" and "Memory processed" must NOT appear in the raw output
|
||||
assert "Scope:" not in output
|
||||
assert "Memory processed" not in output
|
||||
assert "spinner" not in output.lower()
|
||||
|
||||
def test_no_spinner_in_agent_mode(self, mock_backend):
|
||||
"""timed_status must not emit spinner output in agent mode."""
|
||||
err_buf = StringIO()
|
||||
err_console_buf = Console(file=err_buf, force_terminal=False, no_color=True, width=120)
|
||||
console, _buf = _make_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console_buf),
|
||||
):
|
||||
cmd_search(
|
||||
mock_backend,
|
||||
"query",
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
top_k=5,
|
||||
threshold=0.3,
|
||||
rerank=False,
|
||||
keyword=False,
|
||||
filter_json=None,
|
||||
fields=None,
|
||||
output="text",
|
||||
)
|
||||
# The err_buf captures what would have been spinner/timing noise
|
||||
# In agent mode it should be empty (no status lines printed)
|
||||
err_output = err_buf.getvalue()
|
||||
assert "Searching" not in err_output
|
||||
|
||||
@@ -11,6 +11,7 @@ from mem0_cli.output import (
|
||||
format_memories_table,
|
||||
format_memories_text,
|
||||
format_single_memory,
|
||||
sanitize_agent_data,
|
||||
)
|
||||
|
||||
|
||||
@@ -129,8 +130,153 @@ class TestAddResult:
|
||||
output = buf.getvalue()
|
||||
assert output.strip() == ""
|
||||
|
||||
def test_format_add_result_deduplicates_pending_by_event_id(self):
|
||||
console, buf = _make_console()
|
||||
result = {
|
||||
"results": [
|
||||
{"status": "PENDING", "event_id": "evt-dup"},
|
||||
{"status": "PENDING", "event_id": "evt-dup"},
|
||||
]
|
||||
}
|
||||
format_add_result(console, result, "text")
|
||||
output = buf.getvalue()
|
||||
# Should show only one PENDING block despite two entries with same event_id
|
||||
assert output.count("evt-dup") == 2 # event_id line + status hint line
|
||||
assert output.count("Queued") == 1
|
||||
|
||||
def test_format_add_result_empty(self):
|
||||
console, buf = _make_console()
|
||||
format_add_result(console, {"results": []}, "text")
|
||||
output = buf.getvalue()
|
||||
assert "No memories extracted" in output
|
||||
|
||||
|
||||
class TestSanitizeAgentData:
|
||||
def test_add_projects_fields(self):
|
||||
raw = [
|
||||
{
|
||||
"id": "abc",
|
||||
"memory": "test",
|
||||
"event": "ADD",
|
||||
"metadata": {"x": 1},
|
||||
"categories": ["a"],
|
||||
}
|
||||
]
|
||||
result = sanitize_agent_data("add", raw)
|
||||
assert result == [{"id": "abc", "memory": "test", "event": "ADD"}]
|
||||
|
||||
def test_add_pending_passthrough(self):
|
||||
raw = [{"status": "PENDING", "event_id": "evt-123", "metadata": "noise"}]
|
||||
result = sanitize_agent_data("add", raw)
|
||||
assert result == [{"status": "PENDING", "event_id": "evt-123"}]
|
||||
|
||||
def test_search_projects_fields(self):
|
||||
raw = [
|
||||
{
|
||||
"id": "abc",
|
||||
"memory": "test",
|
||||
"score": 0.9,
|
||||
"created_at": "2026-01-01",
|
||||
"categories": ["a"],
|
||||
"user_id": "u1",
|
||||
"agent_id": None,
|
||||
}
|
||||
]
|
||||
result = sanitize_agent_data("search", raw)
|
||||
assert result == [
|
||||
{
|
||||
"id": "abc",
|
||||
"memory": "test",
|
||||
"score": 0.9,
|
||||
"created_at": "2026-01-01",
|
||||
"categories": ["a"],
|
||||
}
|
||||
]
|
||||
|
||||
def test_list_projects_fields(self):
|
||||
raw = [
|
||||
{
|
||||
"id": "abc",
|
||||
"memory": "test",
|
||||
"created_at": "2026-01-01",
|
||||
"categories": ["a"],
|
||||
"user_id": "u1",
|
||||
}
|
||||
]
|
||||
result = sanitize_agent_data("list", raw)
|
||||
assert result == [
|
||||
{"id": "abc", "memory": "test", "created_at": "2026-01-01", "categories": ["a"]}
|
||||
]
|
||||
|
||||
def test_get_projects_fields(self):
|
||||
raw = {
|
||||
"id": "abc",
|
||||
"memory": "test",
|
||||
"created_at": "2026-01-01",
|
||||
"updated_at": "2026-01-02",
|
||||
"categories": ["a"],
|
||||
"metadata": {"k": "v"},
|
||||
"user_id": "u1",
|
||||
}
|
||||
result = sanitize_agent_data("get", raw)
|
||||
assert "user_id" not in result
|
||||
assert "id" in result and "memory" in result
|
||||
|
||||
def test_update_projects_fields(self):
|
||||
raw = {"id": "abc", "memory": "updated", "extra": "noise"}
|
||||
result = sanitize_agent_data("update", raw)
|
||||
assert result == {"id": "abc", "memory": "updated"}
|
||||
|
||||
def test_event_list_projects_fields(self):
|
||||
raw = [
|
||||
{
|
||||
"id": "evt-1",
|
||||
"event_type": "ADD",
|
||||
"status": "SUCCEEDED",
|
||||
"graph_status": None,
|
||||
"latency": 100.0,
|
||||
"created_at": "2026-01-01",
|
||||
"updated_at": "2026-01-02",
|
||||
}
|
||||
]
|
||||
result = sanitize_agent_data("event list", raw)
|
||||
assert result == [
|
||||
{
|
||||
"id": "evt-1",
|
||||
"event_type": "ADD",
|
||||
"status": "SUCCEEDED",
|
||||
"latency": 100.0,
|
||||
"created_at": "2026-01-01",
|
||||
}
|
||||
]
|
||||
assert "updated_at" not in result[0]
|
||||
assert "graph_status" not in result[0]
|
||||
|
||||
def test_event_status_flattens_results(self):
|
||||
raw = {
|
||||
"id": "evt-1",
|
||||
"event_type": "ADD",
|
||||
"status": "SUCCEEDED",
|
||||
"latency": 100.0,
|
||||
"created_at": "2026-01-01",
|
||||
"updated_at": "2026-01-02",
|
||||
"results": [
|
||||
{"id": "mem-1", "event": "ADD", "user_id": "alice", "data": {"memory": "dark mode"}}
|
||||
],
|
||||
}
|
||||
result = sanitize_agent_data("event status", raw)
|
||||
assert result["results"][0] == {
|
||||
"id": "mem-1",
|
||||
"event": "ADD",
|
||||
"user_id": "alice",
|
||||
"memory": "dark mode",
|
||||
}
|
||||
assert "data" not in result["results"][0]
|
||||
|
||||
def test_passthrough_commands(self):
|
||||
for cmd in ("status", "import", "config show", "config get", "config set"):
|
||||
data = {"key": "value", "other": "stuff"}
|
||||
assert sanitize_agent_data(cmd, data) == data
|
||||
|
||||
def test_none_data(self):
|
||||
assert sanitize_agent_data("add", None) is None
|
||||
|
||||
Reference in New Issue
Block a user