-
Notifications
You must be signed in to change notification settings - Fork 188
Expand file tree
/
Copy pathtest_project_set_cloud_local.py
More file actions
289 lines (229 loc) · 11 KB
/
test_project_set_cloud_local.py
File metadata and controls
289 lines (229 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
"""Tests for bm project set-cloud and bm project set-local commands."""
import json
import pytest
from typer.testing import CliRunner
from basic_memory.cli.app import app
# Importing the commands module registers the project subcommands with the app
import basic_memory.cli.commands.project # noqa: F401
@pytest.fixture
def runner():
return CliRunner()
@pytest.fixture
def mock_config(tmp_path, monkeypatch):
"""Create a mock config with projects for testing set-cloud/set-local."""
from basic_memory import config as config_module
config_module._CONFIG_CACHE = None
config_module._CONFIG_MTIME = None
config_module._CONFIG_SIZE = None
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": {
"main": {"path": str(tmp_path / "main")},
"research": {"path": str(tmp_path / "research")},
},
"default_project": "main",
"cloud_api_key": "bmc_test_key_123",
}
config_file.write_text(json.dumps(config_data, indent=2))
monkeypatch.setenv("HOME", str(tmp_path))
yield config_file
class TestSetCloud:
"""Tests for bm project set-cloud command."""
def test_set_cloud_success(self, runner, mock_config):
"""Test setting a project to cloud mode."""
result = runner.invoke(app, ["project", "set-cloud", "research"])
assert result.exit_code == 0
assert "cloud mode" in result.stdout.lower()
# Verify config was updated
config_data = json.loads(mock_config.read_text())
assert config_data["projects"]["research"]["mode"] == "cloud"
def test_set_cloud_nonexistent_project(self, runner, mock_config):
"""Test set-cloud with a project that doesn't exist in config."""
result = runner.invoke(app, ["project", "set-cloud", "nonexistent"])
assert result.exit_code == 1
assert "not found" in result.stdout.lower()
def test_set_cloud_no_credentials(self, runner, tmp_path, monkeypatch):
"""Test set-cloud when neither API key nor OAuth session is available."""
from basic_memory import config as config_module
config_module._CONFIG_CACHE = None
config_module._CONFIG_MTIME = None
config_module._CONFIG_SIZE = None
config_dir = tmp_path / ".basic-memory"
config_dir.mkdir(parents=True, exist_ok=True)
config_file = config_dir / "config.json"
# Config without cloud_api_key
config_data = {
"env": "dev",
"projects": {"research": {"path": str(tmp_path / "research")}},
"default_project": "research",
}
config_file.write_text(json.dumps(config_data, indent=2))
monkeypatch.setenv("HOME", str(tmp_path))
result = runner.invoke(app, ["project", "set-cloud", "research"])
assert result.exit_code == 1
assert "no cloud credentials" in result.stdout.lower()
def test_set_cloud_with_oauth_session(self, runner, tmp_path, monkeypatch):
"""Test set-cloud succeeds with OAuth token but no API key."""
from basic_memory import config as config_module
config_module._CONFIG_CACHE = None
config_module._CONFIG_MTIME = None
config_module._CONFIG_SIZE = None
config_dir = tmp_path / ".basic-memory"
config_dir.mkdir(parents=True, exist_ok=True)
config_file = config_dir / "config.json"
# Config without cloud_api_key but with a project
config_data = {
"env": "dev",
"projects": {"research": {"path": str(tmp_path / "research")}},
"default_project": "research",
}
config_file.write_text(json.dumps(config_data, indent=2))
monkeypatch.setenv("HOME", str(tmp_path))
# Write OAuth token file so CLIAuth.load_tokens() returns something
token_file = config_dir / "basic-memory-cloud.json"
token_data = {
"access_token": "oauth-token-789",
"refresh_token": None,
"expires_at": 9999999999,
"token_type": "Bearer",
}
token_file.write_text(json.dumps(token_data, indent=2))
result = runner.invoke(app, ["project", "set-cloud", "research"])
assert result.exit_code == 0
assert "cloud mode" in result.stdout.lower()
# Verify config was updated
config_data = json.loads(config_file.read_text())
assert config_data["projects"]["research"]["mode"] == "cloud"
class TestSetLocal:
"""Tests for bm project set-local command."""
def test_set_local_success(self, runner, mock_config):
"""Test reverting a project to local mode."""
# First set to cloud
runner.invoke(app, ["project", "set-cloud", "research"])
config_data = json.loads(mock_config.read_text())
assert config_data["projects"]["research"]["mode"] == "cloud"
# Now set back to local
result = runner.invoke(app, ["project", "set-local", "research"])
assert result.exit_code == 0
assert "local mode" in result.stdout.lower()
# Verify config was updated — mode reset to local
config_data = json.loads(mock_config.read_text())
assert config_data["projects"]["research"]["mode"] == "local"
def test_set_local_nonexistent_project(self, runner, mock_config):
"""Test set-local with a project that doesn't exist in config."""
result = runner.invoke(app, ["project", "set-local", "nonexistent"])
assert result.exit_code == 1
assert "not found" in result.stdout.lower()
def test_set_local_already_local(self, runner, mock_config):
"""Test set-local on a project that's already local (no-op, should succeed)."""
result = runner.invoke(app, ["project", "set-local", "main"])
assert result.exit_code == 0
assert "local mode" in result.stdout.lower()
def test_set_local_clears_workspace_id(self, runner, mock_config):
"""Test that set-local clears workspace_id from the project entry."""
from basic_memory import config as config_module
# Manually set workspace_id on the project
config_module._CONFIG_CACHE = None
config_module._CONFIG_MTIME = None
config_module._CONFIG_SIZE = None
config_data = json.loads(mock_config.read_text())
config_data["projects"]["research"]["mode"] = "cloud"
config_data["projects"]["research"]["workspace_id"] = "11111111-1111-1111-1111-111111111111"
mock_config.write_text(json.dumps(config_data, indent=2))
config_module._CONFIG_CACHE = None
config_module._CONFIG_MTIME = None
config_module._CONFIG_SIZE = None
# Set back to local
result = runner.invoke(app, ["project", "set-local", "research"])
assert result.exit_code == 0
# Verify workspace_id was cleared
config_module._CONFIG_CACHE = None
config_module._CONFIG_MTIME = None
config_module._CONFIG_SIZE = None
updated_data = json.loads(mock_config.read_text())
assert updated_data["projects"]["research"]["workspace_id"] is None
assert updated_data["projects"]["research"]["mode"] == "local"
class TestSetCloudWithWorkspace:
"""Tests for 'bm project set-cloud --workspace' option."""
def test_set_cloud_with_workspace_stores_workspace_id(self, runner, mock_config, monkeypatch):
"""Test that --workspace resolves to tenant_id and stores it."""
from basic_memory import config as config_module
from basic_memory.schemas.cloud import WorkspaceInfo
config_module._CONFIG_CACHE = None
config_module._CONFIG_MTIME = None
config_module._CONFIG_SIZE = None
async def fake_get_available_workspaces():
return [
WorkspaceInfo(
tenant_id="11111111-1111-1111-1111-111111111111",
workspace_type="personal",
name="Personal",
role="owner",
),
]
monkeypatch.setattr(
"basic_memory.mcp.project_context.get_available_workspaces",
fake_get_available_workspaces,
)
result = runner.invoke(app, ["project", "set-cloud", "research", "--workspace", "Personal"])
assert result.exit_code == 0
assert "cloud mode" in result.stdout.lower()
assert "11111111-1111-1111-1111-111111111111" in result.stdout
# Verify workspace_id was persisted
config_module._CONFIG_CACHE = None
config_module._CONFIG_MTIME = None
config_module._CONFIG_SIZE = None
updated_data = json.loads(mock_config.read_text())
assert (
updated_data["projects"]["research"]["workspace_id"]
== "11111111-1111-1111-1111-111111111111"
)
def test_set_cloud_with_workspace_not_found(self, runner, mock_config, monkeypatch):
"""Test --workspace with unknown workspace name."""
from basic_memory import config as config_module
from basic_memory.schemas.cloud import WorkspaceInfo
config_module._CONFIG_CACHE = None
config_module._CONFIG_MTIME = None
config_module._CONFIG_SIZE = None
async def fake_get_available_workspaces():
return [
WorkspaceInfo(
tenant_id="11111111-1111-1111-1111-111111111111",
workspace_type="personal",
name="Personal",
role="owner",
),
]
monkeypatch.setattr(
"basic_memory.mcp.project_context.get_available_workspaces",
fake_get_available_workspaces,
)
result = runner.invoke(
app, ["project", "set-cloud", "research", "--workspace", "Nonexistent"]
)
assert result.exit_code == 1
assert "not found" in result.stdout.lower()
def test_set_cloud_uses_default_workspace_when_no_flag(self, runner, mock_config, monkeypatch):
"""Test that set-cloud uses default_workspace when --workspace is not passed."""
from basic_memory import config as config_module
config_module._CONFIG_CACHE = None
config_module._CONFIG_MTIME = None
config_module._CONFIG_SIZE = None
# Set default_workspace in config
config_data = json.loads(mock_config.read_text())
config_data["default_workspace"] = "global-default-tenant-id"
mock_config.write_text(json.dumps(config_data, indent=2))
config_module._CONFIG_CACHE = None
config_module._CONFIG_MTIME = None
config_module._CONFIG_SIZE = None
result = runner.invoke(app, ["project", "set-cloud", "research"])
assert result.exit_code == 0
# Verify workspace_id was set from default
config_module._CONFIG_CACHE = None
config_module._CONFIG_MTIME = None
config_module._CONFIG_SIZE = None
updated_data = json.loads(mock_config.read_text())
assert updated_data["projects"]["research"]["workspace_id"] == "global-default-tenant-id"