foxclaw 0.1.2 → 0.1.4

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.
Files changed (368) hide show
  1. package/index.js +1 -8
  2. package/package.json +1 -1
  3. package/python/.env.example +8 -0
  4. package/python/.github/workflows/ci.yml +47 -2
  5. package/python/.github/workflows/release.yml +5 -0
  6. package/python/CHANGELOG.md +100 -0
  7. package/python/CHANGELOG_CN.md +100 -0
  8. package/python/README.md +17 -2
  9. package/python/README_CN.md +38 -20
  10. package/python/bench/bm25_audit.py +1 -14
  11. package/python/bench/bm25_regression.py +0 -1
  12. package/python/bench/bm25_stress.py +0 -4
  13. package/python/bench/cd_audit.py +0 -23
  14. package/python/bench/chaos.py +6 -8
  15. package/python/bench/e2e_real_models.py +0 -1
  16. package/python/bench/llm_retry_test.py +2 -9
  17. package/python/bench/load_test.py +5 -8
  18. package/python/bench/vector_ivf_upgrade.py +1 -7
  19. package/python/docs/CONTENT_GUARD.md +189 -0
  20. package/python/docs/CONTENT_GUARD_CN.md +189 -0
  21. package/python/docs/RELEASE.md +78 -37
  22. package/python/docs/RUN_CN.md +2 -2
  23. package/python/evals/governance/cases.py +0 -37
  24. package/python/evals/governance/run_evals.py +5 -37
  25. package/python/examples/governance_middleware_demo.py +1 -6
  26. package/python/examples/langgraph_governance_demo.py +7 -11
  27. package/python/foxclaw/__init__.py +74 -152
  28. package/python/foxclaw/agents/base.py +82 -136
  29. package/python/foxclaw/agents/impl/_helpers.py +2 -10
  30. package/python/foxclaw/agents/impl/finance.py +4 -21
  31. package/python/foxclaw/agents/impl/office.py +7 -24
  32. package/python/foxclaw/agents/impl/supervisor.py +12 -79
  33. package/python/foxclaw/agents/mixins/_context.py +0 -23
  34. package/python/foxclaw/agents/mixins/augment.py +10 -16
  35. package/python/foxclaw/agents/mixins/llm.py +7 -6
  36. package/python/foxclaw/agents/mixins/memory.py +23 -3
  37. package/python/foxclaw/agents/mixins/preference.py +58 -8
  38. package/python/foxclaw/agents/mixins/prompt.py +59 -12
  39. package/python/foxclaw/agents/mixins/skillbox.py +6 -6
  40. package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
  41. package/python/foxclaw/agents/rules/finance/RULES.md +131 -0
  42. package/python/foxclaw/api/__init__.py +18 -77
  43. package/python/foxclaw/api/_state.py +43 -0
  44. package/python/foxclaw/api/bootstrap.py +114 -0
  45. package/python/foxclaw/api/middleware.py +5 -28
  46. package/python/foxclaw/api/routes/ahs.py +60 -73
  47. package/python/foxclaw/api/routes/attachments.py +5 -6
  48. package/python/foxclaw/api/routes/browser.py +7 -11
  49. package/python/foxclaw/api/routes/domain.py +18 -26
  50. package/python/foxclaw/api/routes/knowledge.py +3 -21
  51. package/python/foxclaw/api/routes/llm_admin.py +21 -33
  52. package/python/foxclaw/api/routes/memory.py +50 -12
  53. package/python/foxclaw/api/routes/orchestration.py +3 -5
  54. package/python/foxclaw/api/routes/recovery.py +4 -3
  55. package/python/foxclaw/api/routes/review.py +2 -11
  56. package/python/foxclaw/api/routes/search.py +3 -7
  57. package/python/foxclaw/api/routes/system.py +33 -49
  58. package/python/foxclaw/api/routes/task.py +31 -49
  59. package/python/foxclaw/api/routes/ws.py +2 -6
  60. package/python/foxclaw/api/spine.py +1 -4
  61. package/python/foxclaw/behavior/__init__.py +20 -0
  62. package/python/foxclaw/behavior/behavior.yaml +62 -0
  63. package/python/foxclaw/behavior/spec.py +287 -0
  64. package/python/foxclaw/browser/action/action_executor.py +1 -5
  65. package/python/foxclaw/browser/action/action_types.py +0 -1
  66. package/python/foxclaw/browser/action/url_policy.py +1 -5
  67. package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
  68. package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
  69. package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
  70. package/python/foxclaw/browser/perception/element_indexer.py +1 -2
  71. package/python/foxclaw/browser/service.py +7 -11
  72. package/python/foxclaw/capability/embedding_service.py +4 -6
  73. package/python/foxclaw/channels/__init__.py +9 -11
  74. package/python/foxclaw/channels/discord.py +4 -13
  75. package/python/foxclaw/channels/dispatcher.py +2 -27
  76. package/python/foxclaw/channels/feishu.py +10 -20
  77. package/python/foxclaw/channels/openclaw_bridge.py +4 -9
  78. package/python/foxclaw/channels/signing.py +9 -9
  79. package/python/foxclaw/channels/telegram.py +3 -4
  80. package/python/foxclaw/channels/weixin.py +7 -17
  81. package/python/foxclaw/cli.py +42 -13
  82. package/python/foxclaw/cognitive/reasoner.py +9 -10
  83. package/python/foxclaw/cognitive/state.py +18 -24
  84. package/python/foxclaw/core/audit_chain.py +1 -3
  85. package/python/foxclaw/core/audit_sink.py +3 -3
  86. package/python/foxclaw/core/compaction.py +8 -26
  87. package/python/foxclaw/core/config.py +2 -2
  88. package/python/foxclaw/core/confirm_nonce.py +6 -12
  89. package/python/foxclaw/core/content_guard.py +341 -0
  90. package/python/foxclaw/core/envfile.py +2 -2
  91. package/python/foxclaw/core/failure_policy.py +17 -26
  92. package/python/foxclaw/core/gateway.py +6 -10
  93. package/python/foxclaw/core/governance_audit.py +3 -7
  94. package/python/foxclaw/core/idempotency.py +15 -22
  95. package/python/foxclaw/core/injection_guard.py +10 -29
  96. package/python/foxclaw/core/memory_adapter.py +53 -60
  97. package/python/foxclaw/core/metrics.py +4 -5
  98. package/python/foxclaw/core/model_router.py +74 -150
  99. package/python/foxclaw/core/paths.py +1 -1
  100. package/python/foxclaw/core/recovery.py +21 -34
  101. package/python/foxclaw/core/scheduler.py +23 -26
  102. package/python/foxclaw/core/secrets.py +0 -3
  103. package/python/foxclaw/core/sensitive_write.py +283 -0
  104. package/python/foxclaw/core/sessions.py +4 -6
  105. package/python/foxclaw/core/settings.py +16 -32
  106. package/python/foxclaw/core/shared_write.py +11 -23
  107. package/python/foxclaw/core/steering_queue.py +12 -11
  108. package/python/foxclaw/core/system_bus.py +1 -1
  109. package/python/foxclaw/core/thought_chain.py +30 -32
  110. package/python/foxclaw/core/token_meter.py +9 -13
  111. package/python/foxclaw/core/tools.py +4 -12
  112. package/python/foxclaw/core/tracing.py +25 -40
  113. package/python/foxclaw/core/truth_pipeline.py +5 -9
  114. package/python/foxclaw/core/update_check.py +149 -0
  115. package/python/foxclaw/core/vec_recall_adapter.py +11 -37
  116. package/python/foxclaw/core/verify.py +6 -14
  117. package/python/foxclaw/core/web_search.py +4 -28
  118. package/python/foxclaw/finance/evidence.py +14 -18
  119. package/python/foxclaw/finance/fx.py +1 -3
  120. package/python/foxclaw/finance/graph.py +9 -18
  121. package/python/foxclaw/finance/models.py +20 -24
  122. package/python/foxclaw/finance/nlparse.py +12 -72
  123. package/python/foxclaw/finance/policy.py +8 -36
  124. package/python/foxclaw/finance/reasoner.py +3 -11
  125. package/python/foxclaw/finance/reasoning.py +13 -34
  126. package/python/foxclaw/finance/reconcile.py +5 -10
  127. package/python/foxclaw/finance/runtime.py +19 -46
  128. package/python/foxclaw/governance/middleware.py +13 -21
  129. package/python/foxclaw/harness/_base.py +2 -8
  130. package/python/foxclaw/harness/_policy.py +19 -38
  131. package/python/foxclaw/harness/drift.py +1 -13
  132. package/python/foxclaw/harness/event_bus.py +2 -5
  133. package/python/foxclaw/harness/harness.py +9 -97
  134. package/python/foxclaw/harness/loop_detection.py +11 -18
  135. package/python/foxclaw/itinerary/amap_client.py +2 -5
  136. package/python/foxclaw/itinerary/models.py +7 -7
  137. package/python/foxclaw/itinerary/reasoner.py +1 -6
  138. package/python/foxclaw/itinerary/reasoning.py +15 -21
  139. package/python/foxclaw/itinerary/runtime.py +8 -12
  140. package/python/foxclaw/kernel/capabilities.py +5 -6
  141. package/python/foxclaw/kernel/policy_gate.py +4 -10
  142. package/python/foxclaw/kernel/process_manager.py +17 -36
  143. package/python/foxclaw/kernel/scheduler.py +3 -9
  144. package/python/foxclaw/kernel/sentinel.py +13 -23
  145. package/python/foxclaw/kernel/sentinel_client.py +12 -15
  146. package/python/foxclaw/kernel/spine.py +9 -9
  147. package/python/foxclaw/knowledge/__init__.py +4 -20
  148. package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
  149. package/python/foxclaw/learning/knowledge_graph.py +4 -9
  150. package/python/foxclaw/learning/models.py +13 -13
  151. package/python/foxclaw/learning/reasoner.py +1 -4
  152. package/python/foxclaw/learning/runtime.py +19 -31
  153. package/python/foxclaw/learning/spaced_repetition.py +2 -7
  154. package/python/foxclaw/mcp_server.py +2 -3
  155. package/python/foxclaw/orchestration/__init__.py +16 -33
  156. package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
  157. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  158. package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
  159. package/python/foxclaw/planning/llm_planner.py +3 -3
  160. package/python/foxclaw/planning/planner.py +6 -11
  161. package/python/foxclaw/search/agent_port.py +7 -11
  162. package/python/foxclaw/search/evidence.py +0 -4
  163. package/python/foxclaw/search/internal_search.py +3 -5
  164. package/python/foxclaw/search/models.py +11 -13
  165. package/python/foxclaw/search/planner.py +3 -3
  166. package/python/foxclaw/search/retriever.py +1 -1
  167. package/python/foxclaw/search/runtime.py +1 -8
  168. package/python/foxclaw/skills/__init__.py +46 -100
  169. package/python/foxclaw/skills/archive.py +111 -0
  170. package/python/foxclaw/skills/fetch.py +226 -0
  171. package/python/foxclaw/skills/hooks_spec.py +21 -25
  172. package/python/foxclaw/skills/install_cmd.py +216 -0
  173. package/python/foxclaw/skills/lifecycle.py +16 -42
  174. package/python/foxclaw/skills/markdown_skill.py +10 -9
  175. package/python/foxclaw/skills/mcp_client.py +5 -11
  176. package/python/foxclaw/skills/mcp_store.py +0 -3
  177. package/python/foxclaw/skills/permission.py +13 -48
  178. package/python/foxclaw/skills/pipeline.py +21 -53
  179. package/python/foxclaw/skills/policy.py +4 -10
  180. package/python/foxclaw/skills/review.py +3 -3
  181. package/python/frontend/deck/auth.js +0 -7
  182. package/python/frontend/deck/chat_model_picker.js +0 -54
  183. package/python/frontend/deck/domain.js +0 -20
  184. package/python/frontend/deck/main.js +0 -144
  185. package/python/frontend/deck/markdown.js +0 -4
  186. package/python/frontend/deck/settings.js +1 -18
  187. package/python/frontend/deck/state.js +0 -1
  188. package/python/frontend/deck/util.js +0 -2
  189. package/python/frontend/deck/visuals.js +0 -22
  190. package/python/frontend/planet/planet.js +18 -201
  191. package/python/frontend/v3/assets/index-JKfHlb-a.css +1 -0
  192. package/python/frontend/v3/assets/index-tN90dpUH.js +34 -0
  193. package/python/frontend/v3/index.html +3 -3
  194. package/python/frontend/widget_runtime.js +0 -43
  195. package/python/frontend/widgets.js +0 -57
  196. package/python/frontend_src/index.html +1 -1
  197. package/python/frontend_src/package-lock.json +2 -2
  198. package/python/frontend_src/package.json +1 -1
  199. package/python/frontend_src/src/App.vue +3 -7
  200. package/python/frontend_src/src/api.js +0 -7
  201. package/python/frontend_src/src/components/ChatDock.vue +27 -39
  202. package/python/frontend_src/src/components/DocViewer.vue +0 -9
  203. package/python/frontend_src/src/components/Inspector.vue +0 -6
  204. package/python/frontend_src/src/components/LeftRail.vue +60 -39
  205. package/python/frontend_src/src/components/MenuPopover.vue +9 -11
  206. package/python/frontend_src/src/components/MessageStream.vue +18 -30
  207. package/python/frontend_src/src/components/ModelPicker.vue +0 -5
  208. package/python/frontend_src/src/components/QuickIngest.vue +0 -6
  209. package/python/frontend_src/src/components/Stage.vue +71 -25
  210. package/python/frontend_src/src/components/ToolMenu.vue +0 -3
  211. package/python/frontend_src/src/components/TopBar.vue +38 -10
  212. package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
  213. package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
  214. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
  215. package/python/frontend_src/src/components/panels/MemoryPanel.vue +42 -3
  216. package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
  217. package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
  218. package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
  219. package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
  220. package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
  221. package/python/frontend_src/src/lib/fmt.js +0 -4
  222. package/python/frontend_src/src/lib/i18n.js +53 -0
  223. package/python/frontend_src/src/lib/locales/en.js +173 -0
  224. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  225. package/python/frontend_src/src/lib/md.js +0 -14
  226. package/python/frontend_src/src/main.js +2 -0
  227. package/python/frontend_src/src/stores/app.js +17 -32
  228. package/python/frontend_src/src/stores/chat.js +0 -34
  229. package/python/frontend_src/src/stores/model.js +0 -8
  230. package/python/frontend_src/src/stores/panels.js +16 -44
  231. package/python/frontend_src/src/stores/profile.js +31 -0
  232. package/python/frontend_src/src/stores/system.js +25 -0
  233. package/python/frontend_src/vite.config.js +0 -7
  234. package/python/pyproject.toml +17 -3
  235. package/python/requirements.txt +311 -0
  236. package/python/tests/conftest.py +0 -22
  237. package/python/tests/run_all.py +4 -66
  238. package/python/tests/test_agent_lifecycle.py +4 -38
  239. package/python/tests/test_agent_rules.py +81 -0
  240. package/python/tests/test_ahs_p2.py +4 -25
  241. package/python/tests/test_anthropic_tools.py +1 -15
  242. package/python/tests/test_api.py +19 -9
  243. package/python/tests/test_api_auth.py +1 -5
  244. package/python/tests/test_attachments.py +9 -25
  245. package/python/tests/test_audit_chain.py +2 -7
  246. package/python/tests/test_audit_sink.py +0 -6
  247. package/python/tests/test_behavior_spec.py +198 -0
  248. package/python/tests/test_browser.py +2 -6
  249. package/python/tests/test_browser_request_guard.py +0 -4
  250. package/python/tests/test_capabilities.py +0 -6
  251. package/python/tests/test_channel_signing.py +2 -9
  252. package/python/tests/test_channels.py +1 -11
  253. package/python/tests/test_config_layering.py +0 -4
  254. package/python/tests/test_confirm_nonce.py +1 -12
  255. package/python/tests/test_conflict_dismiss.py +143 -0
  256. package/python/tests/test_content_guard.py +177 -0
  257. package/python/tests/test_context_compaction_wiring.py +1 -7
  258. package/python/tests/test_dispatch.py +0 -6
  259. package/python/tests/test_domain_evals.py +11 -80
  260. package/python/tests/test_domain_import.py +0 -6
  261. package/python/tests/test_domain_reasoners.py +0 -14
  262. package/python/tests/test_e2e.py +0 -17
  263. package/python/tests/test_emb_cache.py +7 -17
  264. package/python/tests/test_embedding_singleton.py +1 -3
  265. package/python/tests/test_evals_smoke.py +0 -24
  266. package/python/tests/test_event_spine.py +10 -18
  267. package/python/tests/test_evidence_semantic.py +1 -14
  268. package/python/tests/test_evidence_sse.py +3 -6
  269. package/python/tests/test_file_ingest.py +0 -11
  270. package/python/tests/test_finance.py +0 -9
  271. package/python/tests/test_finance_ledger.py +5 -12
  272. package/python/tests/test_finance_persist.py +8 -12
  273. package/python/tests/test_gateway.py +3 -22
  274. package/python/tests/test_governance.py +5 -52
  275. package/python/tests/test_governance_audit.py +4 -39
  276. package/python/tests/test_governance_middleware.py +1 -7
  277. package/python/tests/test_hkc.py +6 -24
  278. package/python/tests/test_hkc_index_build_manager.py +0 -33
  279. package/python/tests/test_hkc_manifest_reuse.py +4 -27
  280. package/python/tests/test_hkc_search_event_sync.py +3 -54
  281. package/python/tests/test_import_path_guard.py +0 -6
  282. package/python/tests/test_injection_guard.py +18 -36
  283. package/python/tests/test_integration.py +5 -29
  284. package/python/tests/test_itinerary.py +0 -3
  285. package/python/tests/test_kee_semantic.py +0 -5
  286. package/python/tests/test_kernel.py +1 -11
  287. package/python/tests/test_learning.py +3 -12
  288. package/python/tests/test_llm_admin.py +4 -19
  289. package/python/tests/test_llm_planner.py +1 -23
  290. package/python/tests/test_markdown_skills.py +3 -14
  291. package/python/tests/test_markdown_skills_openclaw.py +4 -9
  292. package/python/tests/test_mcp_connectors.py +3 -15
  293. package/python/tests/test_mcp_guardian_discovery.py +2 -6
  294. package/python/tests/test_mcp_no_memory.py +3 -3
  295. package/python/tests/test_mcp_policy_gate.py +0 -1
  296. package/python/tests/test_mcp_server.py +6 -9
  297. package/python/tests/test_memory_recall.py +1 -4
  298. package/python/tests/test_metrics.py +0 -8
  299. package/python/tests/test_multi_chain.py +1 -13
  300. package/python/tests/test_multimodal.py +5 -18
  301. package/python/tests/test_negotiation.py +3 -13
  302. package/python/tests/test_orchestration.py +1 -11
  303. package/python/tests/test_orchestration_decoupled.py +0 -6
  304. package/python/tests/test_p3_p5_hardening.py +3 -17
  305. package/python/tests/test_parallel_dispatch.py +4 -12
  306. package/python/tests/test_pipeline.py +17 -58
  307. package/python/tests/test_plan_visible.py +4 -4
  308. package/python/tests/test_planner.py +0 -7
  309. package/python/tests/test_policy_gate.py +2 -3
  310. package/python/tests/test_profile.py +67 -0
  311. package/python/tests/test_readme_claims.py +163 -0
  312. package/python/tests/test_reasoner.py +2 -17
  313. package/python/tests/test_release_check.py +2 -13
  314. package/python/tests/test_resume_checkpoint.py +7 -15
  315. package/python/tests/test_routing.py +2 -12
  316. package/python/tests/test_scheduler.py +7 -10
  317. package/python/tests/test_search.py +1 -11
  318. package/python/tests/test_secrets.py +4 -10
  319. package/python/tests/test_semantic.py +1 -10
  320. package/python/tests/test_sensitive_write.py +181 -0
  321. package/python/tests/test_sessions.py +3 -4
  322. package/python/tests/test_settings.py +2 -18
  323. package/python/tests/test_shadow_learning.py +3 -43
  324. package/python/tests/test_shared_write.py +0 -7
  325. package/python/tests/test_skill_taint_gate.py +1 -10
  326. package/python/tests/test_skills.py +2 -18
  327. package/python/tests/test_skills_fetch.py +282 -0
  328. package/python/tests/test_stats_and_events.py +2 -5
  329. package/python/tests/test_streaming.py +2 -7
  330. package/python/tests/test_supervisor_pipeline.py +4 -5
  331. package/python/tests/test_supervisor_triage.py +4 -10
  332. package/python/tests/test_taint.py +2 -7
  333. package/python/tests/test_task_stream.py +1 -12
  334. package/python/tests/test_thinking_sse.py +0 -1
  335. package/python/tests/test_tool_center.py +1 -17
  336. package/python/tests/test_tool_parallel.py +2 -2
  337. package/python/tests/test_tool_params.py +1 -5
  338. package/python/tests/test_tool_use.py +0 -13
  339. package/python/tests/test_toplevel_concurrency.py +3 -4
  340. package/python/tests/test_tracing.py +7 -41
  341. package/python/tests/test_truth_crystallizer.py +4 -6
  342. package/python/tests/test_truth_flow.py +2 -7
  343. package/python/tests/test_update_check.py +95 -0
  344. package/python/tests/test_verify_evidence.py +8 -21
  345. package/python/tests/test_version_sync.py +35 -6
  346. package/python/tests/test_web_trigger.py +0 -17
  347. package/python/tools/build_content_lexicon.py +218 -0
  348. package/python/tools/bundle_web.py +98 -0
  349. package/python/tools/check_faiss_type.py +0 -1
  350. package/python/tools/check_log.ps1 +0 -8
  351. package/python/tools/mypy_baseline.txt +12 -4
  352. package/python/tools/mypy_ratchet.py +52 -6
  353. package/python/tools/pack.py +0 -5
  354. package/python/tools/recheck_conflicts.py +156 -0
  355. package/python/tools/release_check.py +24 -15
  356. package/python/tools/restart_foxclaw.ps1 +5 -8
  357. package/python/tools/shared_write_audit.py +1 -15
  358. package/python/tools/size_check.py +1 -1
  359. package/python/tools/strip_comments.py +228 -0
  360. package/python/vendor/hkc/hkc_api/routes/ability.py +31 -0
  361. package/python/vendor/hkc/hkc_api/schemas.py +11 -0
  362. package/python/vendor/hkc/hkc_kee/kee.py +124 -3
  363. package/python/vendor/hkc/hkc_sdk/base.py +22 -0
  364. package/python/vendor/hkc/hkc_sdk/direct_client.py +10 -0
  365. package/python/vendor/hkc/hkc_sdk/http_client.py +19 -0
  366. package/python/foxclaw/api/app.py +0 -123
  367. package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
  368. package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
@@ -35,11 +35,10 @@ def test_assemble_search_close():
35
35
  print(f" ✓ stats: total_kus={s.get('total_kus')} embedding={s.get('embedding')!r}")
36
36
 
37
37
  svc.close()
38
- svc.close() # 幂等
38
+ svc.close()
39
39
  print(" ✓ close() 幂等")
40
40
  finally:
41
- # 关键:close 后 4 个 SQLite 句柄全释放,才能删掉目录(此前 events.db/id_counters.db 泄漏)
42
- shutil.rmtree(d) # 不 ignore_errors:泄漏则抛 OSError → 测试失败
41
+ shutil.rmtree(d)
43
42
  ok_rmtree = True
44
43
  assert ok_rmtree
45
44
  print(" ✓ close 后 rmtree 成功 → H1 资源泄漏修复生效(无 WinError 32)")
@@ -51,11 +50,10 @@ def test_via_foxclawos_lazy_teardown():
51
50
  try:
52
51
  from foxclaw import Foxclaw
53
52
  os_ = Foxclaw(data_dir=d, shadow_rounds=50)
54
- # 惰性:未访问 .hkc 前不该装配(不开句柄)
55
53
  assert os_._hkc_service is None and os_._hkc_inited is False
56
54
  print(" ✓ 构造 OS 后未装配 HKC(惰性,未开 db 句柄)")
57
55
 
58
- hkc = os_.hkc # 触发装配
56
+ hkc = os_.hkc
59
57
  assert os_._hkc_inited is True
60
58
  assert hkc is not None and hkc.available
61
59
  print(" ✓ 首次访问 os_.hkc 触发装配 available=True")
@@ -67,7 +65,7 @@ def test_via_foxclawos_lazy_teardown():
67
65
  asyncio.run(os_.aclose())
68
66
  print(" ✓ aclose() 释放浏览器 + HKC 资源")
69
67
  finally:
70
- shutil.rmtree(d) # aclose 已释放 HKC 句柄,应能删
68
+ shutil.rmtree(d)
71
69
  print(" ✓ teardown 后 rmtree 成功(HKC 句柄经 aclose 释放)")
72
70
 
73
71
 
@@ -79,11 +77,9 @@ def test_ability_readonly_surface():
79
77
  svc = init_hkc(d, api_key=None)
80
78
  assert svc is not None and svc.available
81
79
  from hkc_sdk.exceptions import HKCInsufficientCoverage, HKCNotFoundError
82
- # list_abilities:返回 taxonomy 里定义的能力 key(静态,无需摄入)
83
80
  keys = svc.client.list_abilities()
84
81
  assert isinstance(keys, list) and len(keys) > 0, f"应有内置能力定义,实得 {keys}"
85
82
  print(f" ✓ list_abilities 返回 {len(keys)} 个能力: {keys[:4]}...")
86
- # compile_ability:空知识库 → 覆盖不足 → 证据 gate(抛 HKCInsufficientCoverage,带 missing/coverage)
87
83
  gated = False
88
84
  try:
89
85
  svc.client.compile_ability(keys[0])
@@ -92,9 +88,6 @@ def test_ability_readonly_surface():
92
88
  assert hasattr(e, "missing_skills") and hasattr(e, "coverage")
93
89
  assert gated, "空知识库编译能力应被证据 gate 拦(覆盖不足)"
94
90
  print(" ✓ 空库 compile_ability 被证据 gate 拦(覆盖不足→能力不予授予)")
95
- # neighbors:不存在 KU → SDK 抛 HKCNotFoundError(契约)。注:/api/knowledge/ku/{id}/neighbors
96
- # 端点已在 V3 接口清理删除(前端零用 + HKC router 的 /knowledge/ku/{id}/neighbors 有等价),
97
- # 此处只验 SDK 层契约;SDK neighbors 仍被 ACE 能力/图查询内部使用,保留测试。
98
91
  raised = False
99
92
  try:
100
93
  svc.client.neighbors("KU_nonexistent", max_depth=2, top_k=10)
@@ -116,14 +109,12 @@ def test_agent_available_abilities():
116
109
  try:
117
110
  from foxclaw import Foxclaw
118
111
  os_ = Foxclaw(data_dir=d, shadow_rounds=50)
119
- # 空知识库:所有能力都应 sufficient=False(证据 gate:没知识不予授予)
120
112
  abils = os_.finance.available_abilities()
121
113
  assert isinstance(abils, list) and len(abils) > 0, f"应枚举出能力,实得 {abils}"
122
114
  assert all(a["sufficient"] is False for a in abils), \
123
115
  f"空库→全部证据 gate 拦(sufficient=False),实得 {[a['sufficient'] for a in abils]}"
124
116
  assert all("missing_skills" in a for a in abils), "未达标应附 missing_skills"
125
117
  print(f" ✓ 空库自省:{len(abils)} 个能力全 sufficient=False(证据 gate)")
126
- # 各 Agent 看到一致(知识全局共享)
127
118
  assert len(os_.office.available_abilities()) == len(abils)
128
119
  print(" ✓ 各 Agent 一致(HKC 知识全局共享)")
129
120
  asyncio.run(os_.aclose())
@@ -143,14 +134,11 @@ def test_ability_context_injection():
143
134
  ag = os_.finance
144
135
  base = [{"role": "user", "content": "原始问题"}]
145
136
 
146
- # (a) 默认关:不注入(行为逐字不变)
147
137
  assert ag._ability_augment("帮我做写作专家分析", list(base)) == base
148
138
 
149
- # (b) 启用但无达标能力(真实空 HKC)→ no-op
150
139
  ag.ABILITY_AUGMENT = True
151
140
  assert ag._ability_augment("帮我做写作专家分析", list(base)) == base
152
141
 
153
- # (c) 启用 + 注册假 HKC(有达标能力 + 检索命中)→ 注入知识上下文到 messages 前
154
142
  class _C:
155
143
  def list_abilities(self): return ["writing_expert"]
156
144
  def compile_ability(self, k):
@@ -165,7 +153,6 @@ def test_ability_context_injection():
165
153
  assert out[1] == base[0], "原消息应保留在注入内容之后"
166
154
  print(" ✓ 默认关不注入;空库 no-op;启用+达标+命中→注入知识上下文")
167
155
 
168
- # (d) fail-safe:能力增强内部异常(search 抛错)→ 原样返回,不影响主流程
169
156
  class _Cbad(_C):
170
157
  def search(self, *a, **k): raise RuntimeError("boom")
171
158
  os_.capabilities.register("hkc", SimpleNamespace(available=True, client=_Cbad()))
@@ -187,7 +174,7 @@ def test_ability_as_skill_step4():
187
174
  def list_abilities(self): return ["writing_expert", "quant_analyst"]
188
175
  def compile_ability(self, k):
189
176
  if k == "quant_analyst":
190
- raise RuntimeError("coverage 不足") # 任何异常 → 证据 gate 跳过
177
+ raise RuntimeError("coverage 不足")
191
178
  return SimpleNamespace(ability_key=k, display_name="写作专家",
192
179
  domain="writing", coverage={"修辞": 0.9})
193
180
  def search(self, q, mode="hybrid", top_k=10, domain=""):
@@ -197,19 +184,16 @@ def test_ability_as_skill_step4():
197
184
  hkc = SimpleNamespace(available=True, client=_C())
198
185
  reg = SkillRegistry()
199
186
  ids = register_abilities_as_skills(reg, hkc)
200
- # 证据 gate:只注册达标的 writing_expert;覆盖不足的 quant_analyst 不注册
201
187
  assert ids == ["ability:writing_expert"], ids
202
188
  assert reg.get("ability:quant_analyst") is None, "覆盖不足→不注册(证据 gate)"
203
189
  spec = reg.get("ability:writing_expert")
204
190
  assert spec is not None and spec.side_effect == "read", "能力 skill 为 read(无外部副作用)"
205
191
 
206
- # 经 SkillAdapter(→PolicyGate 四阶段)调用:read → 放行 → handler 返回 domain 内知识
207
192
  res = SkillAdapter(reg).invoke(spec, {"query": "写作风格"})
208
193
  assert res.success and res.output.get("ability") == "writing_expert", res.to_dict()
209
194
  assert res.output["knowledge"] and res.output["knowledge"][0]["name"] == "修辞"
210
195
  print(" ✓ 证据 gate:达标能力注册为 read skill、覆盖不足不注册;经 PolicyGate 放行返回知识")
211
196
 
212
- # 降级:HKC 不可用 / registry 缺失 → 不注册、不崩
213
197
  assert register_abilities_as_skills(SkillRegistry(), None) == []
214
198
  assert register_abilities_as_skills(None, hkc) == []
215
199
  print(" ✓ HKC 不可用/registry 缺失 → 优雅返回 []")
@@ -222,7 +206,6 @@ def test_ability_skill_prune_and_concurrency():
222
206
  from foxclaw.knowledge import register_abilities_as_skills
223
207
  from foxclaw.skills import SkillRegistry, SkillSpec
224
208
 
225
- # (a) 陈旧清理:先达标注册 → 知识掉了不再达标 → re-sync 移除陈旧 ability skill
226
209
  class _Cdrop:
227
210
  def __init__(self): self.ok = True
228
211
  def list_abilities(self): return ["writing_expert"]
@@ -237,12 +220,11 @@ def test_ability_skill_prune_and_concurrency():
237
220
  reg = SkillRegistry()
238
221
  assert register_abilities_as_skills(reg, hkc) == ["ability:writing_expert"]
239
222
  assert reg.get("ability:writing_expert") is not None
240
- c.ok = False # 知识掉了 → 不再达标
223
+ c.ok = False
241
224
  assert register_abilities_as_skills(reg, hkc) == []
242
225
  assert reg.get("ability:writing_expert") is None, "陈旧 ability skill 应在 re-sync 时被移除"
243
226
  print(" ✓ re-sync:覆盖掉了的 ability skill 被清理(不残留)")
244
227
 
245
- # (b) 线程安全:并发 register + match/all/catalog 无 "dict changed size" 异常
246
228
  reg2 = SkillRegistry()
247
229
  errs = []
248
230
  def reg_worker():
@@ -24,7 +24,6 @@ import time
24
24
  from pathlib import Path
25
25
 
26
26
 
27
- # ── vendor 隔离硬阻断 ────────────────────────────────────────────
28
27
  from hkc_search.index_build_manager import IndexBuildManager, _BUILD_STATES
29
28
  from hkc_search.vector_search import VectorIndex
30
29
  from hkc_search.embedding_backends import StubBackend
@@ -55,8 +54,6 @@ for _cls_name, _cls in [
55
54
  _assert_vendor_path(_cls, _cls_name)
56
55
 
57
56
 
58
- # ── Helpers ─────────────────────────────────────────────────────
59
-
60
57
  def _seed_kus(c: HKCContainer, ids: list[str]) -> None:
61
58
  """直接 put 进 store,不发出 KNOWLEDGE_CREATED 事件。
62
59
  用于构造 '库有数据但 vector 没条' 的初始状态,触发 manager 全量构建。
@@ -94,16 +91,12 @@ def _seed_and_start_build(c: HKCContainer, ids: list[str]) -> None:
94
91
  """
95
92
  _seed_kus(c, ids)
96
93
  r = c.index_build_manager.start()
97
- # size>0 不应返 None(已加载的 faiss 有数据但库无相同数据时可能返 None;此测试场景是库空)
98
- # 这里 size=0 + source>0 → 必起构建 → start 返 task_id
99
94
  assert r is not None, (
100
95
  f"_seed_and_start_build 应触发新构建,但 start() 返 None。"
101
96
  f"当前 stats: {c.index_build_manager.stats()}"
102
97
  )
103
98
 
104
99
 
105
- # ── S1:无索引启动,manager 后台跑全量构建 ───────────────────
106
-
107
100
  class TestStartNoIndexManagerBuildsBackground:
108
101
  def test_assemble_returns_fast_manager_builds_then_ready(self, tmp_path):
109
102
  """库有 5 KU 但磁盘无索引 → seed + start → manager 后台构建;
@@ -121,7 +114,6 @@ class TestStartNoIndexManagerBuildsBackground:
121
114
  assert final["source_total"] == 5, final
122
115
  assert final["ready"] == 5, final
123
116
  assert final["failed"] == 0, final
124
- # 内存索引真存在
125
117
  assert c.search.vector.size() == 5
126
118
  finally:
127
119
  c.close()
@@ -133,7 +125,6 @@ class TestStartNoIndexManagerBuildsBackground:
133
125
  data_dir = str(tmp_path)
134
126
  c = init_container(data_dir=data_dir, embedding_kind="stub")
135
127
  try:
136
- # 装配已经返回(库空,manager 立即 idle)。seed + 重 start 让它编码。
137
128
  _seed_and_start_build(c, [f"KU_FAST_{i}" for i in range(2)])
138
129
  s = c.index_build_manager.stats()
139
130
  assert s["index_state"] in _BUILD_STATES
@@ -145,14 +136,11 @@ class TestStartNoIndexManagerBuildsBackground:
145
136
  reset_container()
146
137
 
147
138
 
148
- # ── S2:旧快照启动(磁盘 faiss 已完整) → 不重起构建 ──────────
149
-
150
139
  class TestStartWithPersistedSnapshotNoRebuild:
151
140
  def test_manager_skips_when_disk_snapshot_covers_source(self, tmp_path):
152
141
  """先启动一次让 manager 构建完 → close;重启 → manager 看到 faiss 已非空
153
142
  → start() 返 None,不再构建。"""
154
143
  data_dir = str(tmp_path)
155
- # 第一轮:seed + start → manager 构建 → 落盘
156
144
  c = init_container(data_dir=data_dir, embedding_kind="stub")
157
145
  try:
158
146
  _seed_and_start_build(c, [f"KU_S2A_{i}" for i in range(4)])
@@ -162,7 +150,6 @@ class TestStartWithPersistedSnapshotNoRebuild:
162
150
  c.close()
163
151
  reset_container()
164
152
 
165
- # 第二轮:simulate 重启 —— 装配后,manager.start() 应返 None (已有快照)
166
153
  c2 = init_container(data_dir=data_dir, embedding_kind="stub")
167
154
  try:
168
155
  s = c2.index_build_manager.stats()
@@ -172,7 +159,6 @@ class TestStartWithPersistedSnapshotNoRebuild:
172
159
  assert s["vector_size"] == 4, (
173
160
  f"vector_size 应从磁盘恢复,实际 {s}"
174
161
  )
175
- # 显式再调 start() 也返 None(幂等)
176
162
  r = c2.index_build_manager.start()
177
163
  assert r is None, (
178
164
  f"已有快照时 start() 应返 None,实际 {r!r}"
@@ -182,8 +168,6 @@ class TestStartWithPersistedSnapshotNoRebuild:
182
168
  reset_container()
183
169
 
184
170
 
185
- # ── S3:构建中重启 → 续跑 ───────────────────────────────
186
-
187
171
  class TestMidBuildRestartPicksUp:
188
172
  def test_resume_after_disk_vector_partial(self, tmp_path):
189
173
  """模拟 '上次构建到一半被杀掉':磁盘 faiss 有 5 条全量,manager 看到库空
@@ -192,7 +176,6 @@ class TestMidBuildRestartPicksUp:
192
176
  但因为 manager 的 start() 总是基于「当前 vector vs 当前库」做决策,
193
177
  磁盘有 5 条 + 库 5 条相同 → 跳过(等价于 S2 的 happy path)。"""
194
178
  data_dir = str(tmp_path)
195
- # 第一轮:build 5 条 → save → close
196
179
  c = init_container(data_dir=data_dir, embedding_kind="stub")
197
180
  try:
198
181
  _seed_and_start_build(c, [f"KU_S3_{i}" for i in range(5)])
@@ -202,16 +185,12 @@ class TestMidBuildRestartPicksUp:
202
185
  c.close()
203
186
  reset_container()
204
187
 
205
- # 模拟"上次只跑了部分,faiss 是部分加载的" — 删 vector 文件(变 0 条)
206
- # manager 装配时会 _load_persisted_index_and_bm25 → load 失败(因为已被我们删)→ 0 条
207
- # 然后 manager.start() 看到 size=0 < source=5 → 起新构建
208
188
  vec_path = tmp_path / "search_vector"
209
189
  if (vec_path.with_suffix(".faiss")).exists():
210
190
  (vec_path.with_suffix(".faiss")).unlink()
211
191
  if (vec_path.with_suffix(".meta")).exists():
212
192
  (vec_path.with_suffix(".meta")).unlink()
213
193
 
214
- # 写 state.json 模拟"上次未完成",确认 manager.start 决策时能读到历史
215
194
  import json as _json
216
195
  state = {
217
196
  "task_id": "build-deadbeef",
@@ -232,10 +211,8 @@ class TestMidBuildRestartPicksUp:
232
211
  encoding="utf-8",
233
212
  )
234
213
 
235
- # 第二轮:重启
236
214
  c2 = init_container(data_dir=data_dir, embedding_kind="stub")
237
215
  try:
238
- # 装配已自动 start → 应启构建(因为 faiss 已删)
239
216
  final = _wait_idle(c2, timeout=30)
240
217
  assert final["index_state"] == "ready", (
241
218
  f"重启后应到 ready,实际 {final}"
@@ -243,7 +220,6 @@ class TestMidBuildRestartPicksUp:
243
220
  assert c2.search.vector.size() == 5, (
244
221
  f"重启后 vector_size 应=5,实际 {c2.search.vector.size()}"
245
222
  )
246
- # 跑出了新 task_id(不是历史的 deadbeef)
247
223
  assert final["task_id"] != "build-deadbeef", (
248
224
  f"应起新 task_id,实际仍用历史的 {final['task_id']!r}"
249
225
  )
@@ -252,8 +228,6 @@ class TestMidBuildRestartPicksUp:
252
228
  reset_container()
253
229
 
254
230
 
255
- # ── S4:构建中查询 ──────────────────────────────────────────
256
-
257
231
  class TestSearchDuringBuild:
258
232
  def test_concurrent_search_does_not_crash(self, tmp_path):
259
233
  """manager 后台跑编码的同时,前调 c.search.search() → 不抛错;
@@ -262,13 +236,11 @@ class TestSearchDuringBuild:
262
236
  c = init_container(data_dir=data_dir, embedding_kind="stub")
263
237
  try:
264
238
  _seed_and_start_build(c, [f"KU_S4_{i}" for i in range(6)])
265
- # 不等;立即并发调 search
266
239
  results = c.search.search("ku_1 描述", mode="hybrid", top_k=3)
267
240
  assert isinstance(results, list)
268
241
  for r in results:
269
242
  assert hasattr(r, "ku_id")
270
243
  assert hasattr(r, "score")
271
- # 等构建完再做一次 search 确认 6 条都被 indexed
272
244
  final = _wait_idle(c, timeout=30)
273
245
  assert final["index_state"] == "ready"
274
246
  assert c.search.vector.size() == 6
@@ -279,8 +251,6 @@ class TestSearchDuringBuild:
279
251
  reset_container()
280
252
 
281
253
 
282
- # ── 单元测:IndexBuildManager 状态机字段 ─────────────────
283
-
284
254
  class TestManagerStats:
285
255
  def test_stats_keys_present(self, tmp_path):
286
256
  """stats 字典字段稳定性(对外观测前端要靠这些 key)。"""
@@ -307,14 +277,11 @@ class TestManagerStats:
307
277
  )
308
278
  mgr.start()
309
279
  mgr.join(timeout=5)
310
- # 空 store 扫描到 0 条 → 直接 _mark_ready
311
280
  s = mgr.stats()
312
281
  assert s["index_state"] in ("ready", "cancelled", "failed"), s
313
282
  assert s["source_total"] == 0, s
314
283
 
315
284
 
316
- # ── Fakes(单元测用) ─────────────────────────────────────
317
-
318
285
  class _FakeHybrid:
319
286
  """空 hybrid:vector_size=0,_ku_ids 空列表,_lock 是 RLock,vector 假实例。"""
320
287
  def __init__(self):
@@ -35,7 +35,6 @@ import faiss
35
35
  import numpy as np
36
36
  import pytest
37
37
 
38
- # ── vendor 隔离硬阻断(等价于 test_vendor_path_isolation.py 闸门 2-4) ────────
39
38
  from hkc_search.vector_search import (
40
39
  VectorIndex,
41
40
  _MANIFEST_FORMAT_VERSION,
@@ -68,8 +67,6 @@ for _cls_name, _cls in [
68
67
  _assert_vendor_path(_cls, _cls_name)
69
68
 
70
69
 
71
- # ── Fixtures ────────────────────────────────────────────────────────
72
-
73
70
  @pytest.fixture
74
71
  def stub_vector(tmp_path) -> VectorIndex:
75
72
  """已 build 3 条 KU 的 vector,save 后留磁盘文件。"""
@@ -96,23 +93,18 @@ def _ku_text(ku) -> str:
96
93
  return " ".join(parts)
97
94
 
98
95
 
99
- # ── M1: roundtrip(全过) ───────────────────────────────────────────
100
-
101
96
  class TestManifestRoundtrip:
102
97
  def test_save_load_roundtrip(self, tmp_path, stub_vector):
103
98
  """save → load 完整,数据一致(ku_ids/names/summaries/text_hashes 全部保留)。"""
104
99
  vec_path = str(tmp_path / "search_vector")
105
100
  stub_vector.save(vec_path)
106
- # 落盘了 v2 manifest
107
101
  meta = pickle.loads((Path(vec_path + ".meta")).read_bytes())
108
102
  assert meta["format_version"] == _MANIFEST_FORMAT_VERSION == 2
109
103
  assert meta["ku_text_version"] == _KU_TEXT_FORMAT_VERSION == 1
110
- # backend 字段以"Stub("开头(StubBackend.name 形如 "Stub(dim=384)")
111
104
  assert meta["backend"].startswith("Stub("), meta["backend"]
112
105
  assert meta["dim"] == stub_vector.dim
113
106
  assert len(meta["ku_ids"]) == 3
114
107
  assert len(meta["text_hashes"]) == 3
115
- # 加载
116
108
  fresh = VectorIndex(backend=StubBackend())
117
109
  assert fresh.load(vec_path) is True
118
110
  assert fresh.size() == 3
@@ -138,13 +130,11 @@ class TestManifestRoundtrip:
138
130
  """同文本 → 同 hash(阶段 B 跨进程比对、阶段 C 续跑检测都要靠这个稳定性)。"""
139
131
  a = _hash_text("alpha 一种希腊字母")
140
132
  b = _hash_text("alpha 一种希腊字母")
141
- c = _hash_text("alpha 一种希腊字母 ") # 末尾空格,文本不同
133
+ c = _hash_text("alpha 一种希腊字母 ")
142
134
  assert a == b
143
135
  assert a != c
144
136
 
145
137
 
146
- # ── M2~M9: 8 步校验各失败路径 ───────────────────────────────────
147
-
148
138
  class TestManifestValidation:
149
139
  def _fresh_v2_disk(self, tmp_path) -> str:
150
140
  """构造一份合法的 v2 磁盘,返回 prefix path。"""
@@ -159,14 +149,11 @@ class TestManifestValidation:
159
149
  def test_v1_manifest_no_format_version_resets(self, tmp_path):
160
150
  """M2:旧 v1 manifest(无 format_version 字段)→ _reset。"""
161
151
  vec_path = self._fresh_v2_disk(tmp_path)
162
- # 改写 meta 为 v1:仅 ku_ids/names/summaries/backend,无 format_version/dim/text_hashes
163
152
  with open(vec_path + ".meta", "wb") as f:
164
153
  pickle.dump({"ku_ids": ["X"], "names": ["x"],
165
154
  "summaries": ["x"], "backend": "stub"}, f)
166
155
  v = VectorIndex(backend=StubBackend())
167
- # 加载必失败 → 返 False
168
156
  assert v.load(vec_path) is False
169
- # _reset 已清空
170
157
  assert v.size() == 0
171
158
  assert v._ku_ids == []
172
159
  assert v._text_hashes == []
@@ -224,7 +211,7 @@ class TestManifestValidation:
224
211
  vec_path = self._fresh_v2_disk(tmp_path)
225
212
  with open(vec_path + ".meta", "rb") as f:
226
213
  meta = pickle.load(f)
227
- meta["text_hashes"] = meta["text_hashes"][:1] # 只留 1 个,与 2 条 ku_ids 不一致
214
+ meta["text_hashes"] = meta["text_hashes"][:1]
228
215
  with open(vec_path + ".meta", "wb") as f:
229
216
  pickle.dump(meta, f)
230
217
  v = VectorIndex(backend=StubBackend())
@@ -233,8 +220,7 @@ class TestManifestValidation:
233
220
  def test_faiss_dim_mismatch_resets(self, tmp_path):
234
221
  """M8:faiss 实际维度与 meta["dim"] 不一致(切后端后常见)→ 校验失败。"""
235
222
  vec_path = self._fresh_v2_disk(tmp_path)
236
- # 重建一个维度错的 faiss 文件
237
- wrong_idx = faiss.IndexFlatIP(999) # 错维度
223
+ wrong_idx = faiss.IndexFlatIP(999)
238
224
  wrong_idx.add(np.random.rand(2, 999).astype("float32"))
239
225
  faiss.write_index(wrong_idx, vec_path + ".faiss")
240
226
  v = VectorIndex(backend=StubBackend())
@@ -244,7 +230,6 @@ class TestManifestValidation:
244
230
  def test_faiss_ntotal_mismatch_resets(self, tmp_path):
245
231
  """M9:faiss 实际 ntotal 与 meta len(ku_ids) 不一致 → 校验失败。"""
246
232
  vec_path = self._fresh_v2_disk(tmp_path)
247
- # 重建一个 ntotal=99 的 faiss(其他字段不动)
248
233
  wrong_idx = faiss.IndexFlatIP(stub_vector_dim := VectorIndex(backend=StubBackend()).dim)
249
234
  wrong_idx.add(np.random.rand(99, stub_vector_dim).astype("float32"))
250
235
  faiss.write_index(wrong_idx, vec_path + ".faiss")
@@ -266,8 +251,7 @@ class TestManifestValidation:
266
251
  v = VectorIndex(backend=StubBackend())
267
252
  kus = [ConceptKU(ku_id="KU_A", name="a", summary="a", domain="d")]
268
253
  v.build(kus)
269
- # 故意破坏 invariant:删一个 text_hash
270
- v._text_hashes = v._text_hashes[:0] # 现在 _text_hashes=[] 但 _ku_ids=[KU_A]
254
+ v._text_hashes = v._text_hashes[:0]
271
255
  with pytest.raises(AssertionError):
272
256
  v.save(str(tmp_path / "search_vector"))
273
257
 
@@ -280,12 +264,6 @@ class TestManifestValidation:
280
264
  assert "foo msg" in str(e)
281
265
 
282
266
 
283
- # ── B1~B4: bm25 0 corpus 保护 ────────────────────────────────────
284
- # B 阶段把 BM25Index 重写为自实现倒排索引(不再包装 rank_bm25),旧私有属性
285
- # `_bm25` / `_ku_ids` / `_names` / `_summaries` / `_corpus` / `_rebuild_bm25`
286
- # 均已不存在。故断言改为验证**对外行为**(size / search),不再耦合私有结构 ——
287
- # 旧测试直接拼私有 list 再调私有 rebuild,是它在此次重构后失效的根因。
288
-
289
267
  class TestBM25EmptyCorpus:
290
268
  def test_build_empty_list(self):
291
269
  """B1:build([]) → 索引为空,search 返 []。"""
@@ -313,5 +291,4 @@ class TestBM25EmptyCorpus:
313
291
  def test_search_with_no_bm25(self):
314
292
  """B4:未 build 时 search 返 []。"""
315
293
  b = BM25Index()
316
- # 不 build,直接 search
317
294
  assert b.search("anything") == []