Files
basicmachines-co-basic-memory/tests/cli/test_cli_json_format.py
T
claude[bot] 2c4d923ec1 feat: add --format json support to CLI tool commands
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>
2026-02-08 16:14:01 +00:00

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