mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
feat: SPEC-20 enhancements - cleanup, path normalization, and docs
This commit adds several critical improvements discovered during
manual testing of SPEC-20 project-scoped rclone sync:
**Critical Bug Fixes:**
1. Path Normalization (fixes path doubling bug)
- API: Strip /app/data/ prefix in project_router.py
- CLI: Defensive normalization in project.py
- Rclone: Fix get_project_remote() path construction
- Prevents files syncing to /app/data/app/data/project/
2. Rclone Flag Fix
- Changed --filters-file to correct --filter-from flag
- Fixes "unknown flag" error in sync and bisync
**Enhancements:**
3. Automatic Database Sync
- POST to /{project}/project/sync after file operations
- Keeps database in sync with files automatically
- Skipped on --dry-run operations
4. Enhanced Project Removal
- Clean up local sync directory (with --delete-notes)
- Always remove bisync state directory
- Always remove cloud_projects config entry
- Informative messages about what was/wasn't deleted
5. Bisync State Reset Command
- New: bm project bisync-reset <project>
- Clears corrupted bisync metadata
- Safe recovery tool for bisync issues
6. Improved Project List UI
- Show Local Path column in cloud mode
- Conditionally show/hide columns based on config
- Prevent path truncation with no_wrap/overflow
- Apply path normalization to display
**Documentation:**
7. Cloud CLI Documentation
- Add troubleshooting: empty directory bisync issues
- Add troubleshooting: bisync state corruption
- Document bisync-reset command usage
- Explain rclone bisync limitations
8. SPEC-20 Updates
- Mark implementation complete
- Document all enhancements in Implementation Notes
- Update phase checklists with completed work
- Add manual testing results
**Tests:**
9. Unit Tests for --local-path
- Test config persistence with --local-path
- Test no config without --local-path
- Test tilde expansion
- Test nested directory creation
All changes tested manually end-to-end.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
"""Tests for bm project add with --local-path flag."""
|
||||
|
||||
import json
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from basic_memory.cli.app import app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner():
|
||||
return CliRunner()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config(tmp_path, monkeypatch):
|
||||
"""Create a mock config in cloud mode using environment variables."""
|
||||
config_dir = tmp_path / ".basic-memory"
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
config_file = config_dir / "config.json"
|
||||
|
||||
config_data = {
|
||||
"env": "dev",
|
||||
"projects": {},
|
||||
"default_project": "main",
|
||||
"cloud_mode": True,
|
||||
"cloud_projects": {},
|
||||
}
|
||||
|
||||
config_file.write_text(json.dumps(config_data, indent=2))
|
||||
|
||||
# Set HOME to tmp_path so ConfigManager uses our test config
|
||||
monkeypatch.setenv("HOME", str(tmp_path))
|
||||
|
||||
yield config_file
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_api_client():
|
||||
"""Mock the API client for project add."""
|
||||
with patch("basic_memory.cli.commands.project.get_client"):
|
||||
# Mock call_post to return a proper response
|
||||
mock_response = AsyncMock()
|
||||
mock_response.json = lambda: {
|
||||
"message": "Project 'test-project' added successfully",
|
||||
"status": "success",
|
||||
"default": False,
|
||||
"old_project": None,
|
||||
"new_project": {
|
||||
"name": "test-project",
|
||||
"path": "/test-project",
|
||||
"is_default": False,
|
||||
},
|
||||
}
|
||||
|
||||
with patch(
|
||||
"basic_memory.cli.commands.project.call_post", return_value=mock_response
|
||||
) as mock_post:
|
||||
yield mock_post
|
||||
|
||||
|
||||
def test_project_add_with_local_path_saves_to_config(
|
||||
runner, mock_config, mock_api_client, tmp_path
|
||||
):
|
||||
"""Test that bm project add --local-path saves sync path to config."""
|
||||
local_sync_dir = tmp_path / "sync" / "test-project"
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
[
|
||||
"project",
|
||||
"add",
|
||||
"test-project",
|
||||
"--local-path",
|
||||
str(local_sync_dir),
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, f"Exit code: {result.exit_code}, Stdout: {result.stdout}"
|
||||
assert "Project 'test-project' added successfully" in result.stdout
|
||||
assert "Local sync path configured" in result.stdout
|
||||
# Check path is present (may be line-wrapped in output)
|
||||
assert "test-project" in result.stdout
|
||||
assert "sync" in result.stdout
|
||||
|
||||
# Verify config was updated
|
||||
config_data = json.loads(mock_config.read_text())
|
||||
assert "test-project" in config_data["cloud_projects"]
|
||||
assert config_data["cloud_projects"]["test-project"]["local_path"] == str(local_sync_dir)
|
||||
assert config_data["cloud_projects"]["test-project"]["last_sync"] is None
|
||||
assert config_data["cloud_projects"]["test-project"]["bisync_initialized"] is False
|
||||
|
||||
# Verify local directory was created
|
||||
assert local_sync_dir.exists()
|
||||
assert local_sync_dir.is_dir()
|
||||
|
||||
|
||||
def test_project_add_without_local_path_no_config_entry(runner, mock_config, mock_api_client):
|
||||
"""Test that bm project add without --local-path doesn't save to config."""
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["project", "add", "test-project"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Project 'test-project' added successfully" in result.stdout
|
||||
assert "Local sync path configured" not in result.stdout
|
||||
|
||||
# Verify config was NOT updated with cloud_projects entry
|
||||
config_data = json.loads(mock_config.read_text())
|
||||
assert "test-project" not in config_data.get("cloud_projects", {})
|
||||
|
||||
|
||||
def test_project_add_local_path_expands_tilde(runner, mock_config, mock_api_client):
|
||||
"""Test that --local-path ~/path expands to absolute path."""
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["project", "add", "test-project", "--local-path", "~/test-sync"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify config has expanded path
|
||||
config_data = json.loads(mock_config.read_text())
|
||||
local_path = config_data["cloud_projects"]["test-project"]["local_path"]
|
||||
assert local_path.startswith("/")
|
||||
assert "~" not in local_path
|
||||
assert local_path.endswith("/test-sync")
|
||||
|
||||
|
||||
def test_project_add_local_path_creates_nested_directories(
|
||||
runner, mock_config, mock_api_client, tmp_path
|
||||
):
|
||||
"""Test that --local-path creates nested directories."""
|
||||
nested_path = tmp_path / "a" / "b" / "c" / "test-project"
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["project", "add", "test-project", "--local-path", str(nested_path)],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert nested_path.exists()
|
||||
assert nested_path.is_dir()
|
||||
Reference in New Issue
Block a user