|
10 | 10 | ConfigManager, |
11 | 11 | ProjectEntry, |
12 | 12 | ProjectMode, |
| 13 | + default_fastembed_cache_dir, |
| 14 | + resolve_data_dir, |
13 | 15 | ) |
14 | 16 | from pathlib import Path |
15 | 17 |
|
@@ -129,6 +131,54 @@ def test_app_database_path_defaults_to_home_data_dir(self, config_home, monkeypa |
129 | 131 | assert config.data_dir_path == config_home / ".basic-memory" |
130 | 132 | assert config.app_database_path == config_home / ".basic-memory" / "memory.db" |
131 | 133 |
|
| 134 | + def test_semantic_embedding_cache_dir_field_stays_none_by_default( |
| 135 | + self, config_home, monkeypatch |
| 136 | + ): |
| 137 | + """The raw config field stays None so it isn't persisted into config.json. |
| 138 | +
|
| 139 | + Resolution to a concrete path happens in embedding_provider_factory at |
| 140 | + provider construction time, so ``BASIC_MEMORY_CONFIG_DIR`` and |
| 141 | + ``FASTEMBED_CACHE_PATH`` changes take effect on every run instead of |
| 142 | + being frozen by the first save. See #741. |
| 143 | + """ |
| 144 | + monkeypatch.delenv("BASIC_MEMORY_CONFIG_DIR", raising=False) |
| 145 | + monkeypatch.delenv("FASTEMBED_CACHE_PATH", raising=False) |
| 146 | + |
| 147 | + config = BasicMemoryConfig() |
| 148 | + |
| 149 | + assert config.semantic_embedding_cache_dir is None |
| 150 | + |
| 151 | + def test_semantic_embedding_cache_dir_not_persisted_in_model_dump( |
| 152 | + self, config_home, monkeypatch |
| 153 | + ): |
| 154 | + """model_dump must not bake a resolved cache path into config.json. |
| 155 | +
|
| 156 | + Regression guard for #741: persisting the default would freeze stale |
| 157 | + paths when users later change BASIC_MEMORY_CONFIG_DIR or |
| 158 | + FASTEMBED_CACHE_PATH. |
| 159 | + """ |
| 160 | + monkeypatch.delenv("BASIC_MEMORY_CONFIG_DIR", raising=False) |
| 161 | + monkeypatch.delenv("FASTEMBED_CACHE_PATH", raising=False) |
| 162 | + |
| 163 | + dumped = BasicMemoryConfig().model_dump(mode="json") |
| 164 | + |
| 165 | + assert dumped["semantic_embedding_cache_dir"] is None |
| 166 | + |
| 167 | + def test_semantic_embedding_cache_dir_explicit_user_value_preserved( |
| 168 | + self, config_home, monkeypatch |
| 169 | + ): |
| 170 | + """An explicit user override still round-trips through model_dump.""" |
| 171 | + monkeypatch.delenv("BASIC_MEMORY_CONFIG_DIR", raising=False) |
| 172 | + monkeypatch.delenv("FASTEMBED_CACHE_PATH", raising=False) |
| 173 | + |
| 174 | + config = BasicMemoryConfig(semantic_embedding_cache_dir="/custom/explicit/path") |
| 175 | + |
| 176 | + assert config.semantic_embedding_cache_dir == "/custom/explicit/path" |
| 177 | + assert ( |
| 178 | + config.model_dump(mode="json")["semantic_embedding_cache_dir"] |
| 179 | + == "/custom/explicit/path" |
| 180 | + ) |
| 181 | + |
132 | 182 | def test_explicit_default_project_preserved(self, config_home, monkeypatch): |
133 | 183 | """Test that a valid explicit default_project is not overwritten by model_post_init.""" |
134 | 184 | monkeypatch.delenv("BASIC_MEMORY_HOME", raising=False) |
@@ -252,6 +302,65 @@ def test_config_file_without_default_project_key(self, config_home, monkeypatch) |
252 | 302 | assert loaded.default_project == "work" |
253 | 303 |
|
254 | 304 |
|
| 305 | +class TestDataDirHelpers: |
| 306 | + """Module-level helpers that resolve the Basic Memory data directory.""" |
| 307 | + |
| 308 | + def test_resolve_data_dir_defaults_to_home_dot_basic_memory(self, config_home, monkeypatch): |
| 309 | + """Without BASIC_MEMORY_CONFIG_DIR, resolver returns ~/.basic-memory.""" |
| 310 | + monkeypatch.delenv("BASIC_MEMORY_CONFIG_DIR", raising=False) |
| 311 | + |
| 312 | + assert resolve_data_dir() == config_home / ".basic-memory" |
| 313 | + |
| 314 | + def test_resolve_data_dir_honors_config_dir_env(self, tmp_path, monkeypatch): |
| 315 | + """BASIC_MEMORY_CONFIG_DIR overrides the default location.""" |
| 316 | + custom = tmp_path / "elsewhere" |
| 317 | + monkeypatch.setenv("BASIC_MEMORY_CONFIG_DIR", str(custom)) |
| 318 | + |
| 319 | + assert resolve_data_dir() == custom |
| 320 | + |
| 321 | + def test_default_fastembed_cache_dir_uses_data_dir(self, config_home, monkeypatch): |
| 322 | + """Default cache path is a subdir of the Basic Memory data dir.""" |
| 323 | + monkeypatch.delenv("BASIC_MEMORY_CONFIG_DIR", raising=False) |
| 324 | + monkeypatch.delenv("FASTEMBED_CACHE_PATH", raising=False) |
| 325 | + |
| 326 | + assert default_fastembed_cache_dir() == str( |
| 327 | + config_home / ".basic-memory" / "fastembed_cache" |
| 328 | + ) |
| 329 | + |
| 330 | + def test_default_fastembed_cache_dir_env_override(self, tmp_path, monkeypatch): |
| 331 | + """FASTEMBED_CACHE_PATH is preferred when set.""" |
| 332 | + custom = tmp_path / "custom-cache" |
| 333 | + monkeypatch.setenv("FASTEMBED_CACHE_PATH", str(custom)) |
| 334 | + monkeypatch.setenv("BASIC_MEMORY_CONFIG_DIR", str(tmp_path / "state")) |
| 335 | + |
| 336 | + assert default_fastembed_cache_dir() == str(custom) |
| 337 | + |
| 338 | + def test_default_fastembed_cache_dir_never_falls_back_to_fastembed_tmp_default( |
| 339 | + self, config_home, monkeypatch |
| 340 | + ): |
| 341 | + """Regression guard for #741. |
| 342 | +
|
| 343 | + FastEmbed's own fallback when ``cache_dir`` is ``None`` is |
| 344 | + ``<system tmp>/fastembed_cache`` — the exact path that disappears in |
| 345 | + sandboxed MCP runtimes (Codex CLI). Ensure Basic Memory's resolver |
| 346 | + never lands on that path. |
| 347 | +
|
| 348 | + Compared as exact paths rather than ``startswith(tempfile.gettempdir())`` |
| 349 | + because the test runner itself can legitimately live under ``/tmp`` |
| 350 | + (pytest's ``tmp_path`` does on Linux CI), and that's not the bug we |
| 351 | + care about here. |
| 352 | + """ |
| 353 | + monkeypatch.delenv("BASIC_MEMORY_CONFIG_DIR", raising=False) |
| 354 | + monkeypatch.delenv("FASTEMBED_CACHE_PATH", raising=False) |
| 355 | + |
| 356 | + resolved = Path(default_fastembed_cache_dir()) |
| 357 | + |
| 358 | + # Must equal the data-dir-relative default. |
| 359 | + assert resolved == config_home / ".basic-memory" / "fastembed_cache" |
| 360 | + # And must not equal FastEmbed's own <tempdir>/fastembed_cache fallback. |
| 361 | + assert resolved != Path(tempfile.gettempdir()) / "fastembed_cache" |
| 362 | + |
| 363 | + |
255 | 364 | class TestConfigManager: |
256 | 365 | """Test ConfigManager functionality.""" |
257 | 366 |
|
|
0 commit comments