Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/basic_memory/cli/commands/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,23 @@ def _fetch_cloud_workspace_results() -> tuple[
generate_permalink(project_name): project_name for project_name in config.projects
}

# Trigger: a project in config.projects was surfaced by neither query — the
# cloud branch is skipped without credentials, and a cloud-mode project is
# not returned by the local query.
# Why: without a fallback such a project is invisible in `bm project list`,
# yet `bm project add` reads the DB and reports it already exists (#1003).
# The two commands must agree on whether a configured project exists.
# Outcome: seed a local-keyed row from config so the project still renders;
# the row-building logic below derives its display from the config entry.
# Constraint: only fill from config for the local-inclusive view. A pure
# --cloud listing (no local_result) or a --workspace-filtered view is
# deliberately scoped, so configured local projects must not leak into it.
if local_result is not None and not workspace_filter_requested:
seeded_permalinks = {permalink for _, permalink in row_names_by_key}
for permalink, project_name in configured_names_by_permalink.items():
if permalink not in seeded_permalinks:
row_names_by_key[(None, permalink)] = project_name

def _workspace_priority(row_key: tuple[str | None, str]) -> tuple[bool, int, str, str]:
"""Prefer the user's default/personal workspace when a project is duplicated."""
workspace = cloud_workspaces_by_key.get(row_key)
Expand Down
42 changes: 42 additions & 0 deletions tests/cli/test_project_list_and_ls.py
Original file line number Diff line number Diff line change
Expand Up @@ -1066,3 +1066,45 @@ async def fake_get_mount_info():
assert result.exit_code == 0, f"Exit code: {result.exit_code}, output: {result.stdout}"
assert "Files in alpha (CLOUD)" in result.stdout
assert "cloud.md" in result.stdout


def test_project_list_shows_configured_project_without_cloud_credentials(
runner: CliRunner, write_config, mock_client, tmp_path, monkeypatch
):
"""A cloud-mode project in config must render even with no cloud credentials (#1003).

Regression: ``bm project list`` seeded rows only from live query results, so a
cloud-mode project was skipped by both the credential-gated cloud branch and the
local query — the table came up empty while ``bm project add`` reported the same
project already existed. The two commands must agree that the project exists.
"""
write_config(
{
"env": "dev",
"projects": {
"main": {
"path": "",
"mode": "cloud",
"workspace_id": None,
"local_sync_path": None,
}
},
"default_project": "main",
}
)

# No credentials on this machine: the cloud branch is skipped entirely.
monkeypatch.setattr(project_cmd, "_has_cloud_credentials", lambda config: False)

# The local query does not surface a cloud-mode project, so it returns empty.
async def fake_list_projects(self):
return ProjectList.model_validate({"projects": [], "default_project": "main"})

monkeypatch.setattr(ProjectClient, "list_projects", fake_list_projects)

result = runner.invoke(app, ["project", "list"], env={"COLUMNS": "240"})

assert result.exit_code == 0, f"Exit code: {result.exit_code}, output: {result.stdout}"
# The configured project is now visible instead of an empty table.
main_line = next(line for line in result.stdout.splitlines() if "│ main" in line)
assert "cloud" in main_line # cloud-mode projects route to the cloud CLI
Loading