fix: unify project path so path is always the local filesystem path

Cloud projects with bisync had a split-brain problem: `path` held a cloud
slug while the actual local directory lived in `local_sync_path`. This caused
`bm status` and file sync to fail for bisync'd cloud projects.

Changes:
- Config migration promotes `local_sync_path` → `path` for entries where
  `path` is a non-absolute cloud slug
- `ensure_project_paths_exists` skips cloud-only projects with slug paths
- `initialize_file_sync` and watch service now keep cloud projects that have
  an absolute local path (bisync copy) instead of skipping all cloud projects
- `sync-setup` and `project add --cloud --local-path` set both `path` and
  `local_sync_path` to the local directory
- `sync-setup` creates the project in the local DB for immediate MCP use
- `_get_sync_project` falls back from `local_sync_path` to `path`
- Config load errors now show user-friendly messages instead of stack traces

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
phernandez
2026-02-24 15:08:09 -06:00
parent 538af97cba
commit d763d86798
7 changed files with 195 additions and 38 deletions
+45 -8
View File
@@ -144,23 +144,19 @@ async def test_run_reloads_projects_each_cycle(monkeypatch, tmp_path):
@pytest.mark.asyncio
async def test_run_filters_cloud_projects_each_cycle(monkeypatch, tmp_path):
async def test_run_filters_cloud_only_projects_each_cycle(monkeypatch, tmp_path):
"""Cloud-only projects (slug path, no local directory) are filtered out."""
config = BasicMemoryConfig(
watch_project_reload_interval=1,
projects={
"local-project": {"path": str(tmp_path / "local"), "mode": "local"},
"cloud-project": {"path": str(tmp_path / "cloud"), "mode": "cloud"},
"cloud-only": {"path": "cloud-slug", "mode": "cloud"},
},
)
repo = _Repo(
projects_return=[
Project(id=1, name="local-project", path=str(tmp_path / "local"), permalink="local"),
Project(
id=2,
name="cloud-project",
path=str(tmp_path / "cloud"),
permalink="cloud",
),
Project(id=2, name="cloud-only", path="cloud-slug", permalink="cloud-only"),
]
)
watch_service = WatchService(config, repo, quiet=True)
@@ -183,6 +179,47 @@ async def test_run_filters_cloud_projects_each_cycle(monkeypatch, tmp_path):
assert seen_project_names == [["local-project"]]
@pytest.mark.asyncio
async def test_run_keeps_cloud_projects_with_local_bisync(monkeypatch, tmp_path):
"""Cloud projects with an absolute path (local bisync copy) are kept for watching."""
config = BasicMemoryConfig(
watch_project_reload_interval=1,
projects={
"local-project": {"path": str(tmp_path / "local"), "mode": "local"},
"cloud-bisync": {"path": str(tmp_path / "cloud"), "mode": "cloud"},
},
)
repo = _Repo(
projects_return=[
Project(id=1, name="local-project", path=str(tmp_path / "local"), permalink="local"),
Project(
id=2,
name="cloud-bisync",
path=str(tmp_path / "cloud"),
permalink="cloud-bisync",
),
]
)
watch_service = WatchService(config, repo, quiet=True)
seen_project_names: list[list[str]] = []
async def watch_cycle_stub(projects, stop_event):
seen_project_names.append([p.name for p in projects])
watch_service.state.running = False
stop_event.set()
async def fake_write_status():
return None
monkeypatch.setattr(watch_service, "_watch_projects_cycle", watch_cycle_stub)
monkeypatch.setattr(watch_service, "write_status", fake_write_status)
await watch_service.run()
assert seen_project_names == [["local-project", "cloud-bisync"]]
@pytest.mark.asyncio
async def test_run_continues_after_cycle_error(monkeypatch, tmp_path):
config = BasicMemoryConfig()