Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 90 additions & 64 deletions tests/regression/test_powermem_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@

Automated test script based on test report, covering:
- Basic operations (--help, --version, config init/show/validate/test)
- Data operations (add, update, delete, delete-all)
- Core commands (list, search, get)
- Data operations (pmem memory add/update/delete/delete-all)
- Core commands (pmem memory list/search/get)
- Other commands (stats, manage backup/restore/cleanup/migrate, shell)

Usage:
pytest tests/regression/test_powermem_cli.py -v
pytest tests/regression/test_powermem_cli.py -v -k "test_help" # Run specific tests

Requires ``.env`` at the repository root (resolved from this file, not the process cwd).
"""

import subprocess
Expand All @@ -21,15 +23,32 @@
import time
import tempfile
import shutil
import shlex
import re
import pytest
from typing import Tuple, List, Optional


# ==================== Configuration ====================

# Environment file path (same level as tests directory)
ENV_FILE = ".env"
# Repo root `.env` — must not depend on pytest cwd (running from `tests/regression/`
# would otherwise resolve `.env` to `tests/regression/.env`, which does not exist).
_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
ENV_FILE = os.path.join(_REPO_ROOT, ".env")


def _pmem_shell_invocation() -> str:
"""
Shell fragment to run the PowerMem CLI (equivalent to `pmem` / `powermem-cli`).

Prefer PATH-installed console scripts; fall back to ``python -m powermem.cli.main``
so tests pass when only the venv interpreter is available (common in CI / pytest).
"""
for name in ("pmem", "powermem-cli"):
path = shutil.which(name)
if path:
return path
return f"{shlex.quote(sys.executable)} -m powermem.cli.main"


# ==================== Fixtures ====================
Expand Down Expand Up @@ -89,7 +108,7 @@ def cleanup_test_data(cli_runner, test_data):
# ==================== CLI Runner ====================

class CLIRunner:
"""CLI runner for executing commands"""
"""CLI runner for executing commands (uses ``pmem memory …``, not legacy ``pmem add``)."""

def __init__(self, env_file: str = ".env"):
self.env_file = env_file
Expand Down Expand Up @@ -117,8 +136,9 @@ def run_command(self, cmd: str, timeout: int = 60, input_text: str = None) -> Tu
return -1, "", str(e)

def pmem(self, args: str, timeout: int = 60, input_text: str = None) -> Tuple[int, str, str]:
"""Execute pmem command"""
cmd = f"pmem -f {self.env_file} {args}"
"""Execute ``pmem -f <env> <args>`` (e.g. ``memory add`` → ``pmem -f .env memory add``)."""
env_path = shlex.quote(os.path.abspath(self.env_file))
cmd = f"{_pmem_shell_invocation()} -f {env_path} {args}"
return self.run_command(cmd, timeout, input_text)


Expand Down Expand Up @@ -207,7 +227,7 @@ def test_pmem_version(self, cli_runner):

def test_powermem_cli_help(self, cli_runner):
"""powermem-cli --help should display help information"""
rc, out, err = cli_runner.run_command("powermem-cli --help")
rc, out, err = cli_runner.run_command(f"{_pmem_shell_invocation()} --help")
assert_contains(rc, out, err, ["PowerMem CLI", "Examples", "Options", "Commands"])

def test_powermem_cli_version(self, cli_runner):
Expand Down Expand Up @@ -311,11 +331,12 @@ def test_memory_add_basic(self, cli_runner, test_data):
test_data["memory_ids"].append(memory_id)

def test_memory_add_with_metadata(self, cli_runner, test_data):
"""memory add --metadata should successfully add memory with metadata, output format [SUCCESS] Memory ADD: ID=xxx"""
"""memory add --metadata should add with metadata; output stays [SUCCESS] Memory ADD: ID=…"""
# --no-infer: avoid intelligent path returning no rows / non-ADD events; still validates --metadata.
rc, out, err = cli_runner.pmem(
f'memory add "CLI2 is my best friend" --user-id {test_data["user_id"]} '
f'--agent-id {test_data["agent_id"]} --run-id {test_data["run_id"]} '
f'--metadata \'{{"category":"test"}}\''
f'--metadata \'{{"category":"test"}}\' --no-infer'
)
assert_success(rc, out, err)
assert_contains(rc, out, err, ["[SUCCESS]", "Memory ADD", "ID="])
Expand All @@ -337,7 +358,7 @@ def test_memory_add_no_infer(self, cli_runner, test_data):

def test_memory_add_without_ids(self, cli_runner):
"""memory add without ID parameters should succeed, output format [SUCCESS] Memory ADD: ID=xxx"""
rc, out, err = cli_runner.pmem('memory add "I like drinking coffee"')
rc, out, err = cli_runner.pmem('memory add "user100 is 100 years old"')
assert_success(rc, out, err)
assert_contains(rc, out, err, ["[SUCCESS]", "Memory ADD", "ID="])

Expand All @@ -361,12 +382,16 @@ class TestMemoryUpdate:

def test_memory_update_basic(self, cli_runner, test_data):
"""memory update should successfully update memory"""
if not test_data["memory_ids"]:
pytest.skip("No memory_id; run TestMemoryAdd first (same module) or full file")
rc, out, err = cli_runner.pmem(f'memory update {test_data["memory_ids"][0]} "I like drinking tea"')
assert_success(rc, out, err)
assert_contains(rc, out, err, ["[SUCCESS]", "Memory updated", "ID="])

def test_memory_update_with_metadata(self, cli_runner, test_data):
"""memory update --metadata should successfully update metadata"""
if not test_data["memory_ids"]:
pytest.skip("No memory_id; run TestMemoryAdd first (same module) or full file")
rc, out, err = cli_runner.pmem(
f'memory update {test_data["memory_ids"][0]} "I like drinking coffee" --metadata \'{{"updated":true}}\''
)
Expand All @@ -385,52 +410,6 @@ def test_memory_update_missing_args(self, cli_runner):
assert_contains(rc, out, err, ["Missing", "Error", "MEMORY_ID"])



class TestMemoryDelete:
"""Test memory delete command"""

def test_memory_delete_basic(self, cli_runner, test_data):
"""memory delete --yes should successfully delete memory"""
# First add a memory for deletion
rc, out, err = cli_runner.pmem(
f'memory add "Memory to be deleted" --user-id {test_data["user_id"]} '
f'--agent-id {test_data["agent_id"]} --no-infer'
)
assert_contains(rc, out, err, ["[SUCCESS]", "Memory ADD", "ID="])
delete_id = extract_memory_id(out)
assert delete_id is not None, f"Failed to extract memory_id from output\nstdout: {out}"

rc, out, err = cli_runner.pmem(f"memory delete {delete_id} --yes")
assert_success(rc, out, err)

def test_memory_delete_nonexistent(self, cli_runner):
"""memory delete with non-existent ID should fail"""
rc, out, err = cli_runner.pmem("memory delete 999999999999 --yes")
assert_contains(rc, out, err, ["not found", "ERROR", "denied"])

def test_memory_delete_missing_id(self, cli_runner):
"""memory delete with missing ID should fail"""
rc, out, err = cli_runner.pmem("memory delete --yes")
assert_failure(rc, out, err, "Missing", "MEMORY_ID")


class TestMemoryDeleteAll:
"""Test memory delete-all command"""

def test_memory_delete_all_nonexistent_user(self, cli_runner):
"""memory delete-all for non-existent user should succeed (delete 0 records)"""
rc, out, err = cli_runner.pmem(
"memory delete-all --user-id nonexistent_user_xyz --confirm",
input_text="y\n"
)
assert_success(rc, out, err, "[SUCCESS] All matching memories deleted")

def test_memory_delete_all_missing_confirm(self, cli_runner):
"""memory delete-all without --confirm should fail"""
rc, out, err = cli_runner.pmem("memory delete-all --user-id test")
assert_failure(rc, out, err, "Add --confirm flag to proceed")


# ==================== Core Commands Tests ====================

class TestMemoryList:
Expand Down Expand Up @@ -488,14 +467,16 @@ def test_memory_list_user_agent_id_truncation_shows_ellipsis(self, cli_runner):
long_agent_id = "list_agent_id_1234567890"
try:
rc, out, err = cli_runner.pmem(
f'memory add "list long ids test" --user-id {long_user_id} --agent-id {long_agent_id}'
f'memory add "user1 is 1 year old" --user-id {long_user_id} --agent-id {long_agent_id} '
f"--no-infer"
)
assert_success(rc, out, err, "add")
assert_contains(rc, out, err, ["[SUCCESS]", "Memory ADD", "ID="])

rc, out, err = cli_runner.pmem(
f"memory list --user-id {long_user_id} --agent-id {long_agent_id} --limit 5"
)
assert_success(rc, out, err, "found", "id", "user id", "agent id", "content")
# "Showing" only appears when the list is non-empty (avoid matching "No memories found").
assert_success(rc, out, err, "Found", "ID", "User ID", "Agent ID", "Content", "...")
combined = (out + err).lower()
assert "list_user_id_1234..." in combined, \
f"Expected truncated user_id with ellipsis, got output:\n{out}\n{err}"
Expand Down Expand Up @@ -860,10 +841,9 @@ def test_shell_list_user_id_truncation_shows_ellipsis(self, cli_runner):
long_user_id = "shell_user_id_1234567890"
try:
rc, out, err = cli_runner.pmem(
f'memory add "shell long user id test" --user-id {long_user_id} --agent-id shell_agent'
f'memory add "user2 is 2 year old" --user-id {long_user_id} --agent-id shell_agent'
)
# Be tolerant to wording differences like "ADD" vs "added".
assert_success(rc, out, err, "add")
assert_success(rc, out, err, "[SUCCESS]", "Memory ADD", "ID=")

last_rc, last_out, last_err = 0, "", ""
for _ in range(5):
Expand All @@ -877,7 +857,8 @@ def test_shell_list_user_id_truncation_shows_ellipsis(self, cli_runner):
break
time.sleep(0.5)

assert_success(last_rc, last_out, last_err, "found", "memories")
# Interactive shell prints "Found N memories:" — not the same as "No memories found".
assert_success(last_rc, last_out, last_err, "memories:", "User ID")
combined = (last_out + last_err).lower()
assert (
"shell_user_id_12345..." in combined
Expand All @@ -900,6 +881,51 @@ def test_shell_quit(self, cli_runner):
assert_contains(rc, out, err, ["Goodbye"])


class TestMemoryDelete:
"""Test memory delete command"""

def test_memory_delete_basic(self, cli_runner, test_data):
"""memory delete --yes should successfully delete memory"""
# First add a memory for deletion
rc, out, err = cli_runner.pmem(
f'memory add "Memory to be deleted" --user-id {test_data["user_id"]} '
f'--agent-id {test_data["agent_id"]} --no-infer'
)
assert_contains(rc, out, err, ["[SUCCESS]", "Memory ADD", "ID="])
delete_id = extract_memory_id(out)
assert delete_id is not None, f"Failed to extract memory_id from output\nstdout: {out}"

rc, out, err = cli_runner.pmem(f"memory delete {delete_id} --yes")
assert_success(rc, out, err)

def test_memory_delete_nonexistent(self, cli_runner):
"""memory delete with non-existent ID should fail"""
rc, out, err = cli_runner.pmem("memory delete 999999999999 --yes")
assert_contains(rc, out, err, ["not found", "ERROR", "denied"])

def test_memory_delete_missing_id(self, cli_runner):
"""memory delete with missing ID should fail"""
rc, out, err = cli_runner.pmem("memory delete --yes")
assert_failure(rc, out, err, "Missing", "MEMORY_ID")


class TestMemoryDeleteAll:
"""Test memory delete-all command"""

def test_memory_delete_all_nonexistent_user(self, cli_runner):
"""memory delete-all for non-existent user should succeed (delete 0 records)"""
rc, out, err = cli_runner.pmem(
"memory delete-all --user-id nonexistent_user_xyz --confirm",
input_text="y\n"
)
assert_success(rc, out, err, "[SUCCESS] All matching memories deleted")

def test_memory_delete_all_missing_confirm(self, cli_runner):
"""memory delete-all without --confirm should fail"""
rc, out, err = cli_runner.pmem("memory delete-all --user-id test")
assert_failure(rc, out, err, "Add --confirm flag to proceed")


# ==================== Main Entry Point ====================

if __name__ == "__main__":
Expand Down
6 changes: 3 additions & 3 deletions tests/regression/test_scenario_1_basic_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -650,9 +650,9 @@ def test_data_consistency() -> None:

# Test get_all consistency
print("\nTesting get_all consistency...")
memory.add("Memory 1", user_id=user_id)
memory.add("Memory 2", user_id=user_id)
memory.add("Memory 3", user_id=user_id)
memory.add("user1 is 18 years old", user_id=user_id)
memory.add("user2 is 20 years old", user_id=user_id)
memory.add("user3 is 22 years old", user_id=user_id)

all_memories = memory.get_all(user_id=user_id)
results_list = list(_get_results_list(all_memories))
Expand Down
8 changes: 5 additions & 3 deletions tests/regression/test_scenario_3_multi_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,18 +240,20 @@ def test_step6_memory_scopes() -> None:
"Agent-specific memory",
user_id="user123",
metadata={"scope": "AGENT"},
infer=False,
)
agent.add(
"User-specific memory",
user_id="user123",
metadata={"scope": "USER"},
infer=False,
)

print("Agent-scoped memories:")
results = agent.search(
query="memories",
user_id="user123",
filters={"metadata.scope": "AGENT"}
filters={"scope": "AGENT"}
)
for result in results.get('results', []):
print(f" - {result['memory']}")
Expand Down Expand Up @@ -562,7 +564,7 @@ def test_agent_metadata_filtering() -> None:
category_results = agent.search(
query="customer",
user_id=user_id,
filters={"metadata.category": "communication"}
filters={"category": "communication"}
)
category_memories = _get_results(category_results)
print(f" Found {len(category_memories)} memories with category='communication'")
Expand All @@ -573,7 +575,7 @@ def test_agent_metadata_filtering() -> None:
priority_results = agent.search(
query="customer",
user_id=user_id,
filters={"metadata.priority": "high"}
filters={"priority": "high"}
)
priority_memories = _get_results(priority_results)
print(f" Found {len(priority_memories)} memories with priority='high'")
Expand Down
2 changes: 1 addition & 1 deletion tests/regression/test_scenario_7_multimodal.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ def test_step2_search_image_memories(memory) -> None:

def test_step3_add_audio_memory(memory) -> None:
_print_step("Step 3: Add Audio Memory (OpenAI multimodal)")
audio_url = "https://sis-sample-audio.obs.cn-north-1.myhuaweicloud.com/16k16bit.wav" # Must be a URL
audio_url = "https://storage.googleapis.com/cloud-samples-data/speech/commercial_mono.wav" # Must be a URL
messages = [
{
"role": "user",
Expand Down
Loading