mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
2c4d923ec1
Addresses issue #553 - CLI JSON improvements for OpenClaw plugin integration Changes: - Added --format json flag to write-note, read-note, recent-activity - Added --page and --page-size options to recent-activity (fixes hardcoded pagination) - Added --project parameter to recent-activity - Added --format json to build-context for CLI consistency - Implemented JSON helpers that call API directly to avoid double round-trips - _write_note_json() returns entity metadata without second lookup - _read_note_json() handles titles, permalinks, and memory:// URLs correctly - _recent_activity_json() supports pagination parameters Tests: - Comprehensive test coverage in tests/cli/test_cli_json_format.py - Tests for JSON format output - Tests for pagination parameters - Tests for identifier handling (titles, permalinks, URLs) - Tests for project parameter interaction Resolves: #553 Co-authored-by: bm-clawd <bm-clawd@users.noreply.github.com> Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
283 lines
9.7 KiB
Python
283 lines
9.7 KiB
Python
"""Tests for CLI --format json functionality.
|
|
|
|
These tests verify that the --format json flag works correctly for
|
|
write-note, read-note, recent-activity, and build-context commands.
|
|
|
|
See issue #553 for context on these improvements.
|
|
"""
|
|
|
|
import json
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from typer.testing import CliRunner
|
|
|
|
from basic_memory.cli.main import app as cli_app
|
|
|
|
runner = CliRunner()
|
|
|
|
|
|
class TestWriteNoteJsonFormat:
|
|
"""Tests for write-note --format json."""
|
|
|
|
@patch("basic_memory.cli.commands.tool._write_note_json")
|
|
@patch("basic_memory.cli.commands.tool.ConfigManager")
|
|
def test_write_note_json_format(self, mock_config_manager, mock_write_json):
|
|
"""Test write-note with --format json returns structured data."""
|
|
# Mock config
|
|
mock_config = MagicMock()
|
|
mock_config.default_project = "test-project"
|
|
mock_config_manager.return_value = mock_config
|
|
mock_config_manager.return_value.get_project.return_value = ("test-project", "/tmp/test")
|
|
|
|
# Mock the JSON helper to return entity data
|
|
mock_entity_data = {
|
|
"id": "123",
|
|
"title": "Test Note",
|
|
"permalink": "test-note",
|
|
"file_path": "/tmp/test/test-note.md",
|
|
"checksum": "abc123",
|
|
}
|
|
mock_write_json.return_value = mock_entity_data
|
|
|
|
# Run command
|
|
result = runner.invoke(
|
|
cli_app,
|
|
[
|
|
"tool",
|
|
"write-note",
|
|
"--title",
|
|
"Test Note",
|
|
"--folder",
|
|
"notes",
|
|
"--content",
|
|
"Test content",
|
|
"--format",
|
|
"json",
|
|
],
|
|
)
|
|
|
|
# Verify success
|
|
assert result.exit_code == 0
|
|
|
|
# Verify JSON output
|
|
output = json.loads(result.output)
|
|
assert output["title"] == "Test Note"
|
|
assert output["permalink"] == "test-note"
|
|
assert "checksum" in output
|
|
|
|
# Verify the JSON helper was called (not the MCP tool)
|
|
mock_write_json.assert_called_once()
|
|
|
|
|
|
class TestReadNoteJsonFormat:
|
|
"""Tests for read-note --format json."""
|
|
|
|
@patch("basic_memory.cli.commands.tool._read_note_json")
|
|
@patch("basic_memory.cli.commands.tool.ConfigManager")
|
|
def test_read_note_json_format(self, mock_config_manager, mock_read_json):
|
|
"""Test read-note with --format json returns structured data."""
|
|
# Mock config
|
|
mock_config = MagicMock()
|
|
mock_config.default_project = "test-project"
|
|
mock_config_manager.return_value = mock_config
|
|
mock_config_manager.return_value.get_project.return_value = ("test-project", "/tmp/test")
|
|
|
|
# Mock the JSON helper to return entity data
|
|
mock_entity_data = {
|
|
"id": "123",
|
|
"title": "Test Note",
|
|
"permalink": "test-note",
|
|
"content": "Test content",
|
|
}
|
|
mock_read_json.return_value = mock_entity_data
|
|
|
|
# Run command
|
|
result = runner.invoke(
|
|
cli_app,
|
|
["tool", "read-note", "test-note", "--format", "json"],
|
|
)
|
|
|
|
# Verify success
|
|
assert result.exit_code == 0
|
|
|
|
# Verify JSON output
|
|
output = json.loads(result.output)
|
|
assert output["title"] == "Test Note"
|
|
assert output["content"] == "Test content"
|
|
|
|
# Verify the JSON helper was called
|
|
mock_read_json.assert_called_once()
|
|
|
|
@patch("basic_memory.cli.commands.tool._read_note_json")
|
|
@patch("basic_memory.cli.commands.tool.ConfigManager")
|
|
def test_read_note_handles_plain_title(self, mock_config_manager, mock_read_json):
|
|
"""Test read-note with plain title (not permalink) works with JSON format."""
|
|
# Mock config
|
|
mock_config = MagicMock()
|
|
mock_config.default_project = "test-project"
|
|
mock_config_manager.return_value = mock_config
|
|
mock_config_manager.return_value.get_project.return_value = ("test-project", "/tmp/test")
|
|
|
|
# Mock the JSON helper
|
|
mock_entity_data = {"title": "My Note", "permalink": "my-note"}
|
|
mock_read_json.return_value = mock_entity_data
|
|
|
|
# Run with plain title (should be handled by memory_url_path in helper)
|
|
result = runner.invoke(
|
|
cli_app,
|
|
["tool", "read-note", "My Note", "--format", "json"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
output = json.loads(result.output)
|
|
assert output["title"] == "My Note"
|
|
|
|
|
|
class TestRecentActivityJsonFormat:
|
|
"""Tests for recent-activity --format json."""
|
|
|
|
@patch("basic_memory.cli.commands.tool._recent_activity_json")
|
|
@patch("basic_memory.cli.commands.tool.ConfigManager")
|
|
def test_recent_activity_json_format(self, mock_config_manager, mock_activity_json):
|
|
"""Test recent-activity with --format json returns structured data."""
|
|
# Mock config
|
|
mock_config = MagicMock()
|
|
mock_config.default_project = "test-project"
|
|
mock_config_manager.return_value = mock_config
|
|
mock_config_manager.return_value.get_project.return_value = ("test-project", "/tmp/test")
|
|
|
|
# Mock the JSON helper to return activity data
|
|
mock_activity_data = {
|
|
"results": [
|
|
{"primary_result": {"title": "Note 1", "type": "entity"}},
|
|
{"primary_result": {"title": "Note 2", "type": "entity"}},
|
|
],
|
|
"metadata": {"total_results": 2},
|
|
}
|
|
mock_activity_json.return_value = mock_activity_data
|
|
|
|
# Run command
|
|
result = runner.invoke(
|
|
cli_app,
|
|
["tool", "recent-activity", "--format", "json"],
|
|
)
|
|
|
|
# Verify success
|
|
assert result.exit_code == 0
|
|
|
|
# Verify JSON output
|
|
output = json.loads(result.output)
|
|
assert len(output["results"]) == 2
|
|
assert output["metadata"]["total_results"] == 2
|
|
|
|
@patch("basic_memory.cli.commands.tool._recent_activity_json")
|
|
@patch("basic_memory.cli.commands.tool.ConfigManager")
|
|
def test_recent_activity_pagination_options(self, mock_config_manager, mock_activity_json):
|
|
"""Test recent-activity exposes --page and --page-size options."""
|
|
# Mock config
|
|
mock_config = MagicMock()
|
|
mock_config.default_project = "test-project"
|
|
mock_config_manager.return_value = mock_config
|
|
mock_config_manager.return_value.get_project.return_value = ("test-project", "/tmp/test")
|
|
|
|
# Mock the JSON helper
|
|
mock_activity_data = {"results": [], "metadata": {}}
|
|
mock_activity_json.return_value = mock_activity_data
|
|
|
|
# Run command with pagination
|
|
result = runner.invoke(
|
|
cli_app,
|
|
[
|
|
"tool",
|
|
"recent-activity",
|
|
"--format",
|
|
"json",
|
|
"--page",
|
|
"2",
|
|
"--page-size",
|
|
"25",
|
|
],
|
|
)
|
|
|
|
# Verify success
|
|
assert result.exit_code == 0
|
|
|
|
# Verify pagination params were passed to helper
|
|
call_args = mock_activity_json.call_args
|
|
assert call_args.kwargs["page"] == 2
|
|
assert call_args.kwargs["page_size"] == 25
|
|
|
|
@patch("basic_memory.cli.commands.tool._recent_activity_json")
|
|
@patch("basic_memory.cli.commands.tool.ConfigManager")
|
|
def test_recent_activity_project_param(self, mock_config_manager, mock_activity_json):
|
|
"""Test recent-activity --project works with --format json."""
|
|
# Mock config
|
|
mock_config = MagicMock()
|
|
mock_config_manager.return_value = mock_config
|
|
mock_config_manager.return_value.get_project.return_value = ("my-project", "/tmp/my-proj")
|
|
|
|
# Mock the JSON helper
|
|
mock_activity_data = {"results": [], "metadata": {}}
|
|
mock_activity_json.return_value = mock_activity_data
|
|
|
|
# Run command with project
|
|
result = runner.invoke(
|
|
cli_app,
|
|
[
|
|
"tool",
|
|
"recent-activity",
|
|
"--format",
|
|
"json",
|
|
"--project",
|
|
"my-project",
|
|
],
|
|
)
|
|
|
|
# Verify success
|
|
assert result.exit_code == 0
|
|
|
|
# Verify project was resolved
|
|
mock_config_manager.return_value.get_project.assert_called_with("my-project")
|
|
|
|
|
|
class TestBuildContextFormatFlag:
|
|
"""Tests for build-context --format json consistency."""
|
|
|
|
@patch("basic_memory.cli.commands.tool.mcp_build_context")
|
|
@patch("basic_memory.cli.commands.tool.ConfigManager")
|
|
def test_build_context_accepts_format_flag(self, mock_config_manager, mock_build_context):
|
|
"""Test build-context accepts --format json for consistency."""
|
|
# Mock config
|
|
mock_config = MagicMock()
|
|
mock_config.default_project = "test-project"
|
|
mock_config_manager.return_value = mock_config
|
|
mock_config_manager.return_value.get_project.return_value = ("test-project", "/tmp/test")
|
|
|
|
# Mock the MCP tool
|
|
mock_context_result = MagicMock()
|
|
mock_context_result.model_dump.return_value = {
|
|
"primary_result": {"title": "Test"},
|
|
"related_results": [],
|
|
}
|
|
mock_build_context.fn.return_value = mock_context_result
|
|
|
|
# Run command with format flag (build-context always outputs JSON)
|
|
result = runner.invoke(
|
|
cli_app,
|
|
[
|
|
"tool",
|
|
"build-context",
|
|
"memory://test",
|
|
"--format",
|
|
"json",
|
|
],
|
|
)
|
|
|
|
# Verify command accepts the flag without error
|
|
assert result.exit_code == 0
|
|
|
|
# Verify output is valid JSON
|
|
output = json.loads(result.output)
|
|
assert "primary_result" in output
|