a2516269cc
CLI now consumes the unified mem0_notice surface that the platform side
emits for unclaimed Agent Mode keys. The notice is a directive to the
LLM agent reading the output, with a verbatim sentence to relay to the
human owner. Two presentation paths:
- Human/text output: yellow stderr banner after the primary output,
once per command. Skipped in agent mode (the JSON envelope carries
it instead, so no duplication).
- JSON/agent output (--json/--agent): folded into the envelope as
"mem0_notice" so an agent parsing the output sees it without
inspecting HTTP headers.
CLI changes (Python + Node, kept in lockstep):
- state.{ts,py}: captureNotice / takeNotice helpers — last-write-wins
stash so multi-request commands fire the notice exactly once.
- backend/platform.{ts,py}: _request extracts notice from response
bodies (top-level dict or list[0]) with header fallback, strips
from downstream payload, captures for end-of-command surfacing.
- output.{ts,py}: JSON envelope formatters fold in any pending notice.
- index.ts / app.py: entrypoint surfaces notice on exit when not in
agent mode.
- commands/agent-mode.{ts,py}: init success path prints the platform's
notice verbatim (fallback to dim claim-command line if a stale
backend doesn't return it).
Init-flag handling fix: the Python argv preprocessor was stripping
--agent from sys.argv unconditionally as the global JSON-output alias.
That swallowed `mem0 init --agent` (where --agent is a subcommand flag
for unattended bootstrap). Now preserved when "init" is in argv.
Parity tests: cli/python/tests/test_agent_mode.py and
cli/node/tests/agent-mode.test.ts — 7 tests each, kept in sync.
cli-spec.json updated: init now lists --agent and --source.
Docs:
- README.md: Agent Mode promo at top of Quickstart.
- docs/llms.txt: fast-path block for AI agents reading the docs.
- skills/mem0/SKILL.md, skills/mem0-cli/SKILL.md,
skills/mem0-integrate/SKILL.md, mem0-plugin/skills/mem0/SKILL.md:
autonomous-setup section + fallback hints.
- mem0-plugin/README.md, openclaw/README.md: "Quick path for agents"
blocks above the human Quick Start.
158 lines
5.5 KiB
Python
158 lines
5.5 KiB
Python
"""Parity tests for `mem0 init --agent` (Agent Mode bootstrap).
|
|
|
|
Mirror of ``cli/node/tests/agent-mode.test.ts`` — both files MUST stay in
|
|
sync so that the Python and Node CLIs expose an identical surface for the
|
|
Agent Mode entrypoint. If you add a flag here, add the same assertion on
|
|
the Node side (and vice versa).
|
|
|
|
Network-bound bootstrap is covered by the platform-side E2E suite
|
|
(``backend/tests/e2e/test_05_agent_mode.py``); these tests only verify
|
|
the CLI surface that ships in the binary.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[mKJHABCDfsu]")
|
|
|
|
|
|
def _strip_ansi(text: str) -> str:
|
|
return _ANSI_RE.sub("", text)
|
|
|
|
|
|
def _run(args: list[str], home_dir: str | None = None) -> subprocess.CompletedProcess:
|
|
env = os.environ.copy()
|
|
for key in list(env.keys()):
|
|
if key.startswith("MEM0_"):
|
|
del env[key]
|
|
env.pop("FORCE_COLOR", None)
|
|
if home_dir:
|
|
env["HOME"] = home_dir
|
|
result = subprocess.run(
|
|
[sys.executable, "-m", "mem0_cli", *args],
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
timeout=15,
|
|
)
|
|
return subprocess.CompletedProcess(
|
|
args=result.args,
|
|
returncode=result.returncode,
|
|
stdout=_strip_ansi(result.stdout),
|
|
stderr=_strip_ansi(result.stderr),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def clean_home(tmp_path):
|
|
return str(tmp_path)
|
|
|
|
|
|
class TestInitFlagSurface:
|
|
"""`mem0 init --help` must expose the Agent Mode flags."""
|
|
|
|
def test_init_help_lists_agent_flag(self):
|
|
result = _run(["init", "--help"])
|
|
assert result.returncode == 0
|
|
assert "--agent" in result.stdout
|
|
|
|
def test_init_help_describes_agent_mode(self):
|
|
result = _run(["init", "--help"])
|
|
assert result.returncode == 0
|
|
# Description must mention what --agent actually does so an agent
|
|
# reading the help can self-discover the bootstrap entrypoint.
|
|
assert "Agent Mode" in result.stdout or "unattended" in result.stdout.lower()
|
|
|
|
def test_init_help_lists_source_flag(self):
|
|
result = _run(["init", "--help"])
|
|
assert result.returncode == 0
|
|
assert "--source" in result.stdout
|
|
|
|
def test_init_help_lists_email_and_code(self):
|
|
# Claim flow flags must remain present alongside Agent Mode flags.
|
|
result = _run(["init", "--help"])
|
|
assert result.returncode == 0
|
|
assert "--email" in result.stdout
|
|
assert "--code" in result.stdout
|
|
|
|
|
|
class TestArgvPreprocessing:
|
|
"""`--agent` on `init` must reach init_cmd, not be eaten by the global preprocessor.
|
|
|
|
Regression for the bug where the top-level `--agent` JSON-alias was
|
|
stripped from ``sys.argv`` before Typer could bind it to the init
|
|
subcommand, making ``mem0 init --agent`` indistinguishable from a
|
|
plain ``mem0 init`` (interactive wizard).
|
|
"""
|
|
|
|
def test_init_with_agent_reaches_subcommand(self, clean_home):
|
|
# We can't hit a real backend in unit tests, so we point the CLI at
|
|
# a guaranteed-dead URL and assert the failure is the bootstrap
|
|
# request failing — proving the --agent flag was honored and the
|
|
# bootstrap branch ran, not the interactive wizard.
|
|
result = subprocess.run(
|
|
[sys.executable, "-m", "mem0_cli", "init", "--agent"],
|
|
capture_output=True,
|
|
text=True,
|
|
env={
|
|
**{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")},
|
|
"HOME": clean_home,
|
|
"MEM0_BASE_URL": "http://127.0.0.1:1", # blackhole
|
|
"FORCE_COLOR": "0",
|
|
},
|
|
timeout=15,
|
|
)
|
|
combined = _strip_ansi(result.stdout + result.stderr).lower()
|
|
# Either we got a connection/network error from the bootstrap POST,
|
|
# or the CLI surfaced an Agent Mode-specific failure message.
|
|
assert (
|
|
"agent" in combined
|
|
or "connect" in combined
|
|
or "network" in combined
|
|
or "fetch" in combined
|
|
or "bootstrap" in combined
|
|
), f"Expected bootstrap attempt, got: {combined!r}"
|
|
|
|
|
|
class TestJsonEnvelopeParity:
|
|
"""`mem0 init --agent --json` should produce a JSON envelope on success.
|
|
|
|
Without a live backend we can only assert the failure shape: when the
|
|
backend is unreachable, the CLI must still exit non-zero AND not crash
|
|
on a Python traceback (which would mean we leaked an exception past
|
|
the agent-mode handler).
|
|
"""
|
|
|
|
def test_init_agent_json_no_traceback_on_network_failure(self, clean_home):
|
|
result = subprocess.run(
|
|
[sys.executable, "-m", "mem0_cli", "init", "--agent", "--json"],
|
|
capture_output=True,
|
|
text=True,
|
|
env={
|
|
**{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")},
|
|
"HOME": clean_home,
|
|
"MEM0_BASE_URL": "http://127.0.0.1:1",
|
|
"FORCE_COLOR": "0",
|
|
},
|
|
timeout=15,
|
|
)
|
|
combined = _strip_ansi(result.stdout + result.stderr)
|
|
assert "Traceback (most recent call last)" not in combined
|
|
assert result.returncode != 0
|
|
|
|
|
|
class TestInitInCommandList:
|
|
"""`mem0 --help` must list `init` so agents walking the top-level help
|
|
can discover the Agent Mode entrypoint without prior knowledge."""
|
|
|
|
def test_top_level_help_lists_init(self):
|
|
result = _run(["--help"])
|
|
assert result.returncode == 0
|
|
assert "init" in result.stdout
|