jfox-cli 0.4.0__tar.gz → 0.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/PKG-INFO +1 -1
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/__init__.py +1 -1
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/cli.py +4 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/daemon/__init__.py +8 -1
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/daemon/process.py +88 -21
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/indexer.py +2 -2
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/vector_store.py +52 -1
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/pyproject.toml +1 -1
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/unit/test_daemon_process.py +75 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/unit/test_indexer_clear_before_rebuild.py +13 -13
- jfox_cli-0.4.1/tests/unit/test_vector_store_clear.py +213 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/uv.lock +1 -1
- jfox_cli-0.4.0/tests/unit/test_vector_store_clear.py +0 -76
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/.claude/settings.local.json +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/.githooks/pre-push +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/.github/workflows/integration-test.yml +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/.github/workflows/publish.yml +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/.gitignore +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/.python-version +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/AGENTS.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/CHANGELOG.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/CLAUDE.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/DEVELOPMENT_PLAN.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/README.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/SESSION.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/SESSION_SUMMARY.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/installation.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/plans/2026-04-11-bulk-import-bm25-fix.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/plans/2026-04-11-edit-command.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/plans/2026-04-11-unify-format-option.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/plans/2026-04-12-ci-coverage-optimization.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/plans/2026-04-12-edit-content-file.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/plans/2026-04-12-fix-index-rebuild-clear.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/plans/2026-04-12-fix-index-verify-id-mismatch.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/plans/2026-04-12-fix-jfox-health-skill-kb-param.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/plans/2026-04-12-index-kb-param.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/plans/2026-04-12-lazy-import-perf.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/plans/2026-04-12-skill-redesign.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/plans/2026-04-14-sync-docs-daemon-show.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/plans/2026-04-15-session-summary-confirmation.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/specs/2026-04-03-bugfixes-design.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/specs/2026-04-12-skill-redesign-design.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/specs/2026-04-13-pr-auto-code-review-design.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/specs/2026-04-14-show-command-design.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/specs/2026-04-15-session-summary-confirmation-design.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/troubleshooting.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jessica-jones-static-cable.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/__main__.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/bm25_index.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/config.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/daemon/__main__.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/daemon/client.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/daemon/server.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/embedding_backend.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/formatters.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/git_extractor.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/global_config.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/graph.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/kb_manager.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/models.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/note.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/performance.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/search_engine.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/template.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/jfox/template_cli.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/pytest.ini +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/run_full_test.ps1 +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/skills-recommend/README.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/skills-recommend/claude-code/jfox-common/SKILL.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/skills-recommend/claude-code/jfox-ingest/SKILL.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/skills-recommend/claude-code/jfox-organize/SKILL.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/skills-recommend/claude-code/jfox-search/SKILL.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/skills-recommend/claude-code/jfox-session-summary/SKILL.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/COVERAGE_PLAN.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/MIGRATION.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/TESTS.md +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/conftest.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/integration/__init__.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/integration/test_backlinks.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/performance/__init__.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/performance/test_performance.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/test_advanced_features.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/test_cli_format.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/test_config_set_unit.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/test_config_unit.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/test_core_workflow.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/test_embedding_device.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/test_hybrid_search.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/test_integration.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/test_kb_current.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/test_suggest_links.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/unit/__init__.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/unit/test_bm25_batch.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/unit/test_edit.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/unit/test_format_unify.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/unit/test_formatters.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/unit/test_git_extractor.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/unit/test_global_config.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/unit/test_index_kb_param.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/unit/test_indexer_verify.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/unit/test_kb_manager.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/unit/test_lazy_import.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/unit/test_logging_config.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/unit/test_show.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/unit/test_template.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/unit/test_template_cli.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/utils/__init__.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/utils/assertions.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/utils/jfox_cli.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/utils/note_generator.py +0 -0
- {jfox_cli-0.4.0 → jfox_cli-0.4.1}/tests/utils/temp_kb.py +0 -0
|
@@ -2612,7 +2612,10 @@ def daemon(
|
|
|
2612
2612
|
|
|
2613
2613
|
try:
|
|
2614
2614
|
if action == "start":
|
|
2615
|
+
from .daemon.process import DAEMON_LOG_FILE
|
|
2616
|
+
|
|
2615
2617
|
console.print("[yellow]正在启动 embedding daemon...[/yellow]")
|
|
2618
|
+
console.print(f"[dim]日志文件: {DAEMON_LOG_FILE}[/dim]")
|
|
2616
2619
|
ok = start_daemon(port=port)
|
|
2617
2620
|
if ok:
|
|
2618
2621
|
info = get_daemon_status()
|
|
@@ -2631,6 +2634,7 @@ def daemon(
|
|
|
2631
2634
|
console.print("[green]✓ Daemon 已启动[/green]")
|
|
2632
2635
|
else:
|
|
2633
2636
|
console.print("[red]✗ Daemon 启动失败[/red]")
|
|
2637
|
+
console.print(f"[dim]查看日志: {DAEMON_LOG_FILE}[/dim]")
|
|
2634
2638
|
raise typer.Exit(1)
|
|
2635
2639
|
|
|
2636
2640
|
elif action == "stop":
|
|
@@ -3,9 +3,16 @@
|
|
|
3
3
|
DEFAULT_HOST = "127.0.0.1"
|
|
4
4
|
DEFAULT_PORT = 18700
|
|
5
5
|
|
|
6
|
-
from .process import
|
|
6
|
+
from .process import (
|
|
7
|
+
DAEMON_LOG_FILE,
|
|
8
|
+
get_daemon_status,
|
|
9
|
+
is_daemon_running,
|
|
10
|
+
start_daemon,
|
|
11
|
+
stop_daemon,
|
|
12
|
+
)
|
|
7
13
|
|
|
8
14
|
__all__ = [
|
|
15
|
+
"DAEMON_LOG_FILE",
|
|
9
16
|
"DEFAULT_HOST",
|
|
10
17
|
"DEFAULT_PORT",
|
|
11
18
|
"start_daemon",
|
|
@@ -36,6 +36,8 @@ def _get_pythonw_executable() -> str:
|
|
|
36
36
|
logger = logging.getLogger(__name__)
|
|
37
37
|
|
|
38
38
|
STARTUP_TIMEOUT = 60 # 模型加载可能较慢
|
|
39
|
+
FIRST_RUN_TIMEOUT = 300 # 首次下载模型超时(秒)
|
|
40
|
+
DAEMON_LOG_FILE = Path.home() / ".jfox_daemon.log"
|
|
39
41
|
PID_FILE = Path.home() / ".jfox_daemon.pid"
|
|
40
42
|
|
|
41
43
|
|
|
@@ -103,6 +105,56 @@ def is_daemon_running() -> bool:
|
|
|
103
105
|
return False
|
|
104
106
|
|
|
105
107
|
|
|
108
|
+
def _check_model_cache() -> dict:
|
|
109
|
+
"""
|
|
110
|
+
检查当前模型是否已缓存
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
dict: {"needs_download": bool, "model_name": str, "size_hint": str}
|
|
114
|
+
"""
|
|
115
|
+
try:
|
|
116
|
+
from ..config import config as _cfg
|
|
117
|
+
from ..embedding_backend import _CPU_DEFAULT_MODEL, _GPU_DEFAULT_MODEL
|
|
118
|
+
|
|
119
|
+
# 确定目标模型名
|
|
120
|
+
model_name = _cfg.embedding_model
|
|
121
|
+
if model_name == "auto" or not model_name:
|
|
122
|
+
try:
|
|
123
|
+
import torch
|
|
124
|
+
|
|
125
|
+
if torch.cuda.is_available():
|
|
126
|
+
model_name = _GPU_DEFAULT_MODEL
|
|
127
|
+
else:
|
|
128
|
+
model_name = _CPU_DEFAULT_MODEL
|
|
129
|
+
except Exception:
|
|
130
|
+
model_name = _CPU_DEFAULT_MODEL
|
|
131
|
+
|
|
132
|
+
# 检查 HuggingFace 缓存
|
|
133
|
+
hf_home = os.environ.get("HF_HOME", str(Path.home() / ".cache" / "huggingface"))
|
|
134
|
+
hub_cache = os.environ.get("HUGGINGFACE_HUB_CACHE", str(Path(hf_home) / "hub"))
|
|
135
|
+
model_cache_dir = Path(hub_cache) / f"models--{model_name.replace('/', '--')}"
|
|
136
|
+
|
|
137
|
+
size_hint = "2GB" if "bge-m3" in model_name else "90MB"
|
|
138
|
+
|
|
139
|
+
if model_cache_dir.exists():
|
|
140
|
+
snapshots_dir = model_cache_dir / "snapshots"
|
|
141
|
+
has_files = snapshots_dir.exists() and any(snapshots_dir.iterdir())
|
|
142
|
+
return {
|
|
143
|
+
"needs_download": not has_files,
|
|
144
|
+
"model_name": model_name,
|
|
145
|
+
"size_hint": size_hint,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
"needs_download": True,
|
|
150
|
+
"model_name": model_name,
|
|
151
|
+
"size_hint": size_hint,
|
|
152
|
+
}
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logger.debug(f"Model cache check failed, assuming download needed: {e}")
|
|
155
|
+
return {"needs_download": True, "model_name": "unknown", "size_hint": ""}
|
|
156
|
+
|
|
157
|
+
|
|
106
158
|
def start_daemon(host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> bool:
|
|
107
159
|
"""
|
|
108
160
|
启动 daemon 后台进程
|
|
@@ -124,6 +176,15 @@ def start_daemon(host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> bool:
|
|
|
124
176
|
)
|
|
125
177
|
return True
|
|
126
178
|
|
|
179
|
+
# 首次启动预检:检查模型缓存是否存在
|
|
180
|
+
timeout = STARTUP_TIMEOUT
|
|
181
|
+
cache_info = _check_model_cache()
|
|
182
|
+
if cache_info["needs_download"]:
|
|
183
|
+
logger.info(
|
|
184
|
+
f"首次启动需要下载模型 {cache_info['model_name']}" f"(约 {cache_info['size_hint']})"
|
|
185
|
+
)
|
|
186
|
+
timeout = FIRST_RUN_TIMEOUT
|
|
187
|
+
|
|
127
188
|
# 构建启动命令(Windows 使用 pythonw.exe 避免控制台窗口)
|
|
128
189
|
cmd = [
|
|
129
190
|
_get_pythonw_executable(),
|
|
@@ -137,7 +198,6 @@ def start_daemon(host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> bool:
|
|
|
137
198
|
|
|
138
199
|
kwargs = {}
|
|
139
200
|
if sys.platform == "win32":
|
|
140
|
-
# Windows: 后台分离进程,不弹窗
|
|
141
201
|
CREATE_NEW_PROCESS_GROUP = 0x00000200
|
|
142
202
|
DETACHED_PROCESS = 0x00000008
|
|
143
203
|
CREATE_NO_WINDOW = 0x08000000
|
|
@@ -145,39 +205,46 @@ def start_daemon(host: str = DEFAULT_HOST, port: int = DEFAULT_PORT) -> bool:
|
|
|
145
205
|
else:
|
|
146
206
|
kwargs["start_new_session"] = True
|
|
147
207
|
|
|
208
|
+
# 子进程日志落盘(stdout/stderr → 日志文件)
|
|
209
|
+
log_file = open(DAEMON_LOG_FILE, "a", encoding="utf-8")
|
|
210
|
+
|
|
148
211
|
try:
|
|
149
212
|
proc = subprocess.Popen(
|
|
150
213
|
cmd,
|
|
151
|
-
stdout=
|
|
152
|
-
stderr=
|
|
214
|
+
stdout=log_file,
|
|
215
|
+
stderr=log_file,
|
|
153
216
|
stdin=subprocess.DEVNULL,
|
|
154
217
|
**kwargs,
|
|
155
218
|
)
|
|
156
219
|
logger.info(f"Daemon 进程已启动 (PID: {proc.pid})")
|
|
220
|
+
logger.info(f"Daemon 日志文件: {DAEMON_LOG_FILE}")
|
|
157
221
|
except Exception as e:
|
|
222
|
+
log_file.close()
|
|
158
223
|
logger.error(f"启动 daemon 失败: {e}")
|
|
159
224
|
return False
|
|
160
225
|
|
|
161
226
|
# 等待 daemon 就绪(用 HTTP 健康检查判断,不用 PID)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
227
|
+
try:
|
|
228
|
+
for i in range(timeout):
|
|
229
|
+
time.sleep(1)
|
|
230
|
+
health = _http_health_check(host, port)
|
|
231
|
+
if health is not None:
|
|
232
|
+
real_pid = health.get("pid", proc.pid)
|
|
233
|
+
_write_pid_file(
|
|
234
|
+
{
|
|
235
|
+
"pid": real_pid,
|
|
236
|
+
"host": host,
|
|
237
|
+
"port": port,
|
|
238
|
+
"started_at": time.time(),
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
logger.info(f"Daemon 已就绪 (PID: {real_pid}, port: {port})")
|
|
242
|
+
return True
|
|
178
243
|
|
|
179
|
-
|
|
180
|
-
|
|
244
|
+
logger.warning(f"Daemon 启动超时({timeout}秒),日志见 {DAEMON_LOG_FILE}")
|
|
245
|
+
return False
|
|
246
|
+
finally:
|
|
247
|
+
log_file.close()
|
|
181
248
|
|
|
182
249
|
|
|
183
250
|
def stop_daemon() -> bool:
|
|
@@ -249,8 +249,8 @@ class Indexer:
|
|
|
249
249
|
if not notes_dir.exists():
|
|
250
250
|
return 0
|
|
251
251
|
|
|
252
|
-
#
|
|
253
|
-
self.vector_store.
|
|
252
|
+
# 彻底重建 collection(删除旧结构 + 重建,解决模型切换后维度不匹配)
|
|
253
|
+
self.vector_store.reset_collection()
|
|
254
254
|
|
|
255
255
|
# Find all note files
|
|
256
256
|
note_files = list(notes_dir.rglob("*.md"))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""ChromaDB 向量存储封装"""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import re
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Any, Dict, List, Optional
|
|
6
7
|
|
|
@@ -82,7 +83,26 @@ class VectorStore:
|
|
|
82
83
|
return True
|
|
83
84
|
|
|
84
85
|
except Exception as e:
|
|
85
|
-
|
|
86
|
+
error_msg = str(e)
|
|
87
|
+
if "dimension" in error_msg.lower() and "expecting" in error_msg.lower():
|
|
88
|
+
# 维度不匹配:模型已切换,提示用户 rebuild
|
|
89
|
+
dim_match = re.search(r"dimension of (\d+).*got (\d+)", error_msg, re.IGNORECASE)
|
|
90
|
+
if dim_match:
|
|
91
|
+
old_dim, new_dim = dim_match.group(1), dim_match.group(2)
|
|
92
|
+
logger.error(
|
|
93
|
+
f"Embedding 维度不匹配(collection: {old_dim}, "
|
|
94
|
+
f"当前模型: {new_dim})。"
|
|
95
|
+
f"可能是模型已切换,请执行 jfox index rebuild "
|
|
96
|
+
f"重建索引。原始错误: {error_msg}"
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
logger.error(
|
|
100
|
+
f"Embedding 维度不匹配,可能是模型已切换。"
|
|
101
|
+
f"请执行 jfox index rebuild 重建索引。"
|
|
102
|
+
f"原始错误: {error_msg}"
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
logger.error(f"Failed to add note {note.id}: {error_msg}")
|
|
86
106
|
return False
|
|
87
107
|
|
|
88
108
|
def search(
|
|
@@ -206,6 +226,37 @@ class VectorStore:
|
|
|
206
226
|
logger.error(f"Failed to clear vector store: {e}")
|
|
207
227
|
return False
|
|
208
228
|
|
|
229
|
+
def reset_collection(self) -> bool:
|
|
230
|
+
"""
|
|
231
|
+
彻底删除并重建 collection(用于 index rebuild)
|
|
232
|
+
|
|
233
|
+
与 clear() 不同,reset_collection() 会删除整个 collection 结构再重建,
|
|
234
|
+
确保 embedding dimension 等元信息也被重置。
|
|
235
|
+
适用于切换模型后需要 rebuild 的场景。
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
是否成功重建
|
|
239
|
+
"""
|
|
240
|
+
if self.client is None:
|
|
241
|
+
self.init()
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
self.client.delete_collection("notes")
|
|
245
|
+
logger.info("Deleted old collection 'notes'")
|
|
246
|
+
except ValueError:
|
|
247
|
+
# ChromaDB 对不存在的 collection 抛 ValueError,这是正常情况
|
|
248
|
+
logger.debug("Collection 'notes' did not exist, skipping delete")
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
self.collection = self.client.get_or_create_collection(
|
|
252
|
+
name="notes", metadata={"hnsw:space": "cosine"}
|
|
253
|
+
)
|
|
254
|
+
logger.info("Recreated collection 'notes'")
|
|
255
|
+
return True
|
|
256
|
+
except Exception as e:
|
|
257
|
+
logger.error(f"Failed to recreate collection: {e}")
|
|
258
|
+
return False
|
|
259
|
+
|
|
209
260
|
|
|
210
261
|
# 全局向量存储实例
|
|
211
262
|
_vector_store: Optional[VectorStore] = None
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Daemon 进程管理单元测试"""
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
from unittest.mock import patch
|
|
5
6
|
|
|
6
7
|
import pytest
|
|
@@ -84,3 +85,77 @@ class TestStartDaemonWindowsExecutable:
|
|
|
84
85
|
flags = kwargs.get("creationflags", 0)
|
|
85
86
|
CREATE_NO_WINDOW = 0x08000000
|
|
86
87
|
assert flags & CREATE_NO_WINDOW, "CREATE_NO_WINDOW flag not set"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestDaemonLogFile:
|
|
91
|
+
"""测试 daemon 子进程日志落盘"""
|
|
92
|
+
|
|
93
|
+
@patch("jfox.daemon.process.subprocess.Popen")
|
|
94
|
+
@patch("jfox.daemon.process._http_health_check")
|
|
95
|
+
def test_start_daemon_writes_to_log_file(self, mock_health, mock_popen):
|
|
96
|
+
"""start_daemon 应将子进程 stdout/stderr 重定向到日志文件"""
|
|
97
|
+
import subprocess
|
|
98
|
+
|
|
99
|
+
from jfox.daemon.process import start_daemon
|
|
100
|
+
|
|
101
|
+
mock_health.side_effect = [None, {"pid": 9999}]
|
|
102
|
+
mock_popen.return_value.pid = 1234
|
|
103
|
+
|
|
104
|
+
start_daemon()
|
|
105
|
+
|
|
106
|
+
call_kwargs = mock_popen.call_args[1]
|
|
107
|
+
# stdout 和 stderr 不应是 DEVNULL
|
|
108
|
+
assert call_kwargs["stdout"] != subprocess.DEVNULL
|
|
109
|
+
assert call_kwargs["stderr"] != subprocess.DEVNULL
|
|
110
|
+
|
|
111
|
+
@patch("jfox.daemon.process.subprocess.Popen")
|
|
112
|
+
@patch("jfox.daemon.process._http_health_check")
|
|
113
|
+
def test_daemon_log_file_path(self, mock_health, mock_popen):
|
|
114
|
+
"""DAEMON_LOG_FILE 应在用户 home 目录下"""
|
|
115
|
+
from jfox.daemon.process import DAEMON_LOG_FILE
|
|
116
|
+
|
|
117
|
+
assert str(DAEMON_LOG_FILE).endswith(".jfox_daemon.log")
|
|
118
|
+
assert DAEMON_LOG_FILE.parent == Path.home()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class TestDaemonModelCacheCheck:
|
|
122
|
+
"""测试模型缓存预检"""
|
|
123
|
+
|
|
124
|
+
def test_check_model_cache_returns_dict(self):
|
|
125
|
+
"""_check_model_cache 应返回包含必要键的字典"""
|
|
126
|
+
from jfox.daemon.process import _check_model_cache
|
|
127
|
+
|
|
128
|
+
result = _check_model_cache()
|
|
129
|
+
assert "needs_download" in result
|
|
130
|
+
assert "model_name" in result
|
|
131
|
+
assert "size_hint" in result
|
|
132
|
+
assert isinstance(result["needs_download"], bool)
|
|
133
|
+
|
|
134
|
+
@patch("jfox.daemon.process.os.environ", {})
|
|
135
|
+
def test_first_run_timeout_is_longer(self):
|
|
136
|
+
"""FIRST_RUN_TIMEOUT 应大于 STARTUP_TIMEOUT"""
|
|
137
|
+
from jfox.daemon.process import FIRST_RUN_TIMEOUT, STARTUP_TIMEOUT
|
|
138
|
+
|
|
139
|
+
assert FIRST_RUN_TIMEOUT > STARTUP_TIMEOUT
|
|
140
|
+
assert FIRST_RUN_TIMEOUT >= 300
|
|
141
|
+
|
|
142
|
+
@patch("jfox.daemon.process._check_model_cache")
|
|
143
|
+
@patch("jfox.daemon.process.subprocess.Popen")
|
|
144
|
+
@patch("jfox.daemon.process._http_health_check")
|
|
145
|
+
def test_first_run_uses_extended_timeout(self, mock_health, mock_popen, mock_cache):
|
|
146
|
+
"""首次下载模型时应使用 FIRST_RUN_TIMEOUT"""
|
|
147
|
+
from jfox.daemon.process import start_daemon
|
|
148
|
+
|
|
149
|
+
mock_health.side_effect = [None, {"pid": 9999}]
|
|
150
|
+
mock_popen.return_value.pid = 1234
|
|
151
|
+
mock_cache.return_value = {
|
|
152
|
+
"needs_download": True,
|
|
153
|
+
"model_name": "BAAI/bge-m3",
|
|
154
|
+
"size_hint": "2GB",
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
start_daemon()
|
|
158
|
+
|
|
159
|
+
# 验证 health check 被调用了足够多次(使用 FIRST_RUN_TIMEOUT)
|
|
160
|
+
# 由于第二次就返回了,至少被调用了 2 次
|
|
161
|
+
assert mock_health.call_count >= 2
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
测试 Indexer.index_all()
|
|
2
|
+
测试 Indexer.index_all() 在重建前重置 collection
|
|
3
3
|
|
|
4
|
-
验证 rebuild 流程:先
|
|
4
|
+
验证 rebuild 流程:先 reset_collection 再 index
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import tempfile
|
|
@@ -9,11 +9,11 @@ from pathlib import Path
|
|
|
9
9
|
from unittest.mock import MagicMock, patch
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
class
|
|
13
|
-
"""Indexer.index_all()
|
|
12
|
+
class TestIndexerResetBeforeRebuild:
|
|
13
|
+
"""Indexer.index_all() 应先重置 collection 再重建"""
|
|
14
14
|
|
|
15
|
-
def
|
|
16
|
-
"""index_all() 应在索引笔记前调用 vector_store.
|
|
15
|
+
def test_index_all_calls_vector_store_reset_collection(self):
|
|
16
|
+
"""index_all() 应在索引笔记前调用 vector_store.reset_collection()"""
|
|
17
17
|
from jfox.indexer import Indexer
|
|
18
18
|
|
|
19
19
|
mock_config = MagicMock()
|
|
@@ -29,10 +29,10 @@ class TestIndexerClearBeforeRebuild:
|
|
|
29
29
|
count = indexer.index_all()
|
|
30
30
|
|
|
31
31
|
assert count == 0
|
|
32
|
-
mock_vector_store.
|
|
32
|
+
mock_vector_store.reset_collection.assert_called_once()
|
|
33
33
|
|
|
34
|
-
def
|
|
35
|
-
"""
|
|
34
|
+
def test_index_all_reset_before_add(self):
|
|
35
|
+
"""reset_collection() 必须在 add_or_update_note() 之前调用"""
|
|
36
36
|
from jfox.indexer import Indexer
|
|
37
37
|
|
|
38
38
|
mock_config = MagicMock()
|
|
@@ -59,11 +59,11 @@ class TestIndexerClearBeforeRebuild:
|
|
|
59
59
|
|
|
60
60
|
# 验证调用顺序
|
|
61
61
|
calls = mock_vector_store.method_calls
|
|
62
|
-
|
|
62
|
+
reset_indices = [i for i, c in enumerate(calls) if c[0] == "reset_collection"]
|
|
63
63
|
add_indices = [i for i, c in enumerate(calls) if c[0] == "add_or_update_note"]
|
|
64
64
|
|
|
65
|
-
if
|
|
66
|
-
assert
|
|
67
|
-
f"
|
|
65
|
+
if reset_indices and add_indices:
|
|
66
|
+
assert reset_indices[0] < add_indices[0], (
|
|
67
|
+
f"reset_collection() (call #{reset_indices[0]}) must be before "
|
|
68
68
|
f"add_or_update_note() (call #{add_indices[0]})"
|
|
69
69
|
)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
测试 VectorStore.clear() 方法
|
|
3
|
+
|
|
4
|
+
验证 rebuild 前清除旧数据的逻辑
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from unittest.mock import MagicMock, patch
|
|
9
|
+
|
|
10
|
+
import chromadb
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestVectorStoreClear:
|
|
14
|
+
"""VectorStore.clear() 单元测试"""
|
|
15
|
+
|
|
16
|
+
def test_clear_removes_all_documents(self):
|
|
17
|
+
"""clear() 应删除 collection 中所有文档"""
|
|
18
|
+
from jfox.vector_store import VectorStore
|
|
19
|
+
|
|
20
|
+
store = VectorStore()
|
|
21
|
+
# 使用 EphemeralClient 避免文件锁,手动设置 client 和 collection
|
|
22
|
+
client = chromadb.EphemeralClient()
|
|
23
|
+
store.client = client
|
|
24
|
+
store.collection = client.create_collection(
|
|
25
|
+
name="test_clear", metadata={"hnsw:space": "cosine"}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# 插入 mock 数据(跳过 embedding,直接操作 collection)
|
|
29
|
+
store.collection.add(
|
|
30
|
+
ids=["note_001", "note_002", "note_003"],
|
|
31
|
+
documents=["doc1", "doc2", "doc3"],
|
|
32
|
+
embeddings=[[0.1] * 384, [0.2] * 384, [0.3] * 384],
|
|
33
|
+
metadatas=[
|
|
34
|
+
{"title": "t1", "type": "permanent", "filepath": "/a", "tags": ""},
|
|
35
|
+
{"title": "t2", "type": "permanent", "filepath": "/b", "tags": ""},
|
|
36
|
+
{"title": "t3", "type": "permanent", "filepath": "/c", "tags": ""},
|
|
37
|
+
],
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
assert store.collection.count() == 3
|
|
41
|
+
|
|
42
|
+
# 清除
|
|
43
|
+
result = store.clear()
|
|
44
|
+
|
|
45
|
+
assert result is True
|
|
46
|
+
assert store.collection.count() == 0
|
|
47
|
+
|
|
48
|
+
def test_clear_on_empty_collection(self):
|
|
49
|
+
"""clear() 在空 collection 上应正常返回 True"""
|
|
50
|
+
from jfox.vector_store import VectorStore
|
|
51
|
+
|
|
52
|
+
store = VectorStore()
|
|
53
|
+
client = chromadb.EphemeralClient()
|
|
54
|
+
store.client = client
|
|
55
|
+
store.collection = client.create_collection(
|
|
56
|
+
name="test_empty", metadata={"hnsw:space": "cosine"}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
assert store.collection.count() == 0
|
|
60
|
+
|
|
61
|
+
result = store.clear()
|
|
62
|
+
|
|
63
|
+
assert result is True
|
|
64
|
+
assert store.collection.count() == 0
|
|
65
|
+
|
|
66
|
+
def test_clear_returns_false_on_failure(self):
|
|
67
|
+
"""clear() 在异常时应返回 False 而非抛出"""
|
|
68
|
+
from jfox.vector_store import VectorStore
|
|
69
|
+
|
|
70
|
+
store = VectorStore()
|
|
71
|
+
# mock 一个会抛异常的 collection
|
|
72
|
+
store.collection = MagicMock()
|
|
73
|
+
store.collection.get.side_effect = Exception("DB error")
|
|
74
|
+
|
|
75
|
+
result = store.clear()
|
|
76
|
+
|
|
77
|
+
assert result is False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TestVectorStoreResetCollection:
|
|
81
|
+
"""VectorStore.reset_collection() 单元测试"""
|
|
82
|
+
|
|
83
|
+
def test_reset_collection_recreates_collection(self):
|
|
84
|
+
"""reset_collection() 应删除旧 collection 并创建新的(维度重置)"""
|
|
85
|
+
from jfox.vector_store import VectorStore
|
|
86
|
+
|
|
87
|
+
store = VectorStore()
|
|
88
|
+
client = chromadb.EphemeralClient()
|
|
89
|
+
store.client = client
|
|
90
|
+
store.collection = client.create_collection(name="notes", metadata={"hnsw:space": "cosine"})
|
|
91
|
+
|
|
92
|
+
# 插入 384 维数据
|
|
93
|
+
store.collection.add(
|
|
94
|
+
ids=["note_001"],
|
|
95
|
+
documents=["doc1"],
|
|
96
|
+
embeddings=[[0.1] * 384],
|
|
97
|
+
metadatas=[{"title": "t1", "type": "permanent", "filepath": "/a", "tags": ""}],
|
|
98
|
+
)
|
|
99
|
+
assert store.collection.count() == 1
|
|
100
|
+
|
|
101
|
+
# reset 后 collection 应为空
|
|
102
|
+
result = store.reset_collection()
|
|
103
|
+
|
|
104
|
+
assert result is True
|
|
105
|
+
assert store.collection.count() == 0
|
|
106
|
+
|
|
107
|
+
def test_reset_collection_on_nonexistent_collection(self):
|
|
108
|
+
"""reset_collection() 在 collection 不存在时应正常创建新的"""
|
|
109
|
+
from jfox.vector_store import VectorStore
|
|
110
|
+
|
|
111
|
+
store = VectorStore()
|
|
112
|
+
client = chromadb.EphemeralClient()
|
|
113
|
+
store.client = client
|
|
114
|
+
# 不创建 collection,client 上没有 "notes" collection
|
|
115
|
+
|
|
116
|
+
result = store.reset_collection()
|
|
117
|
+
|
|
118
|
+
assert result is True
|
|
119
|
+
assert store.collection is not None
|
|
120
|
+
assert store.collection.count() == 0
|
|
121
|
+
|
|
122
|
+
def test_reset_collection_returns_false_on_recreate_failure(self):
|
|
123
|
+
"""get_or_create_collection 失败时返回 False"""
|
|
124
|
+
from jfox.vector_store import VectorStore
|
|
125
|
+
|
|
126
|
+
store = VectorStore()
|
|
127
|
+
store.client = MagicMock()
|
|
128
|
+
store.client.delete_collection.return_value = None
|
|
129
|
+
store.client.get_or_create_collection.side_effect = Exception("DB error")
|
|
130
|
+
|
|
131
|
+
result = store.reset_collection()
|
|
132
|
+
|
|
133
|
+
assert result is False
|
|
134
|
+
|
|
135
|
+
def test_reset_collection_propagates_delete_failure(self):
|
|
136
|
+
"""delete_collection 非预期异常应向上冒泡(如文件锁、磁盘错误)"""
|
|
137
|
+
import pytest
|
|
138
|
+
|
|
139
|
+
from jfox.vector_store import VectorStore
|
|
140
|
+
|
|
141
|
+
store = VectorStore()
|
|
142
|
+
store.client = MagicMock()
|
|
143
|
+
# 非 ValueError 的异常(如文件锁)不应被静默吞掉
|
|
144
|
+
store.client.delete_collection.side_effect = RuntimeError("file locked")
|
|
145
|
+
|
|
146
|
+
with pytest.raises(RuntimeError, match="file locked"):
|
|
147
|
+
store.reset_collection()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class TestVectorStoreDimensionMismatch:
|
|
151
|
+
"""add_note() 维度不匹配时应给出友好提示"""
|
|
152
|
+
|
|
153
|
+
def test_add_note_dimension_mismatch_friendly_message(self):
|
|
154
|
+
"""维度不匹配时 logger.error 应包含 rebuild 提示"""
|
|
155
|
+
from jfox.vector_store import VectorStore
|
|
156
|
+
|
|
157
|
+
store = VectorStore()
|
|
158
|
+
client = chromadb.EphemeralClient()
|
|
159
|
+
store.client = client
|
|
160
|
+
store.collection = client.create_collection(
|
|
161
|
+
name="test_dim_mismatch", metadata={"hnsw:space": "cosine"}
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# 创建一个假笔记
|
|
165
|
+
note = MagicMock()
|
|
166
|
+
note.id = "20260412120000"
|
|
167
|
+
note.title = "Test"
|
|
168
|
+
note.content = "Test content"
|
|
169
|
+
note.type = MagicMock(value="permanent")
|
|
170
|
+
note.tags = []
|
|
171
|
+
note.filepath = MagicMock()
|
|
172
|
+
|
|
173
|
+
# mock collection.add 抛出维度不匹配异常
|
|
174
|
+
store.collection.add = MagicMock(
|
|
175
|
+
side_effect=Exception("Collection expecting embedding with dimension of 384, got 1024")
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
with patch("jfox.embedding_backend.get_backend") as mock_backend:
|
|
179
|
+
mock_backend.return_value.encode_single.return_value.tolist.return_value = [0.1] * 1024
|
|
180
|
+
with patch.object(logging.getLogger("jfox.vector_store"), "error") as mock_error:
|
|
181
|
+
result = store.add_note(note)
|
|
182
|
+
|
|
183
|
+
assert result is False
|
|
184
|
+
# 验证错误信息包含 rebuild 提示
|
|
185
|
+
error_msg = mock_error.call_args[0][0]
|
|
186
|
+
assert "jfox index rebuild" in error_msg
|
|
187
|
+
assert "384" in error_msg
|
|
188
|
+
assert "1024" in error_msg
|
|
189
|
+
|
|
190
|
+
def test_add_note_non_dimension_exception_unchanged(self):
|
|
191
|
+
"""非维度不匹配的异常仍使用原始错误信息格式"""
|
|
192
|
+
from jfox.vector_store import VectorStore
|
|
193
|
+
|
|
194
|
+
store = VectorStore()
|
|
195
|
+
store.collection = MagicMock()
|
|
196
|
+
store.collection.add.side_effect = Exception("Some other error")
|
|
197
|
+
|
|
198
|
+
note = MagicMock()
|
|
199
|
+
note.id = "20260412120000"
|
|
200
|
+
note.title = "Test"
|
|
201
|
+
note.content = "Content"
|
|
202
|
+
note.type = MagicMock(value="permanent")
|
|
203
|
+
note.tags = []
|
|
204
|
+
note.filepath = MagicMock()
|
|
205
|
+
|
|
206
|
+
with patch("jfox.embedding_backend.get_backend") as mock_backend:
|
|
207
|
+
mock_backend.return_value.encode_single.return_value.tolist.return_value = [0.1] * 384
|
|
208
|
+
with patch.object(logging.getLogger("jfox.vector_store"), "error") as mock_error:
|
|
209
|
+
result = store.add_note(note)
|
|
210
|
+
|
|
211
|
+
assert result is False
|
|
212
|
+
error_msg = mock_error.call_args[0][0]
|
|
213
|
+
assert "rebuild" not in error_msg.lower()
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
测试 VectorStore.clear() 方法
|
|
3
|
-
|
|
4
|
-
验证 rebuild 前清除旧数据的逻辑
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from unittest.mock import MagicMock
|
|
8
|
-
|
|
9
|
-
import chromadb
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class TestVectorStoreClear:
|
|
13
|
-
"""VectorStore.clear() 单元测试"""
|
|
14
|
-
|
|
15
|
-
def test_clear_removes_all_documents(self):
|
|
16
|
-
"""clear() 应删除 collection 中所有文档"""
|
|
17
|
-
from jfox.vector_store import VectorStore
|
|
18
|
-
|
|
19
|
-
store = VectorStore()
|
|
20
|
-
# 使用 EphemeralClient 避免文件锁,手动设置 client 和 collection
|
|
21
|
-
client = chromadb.EphemeralClient()
|
|
22
|
-
store.client = client
|
|
23
|
-
store.collection = client.create_collection(
|
|
24
|
-
name="test_clear", metadata={"hnsw:space": "cosine"}
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
# 插入 mock 数据(跳过 embedding,直接操作 collection)
|
|
28
|
-
store.collection.add(
|
|
29
|
-
ids=["note_001", "note_002", "note_003"],
|
|
30
|
-
documents=["doc1", "doc2", "doc3"],
|
|
31
|
-
embeddings=[[0.1] * 384, [0.2] * 384, [0.3] * 384],
|
|
32
|
-
metadatas=[
|
|
33
|
-
{"title": "t1", "type": "permanent", "filepath": "/a", "tags": ""},
|
|
34
|
-
{"title": "t2", "type": "permanent", "filepath": "/b", "tags": ""},
|
|
35
|
-
{"title": "t3", "type": "permanent", "filepath": "/c", "tags": ""},
|
|
36
|
-
],
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
assert store.collection.count() == 3
|
|
40
|
-
|
|
41
|
-
# 清除
|
|
42
|
-
result = store.clear()
|
|
43
|
-
|
|
44
|
-
assert result is True
|
|
45
|
-
assert store.collection.count() == 0
|
|
46
|
-
|
|
47
|
-
def test_clear_on_empty_collection(self):
|
|
48
|
-
"""clear() 在空 collection 上应正常返回 True"""
|
|
49
|
-
from jfox.vector_store import VectorStore
|
|
50
|
-
|
|
51
|
-
store = VectorStore()
|
|
52
|
-
client = chromadb.EphemeralClient()
|
|
53
|
-
store.client = client
|
|
54
|
-
store.collection = client.create_collection(
|
|
55
|
-
name="test_empty", metadata={"hnsw:space": "cosine"}
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
assert store.collection.count() == 0
|
|
59
|
-
|
|
60
|
-
result = store.clear()
|
|
61
|
-
|
|
62
|
-
assert result is True
|
|
63
|
-
assert store.collection.count() == 0
|
|
64
|
-
|
|
65
|
-
def test_clear_returns_false_on_failure(self):
|
|
66
|
-
"""clear() 在异常时应返回 False 而非抛出"""
|
|
67
|
-
from jfox.vector_store import VectorStore
|
|
68
|
-
|
|
69
|
-
store = VectorStore()
|
|
70
|
-
# mock 一个会抛异常的 collection
|
|
71
|
-
store.collection = MagicMock()
|
|
72
|
-
store.collection.get.side_effect = Exception("DB error")
|
|
73
|
-
|
|
74
|
-
result = store.clear()
|
|
75
|
-
|
|
76
|
-
assert result is False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/plans/2026-04-12-ci-coverage-optimization.md
RENAMED
|
File without changes
|
|
File without changes
|
{jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/plans/2026-04-12-fix-index-rebuild-clear.md
RENAMED
|
File without changes
|
{jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/plans/2026-04-12-fix-index-verify-id-mismatch.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/plans/2026-04-14-sync-docs-daemon-show.md
RENAMED
|
File without changes
|
{jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/plans/2026-04-15-session-summary-confirmation.md
RENAMED
|
File without changes
|
|
File without changes
|
{jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/specs/2026-04-12-skill-redesign-design.md
RENAMED
|
File without changes
|
{jfox_cli-0.4.0 → jfox_cli-0.4.1}/docs/superpowers/specs/2026-04-13-pr-auto-code-review-design.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jfox_cli-0.4.0 → jfox_cli-0.4.1}/skills-recommend/claude-code/jfox-session-summary/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|