mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
415c2b3d6e
Signed-off-by: phernandez <paul@basicmachines.co>
151 lines
5.9 KiB
Python
151 lines
5.9 KiB
Python
"""Tests for the 'basic-memory orphans' CLI command."""
|
|
|
|
import json
|
|
from contextlib import asynccontextmanager, nullcontext
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
from mcp.server.fastmcp.exceptions import ToolError
|
|
from typer.testing import CliRunner
|
|
|
|
from basic_memory.cli.main import app as cli_app
|
|
from basic_memory.schemas.v2.graph import GraphNode
|
|
|
|
import basic_memory.cli.commands.orphans as orphans_cmd # noqa: F401
|
|
|
|
runner = CliRunner()
|
|
|
|
_MOCK_PROJECT_ITEM = MagicMock()
|
|
_MOCK_PROJECT_ITEM.name = "test-project"
|
|
_MOCK_PROJECT_ITEM.external_id = "11111111-1111-1111-1111-111111111111"
|
|
|
|
_ORPHAN_ENTITIES = [
|
|
GraphNode(
|
|
external_id="aaaa-1111",
|
|
title="Isolated Note",
|
|
file_path="notes/isolated.md",
|
|
note_type="note",
|
|
),
|
|
GraphNode(
|
|
external_id="bbbb-2222",
|
|
title="Dangling Spec",
|
|
file_path="specs/dangling.md",
|
|
note_type="spec",
|
|
),
|
|
]
|
|
|
|
|
|
def _mock_config_manager():
|
|
mock_config = MagicMock()
|
|
mock_config.default_project = "test-project"
|
|
return mock_config
|
|
|
|
|
|
@asynccontextmanager
|
|
async def _fake_get_client(project_name=None):
|
|
yield MagicMock()
|
|
|
|
|
|
@patch("basic_memory.cli.commands.orphans.run_orphans", new_callable=AsyncMock)
|
|
@patch("basic_memory.cli.commands.orphans.force_routing")
|
|
def test_orphans_preserves_project_routing_by_default(mock_force_routing, mock_run_orphans):
|
|
"""Default invocation keeps routing implicit so project mode can choose local/cloud."""
|
|
mock_force_routing.return_value = nullcontext()
|
|
mock_run_orphans.return_value = ("test-project", [])
|
|
|
|
result = runner.invoke(cli_app, ["orphans"])
|
|
|
|
assert result.exit_code == 0, f"CLI failed: {result.output}"
|
|
mock_force_routing.assert_called_once_with(local=False, cloud=False)
|
|
|
|
|
|
@patch("basic_memory.cli.commands.orphans.ConfigManager")
|
|
@patch("basic_memory.cli.commands.orphans.get_active_project", new_callable=AsyncMock)
|
|
@patch("basic_memory.cli.commands.orphans.get_client")
|
|
@patch("basic_memory.cli.commands.orphans.KnowledgeClient")
|
|
def test_orphans_json_output(mock_knowledge_cls, mock_get_client, mock_get_active, mock_config_cls):
|
|
"""basic-memory orphans --json outputs a JSON array of orphan entity objects."""
|
|
mock_config_cls.return_value = _mock_config_manager()
|
|
mock_get_active.return_value = _MOCK_PROJECT_ITEM
|
|
mock_get_client.side_effect = _fake_get_client
|
|
mock_knowledge = AsyncMock()
|
|
mock_knowledge.get_orphans.return_value = _ORPHAN_ENTITIES
|
|
mock_knowledge_cls.return_value = mock_knowledge
|
|
|
|
result = runner.invoke(cli_app, ["orphans", "--json"])
|
|
|
|
assert result.exit_code == 0, f"CLI failed: {result.output}"
|
|
json_start = result.output.rfind("[\n")
|
|
data = json.loads(result.output[json_start:])
|
|
titles = {entity["title"] for entity in data}
|
|
assert titles == {"Isolated Note", "Dangling Spec"}
|
|
mock_get_client.assert_called_once_with(project_name="test-project")
|
|
|
|
|
|
@patch("basic_memory.cli.commands.orphans.ConfigManager")
|
|
@patch("basic_memory.cli.commands.orphans.get_active_project", new_callable=AsyncMock)
|
|
@patch("basic_memory.cli.commands.orphans.get_client")
|
|
@patch("basic_memory.cli.commands.orphans.KnowledgeClient")
|
|
def test_orphans_table_output(
|
|
mock_knowledge_cls, mock_get_client, mock_get_active, mock_config_cls
|
|
):
|
|
"""basic-memory orphans renders a table with orphan titles and paths."""
|
|
mock_config_cls.return_value = _mock_config_manager()
|
|
mock_get_active.return_value = _MOCK_PROJECT_ITEM
|
|
mock_get_client.side_effect = _fake_get_client
|
|
mock_knowledge = AsyncMock()
|
|
mock_knowledge.get_orphans.return_value = _ORPHAN_ENTITIES
|
|
mock_knowledge_cls.return_value = mock_knowledge
|
|
|
|
result = runner.invoke(cli_app, ["orphans"])
|
|
|
|
assert result.exit_code == 0, f"CLI failed: {result.output}"
|
|
assert "Isolated Note" in result.output
|
|
assert "Dangling Spec" in result.output
|
|
assert "notes/isolated.md" in result.output
|
|
|
|
|
|
@patch("basic_memory.cli.commands.orphans.ConfigManager")
|
|
@patch("basic_memory.cli.commands.orphans.get_active_project", new_callable=AsyncMock)
|
|
@patch("basic_memory.cli.commands.orphans.get_client")
|
|
@patch("basic_memory.cli.commands.orphans.KnowledgeClient")
|
|
def test_orphans_no_results(mock_knowledge_cls, mock_get_client, mock_get_active, mock_config_cls):
|
|
"""basic-memory orphans prints a success message when no orphans are found."""
|
|
mock_config_cls.return_value = _mock_config_manager()
|
|
mock_get_active.return_value = _MOCK_PROJECT_ITEM
|
|
mock_get_client.side_effect = _fake_get_client
|
|
mock_knowledge = AsyncMock()
|
|
mock_knowledge.get_orphans.return_value = []
|
|
mock_knowledge_cls.return_value = mock_knowledge
|
|
|
|
result = runner.invoke(cli_app, ["orphans"])
|
|
|
|
assert result.exit_code == 0, f"CLI failed: {result.output}"
|
|
assert "No orphan entities" in result.output
|
|
|
|
|
|
@patch("basic_memory.cli.commands.orphans.run_orphans", new_callable=AsyncMock)
|
|
@patch("basic_memory.cli.commands.orphans.force_routing")
|
|
def test_orphans_value_error(mock_force_routing, mock_run_orphans):
|
|
"""User-facing command errors are printed and exit with failure."""
|
|
mock_force_routing.return_value = nullcontext()
|
|
mock_run_orphans.side_effect = ValueError("project not found")
|
|
|
|
result = runner.invoke(cli_app, ["orphans"])
|
|
|
|
assert result.exit_code == 1
|
|
assert "Error: project not found" in result.output
|
|
|
|
|
|
@patch("basic_memory.cli.commands.orphans.run_orphans", new_callable=AsyncMock)
|
|
@patch("basic_memory.cli.commands.orphans.force_routing")
|
|
def test_orphans_tool_error_json_output(mock_force_routing, mock_run_orphans):
|
|
"""User-facing command errors are JSON formatted when requested."""
|
|
mock_force_routing.return_value = nullcontext()
|
|
mock_run_orphans.side_effect = ToolError("cloud request failed")
|
|
|
|
result = runner.invoke(cli_app, ["orphans", "--json"])
|
|
|
|
assert result.exit_code == 1
|
|
json_start = result.output.rfind("{\n")
|
|
assert json.loads(result.output[json_start:]) == {"error": "cloud request failed"}
|