foxclaw 0.1.0 → 0.1.3

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 (514) hide show
  1. package/README.md +54 -29
  2. package/bin/foxclaw.cjs +51 -0
  3. package/index.js +4 -10
  4. package/package.json +46 -38
  5. package/python/.dockerignore +35 -0
  6. package/python/.env.example +173 -0
  7. package/python/.gitattributes +46 -0
  8. package/python/.github/workflows/ci.yml +167 -0
  9. package/python/.github/workflows/release.yml +183 -0
  10. package/python/CHANGELOG.md +57 -0
  11. package/python/CHANGELOG_CN.md +57 -0
  12. package/python/Dockerfile +64 -0
  13. package/python/LICENSE +21 -0
  14. package/python/MANIFEST.in +14 -0
  15. package/python/README.md +201 -0
  16. package/python/README_CN.md +336 -0
  17. package/python/bench/bm25_audit.py +186 -0
  18. package/python/bench/bm25_regression.py +177 -0
  19. package/python/bench/bm25_stress.py +155 -0
  20. package/python/bench/cd_audit.py +215 -0
  21. package/python/bench/chaos.py +140 -0
  22. package/python/bench/concurrency.py +103 -0
  23. package/python/bench/e2e_real_models.py +94 -0
  24. package/python/bench/llm_retry_test.py +241 -0
  25. package/python/bench/load_test.py +193 -0
  26. package/python/bench/vector_ivf_upgrade.py +128 -0
  27. package/python/docs/CHANNELS.md +195 -0
  28. package/python/docs/GOVERNANCE.md +164 -0
  29. package/python/docs/RELEASE.md +110 -0
  30. package/python/docs/RUN.md +75 -0
  31. package/python/docs/RUN_CN.md +121 -0
  32. package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
  33. package/python/evals/__init__.py +0 -0
  34. package/python/evals/governance/README.md +61 -0
  35. package/python/evals/governance/__init__.py +0 -0
  36. package/python/evals/governance/cases.py +115 -0
  37. package/python/evals/governance/compare_embeddings.py +94 -0
  38. package/python/evals/governance/convergence.svg +11 -0
  39. package/python/evals/governance/report.json +371 -0
  40. package/python/evals/governance/run_evals.py +368 -0
  41. package/python/examples/governance_middleware_demo.py +72 -0
  42. package/python/examples/langgraph_governance_demo.py +104 -0
  43. package/python/examples/sample_itinerary.csv +6 -0
  44. package/python/examples/sample_knowledge.csv +9 -0
  45. package/python/examples/sample_transactions.csv +14 -0
  46. package/python/foxclaw/__init__.py +408 -0
  47. package/python/foxclaw/agents/__init__.py +12 -0
  48. package/python/foxclaw/agents/agents.py +35 -0
  49. package/python/foxclaw/agents/base.py +794 -0
  50. package/python/foxclaw/agents/impl/__init__.py +1 -0
  51. package/python/foxclaw/agents/impl/_helpers.py +70 -0
  52. package/python/foxclaw/agents/impl/finance.py +231 -0
  53. package/python/foxclaw/agents/impl/office.py +350 -0
  54. package/python/foxclaw/agents/impl/supervisor.py +592 -0
  55. package/python/foxclaw/agents/mixins/_context.py +128 -0
  56. package/python/foxclaw/agents/mixins/augment.py +143 -0
  57. package/python/foxclaw/agents/mixins/llm.py +67 -0
  58. package/python/foxclaw/agents/mixins/memory.py +41 -0
  59. package/python/foxclaw/agents/mixins/preference.py +131 -0
  60. package/python/foxclaw/agents/mixins/prompt.py +71 -0
  61. package/python/foxclaw/agents/mixins/skillbox.py +58 -0
  62. package/python/foxclaw/agents/mixins/tool_exec.py +417 -0
  63. package/python/foxclaw/api/__init__.py +224 -0
  64. package/python/foxclaw/api/_state.py +43 -0
  65. package/python/foxclaw/api/bootstrap.py +96 -0
  66. package/python/foxclaw/api/middleware.py +294 -0
  67. package/python/foxclaw/api/routes/ahs.py +404 -0
  68. package/python/foxclaw/api/routes/attachments.py +116 -0
  69. package/python/foxclaw/api/routes/browser.py +35 -0
  70. package/python/foxclaw/api/routes/domain.py +306 -0
  71. package/python/foxclaw/api/routes/knowledge.py +145 -0
  72. package/python/foxclaw/api/routes/llm_admin.py +449 -0
  73. package/python/foxclaw/api/routes/memory.py +105 -0
  74. package/python/foxclaw/api/routes/orchestration.py +39 -0
  75. package/python/foxclaw/api/routes/recovery.py +44 -0
  76. package/python/foxclaw/api/routes/review.py +266 -0
  77. package/python/foxclaw/api/routes/search.py +33 -0
  78. package/python/foxclaw/api/routes/system.py +311 -0
  79. package/python/foxclaw/api/routes/task.py +263 -0
  80. package/python/foxclaw/api/routes/ws.py +64 -0
  81. package/python/foxclaw/api/spine.py +52 -0
  82. package/python/foxclaw/browser/__init__.py +13 -0
  83. package/python/foxclaw/browser/action/__init__.py +1 -0
  84. package/python/foxclaw/browser/action/action_executor.py +50 -0
  85. package/python/foxclaw/browser/action/action_types.py +44 -0
  86. package/python/foxclaw/browser/action/action_verifier.py +68 -0
  87. package/python/foxclaw/browser/action/url_policy.py +143 -0
  88. package/python/foxclaw/browser/adapter/__init__.py +1 -0
  89. package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
  90. package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
  91. package/python/foxclaw/browser/driver/__init__.py +1 -0
  92. package/python/foxclaw/browser/driver/base_driver.py +61 -0
  93. package/python/foxclaw/browser/driver/playwright_driver.py +170 -0
  94. package/python/foxclaw/browser/perception/__init__.py +1 -0
  95. package/python/foxclaw/browser/perception/dom_extractor.py +42 -0
  96. package/python/foxclaw/browser/perception/element_indexer.py +56 -0
  97. package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
  98. package/python/foxclaw/browser/perception/snapshot.py +29 -0
  99. package/python/foxclaw/browser/service.py +144 -0
  100. package/python/foxclaw/browser/session/__init__.py +1 -0
  101. package/python/foxclaw/browser/session/session_manager.py +57 -0
  102. package/python/foxclaw/capability/__init__.py +12 -0
  103. package/python/foxclaw/capability/embedding_service.py +65 -0
  104. package/python/foxclaw/channels/__init__.py +138 -0
  105. package/python/foxclaw/channels/discord.py +175 -0
  106. package/python/foxclaw/channels/dispatcher.py +179 -0
  107. package/python/foxclaw/channels/feishu.py +215 -0
  108. package/python/foxclaw/channels/openclaw_bridge.py +151 -0
  109. package/python/foxclaw/channels/signing.py +187 -0
  110. package/python/foxclaw/channels/telegram.py +120 -0
  111. package/python/foxclaw/channels/weixin.py +215 -0
  112. package/python/foxclaw/cli.py +183 -0
  113. package/python/foxclaw/cognitive/__init__.py +4 -0
  114. package/python/foxclaw/cognitive/reasoner.py +81 -0
  115. package/python/foxclaw/cognitive/state.py +141 -0
  116. package/python/foxclaw/core/__init__.py +9 -0
  117. package/python/foxclaw/core/audit_chain.py +131 -0
  118. package/python/foxclaw/core/audit_sink.py +100 -0
  119. package/python/foxclaw/core/compaction.py +265 -0
  120. package/python/foxclaw/core/config.py +65 -0
  121. package/python/foxclaw/core/confirm_nonce.py +151 -0
  122. package/python/foxclaw/core/dispatch.py +27 -0
  123. package/python/foxclaw/core/envfile.py +48 -0
  124. package/python/foxclaw/core/failure_policy.py +142 -0
  125. package/python/foxclaw/core/gateway.py +81 -0
  126. package/python/foxclaw/core/governance_audit.py +171 -0
  127. package/python/foxclaw/core/idempotency.py +282 -0
  128. package/python/foxclaw/core/injection_guard.py +202 -0
  129. package/python/foxclaw/core/memory_adapter.py +489 -0
  130. package/python/foxclaw/core/metrics.py +88 -0
  131. package/python/foxclaw/core/model_router.py +985 -0
  132. package/python/foxclaw/core/paths.py +103 -0
  133. package/python/foxclaw/core/recovery.py +349 -0
  134. package/python/foxclaw/core/scheduler.py +141 -0
  135. package/python/foxclaw/core/secrets.py +131 -0
  136. package/python/foxclaw/core/sessions.py +155 -0
  137. package/python/foxclaw/core/settings.py +414 -0
  138. package/python/foxclaw/core/shared_write.py +186 -0
  139. package/python/foxclaw/core/steering_queue.py +154 -0
  140. package/python/foxclaw/core/system_bus.py +84 -0
  141. package/python/foxclaw/core/thought_chain.py +316 -0
  142. package/python/foxclaw/core/token_meter.py +231 -0
  143. package/python/foxclaw/core/tools.py +164 -0
  144. package/python/foxclaw/core/tracing.py +375 -0
  145. package/python/foxclaw/core/truth_pipeline.py +110 -0
  146. package/python/foxclaw/core/update_check.py +149 -0
  147. package/python/foxclaw/core/vec_recall_adapter.py +477 -0
  148. package/python/foxclaw/core/verify.py +173 -0
  149. package/python/foxclaw/core/web_search.py +218 -0
  150. package/python/foxclaw/finance/__init__.py +15 -0
  151. package/python/foxclaw/finance/evidence.py +149 -0
  152. package/python/foxclaw/finance/fx.py +98 -0
  153. package/python/foxclaw/finance/graph.py +179 -0
  154. package/python/foxclaw/finance/models.py +146 -0
  155. package/python/foxclaw/finance/nlparse.py +515 -0
  156. package/python/foxclaw/finance/policy.py +301 -0
  157. package/python/foxclaw/finance/reasoner.py +121 -0
  158. package/python/foxclaw/finance/reasoning.py +319 -0
  159. package/python/foxclaw/finance/reconcile.py +175 -0
  160. package/python/foxclaw/finance/runtime.py +500 -0
  161. package/python/foxclaw/governance/__init__.py +24 -0
  162. package/python/foxclaw/governance/middleware.py +176 -0
  163. package/python/foxclaw/harness/__init__.py +17 -0
  164. package/python/foxclaw/harness/_base.py +66 -0
  165. package/python/foxclaw/harness/_policy.py +340 -0
  166. package/python/foxclaw/harness/drift.py +77 -0
  167. package/python/foxclaw/harness/event_bus.py +28 -0
  168. package/python/foxclaw/harness/harness.py +358 -0
  169. package/python/foxclaw/harness/loop_detection.py +145 -0
  170. package/python/foxclaw/itinerary/__init__.py +10 -0
  171. package/python/foxclaw/itinerary/amap_client.py +114 -0
  172. package/python/foxclaw/itinerary/models.py +80 -0
  173. package/python/foxclaw/itinerary/reasoner.py +96 -0
  174. package/python/foxclaw/itinerary/reasoning.py +168 -0
  175. package/python/foxclaw/itinerary/runtime.py +144 -0
  176. package/python/foxclaw/kernel/__init__.py +13 -0
  177. package/python/foxclaw/kernel/capabilities.py +72 -0
  178. package/python/foxclaw/kernel/policy_gate.py +108 -0
  179. package/python/foxclaw/kernel/process_manager.py +186 -0
  180. package/python/foxclaw/kernel/scheduler.py +125 -0
  181. package/python/foxclaw/kernel/sentinel.py +443 -0
  182. package/python/foxclaw/kernel/sentinel_client.py +243 -0
  183. package/python/foxclaw/kernel/spine.py +97 -0
  184. package/python/foxclaw/knowledge/__init__.py +360 -0
  185. package/python/foxclaw/knowledge/truth_crystallizer.py +89 -0
  186. package/python/foxclaw/learning/__init__.py +12 -0
  187. package/python/foxclaw/learning/knowledge_graph.py +144 -0
  188. package/python/foxclaw/learning/models.py +93 -0
  189. package/python/foxclaw/learning/reasoner.py +67 -0
  190. package/python/foxclaw/learning/runtime.py +351 -0
  191. package/python/foxclaw/learning/spaced_repetition.py +91 -0
  192. package/python/foxclaw/mcp_server.py +188 -0
  193. package/python/foxclaw/orchestration/__init__.py +167 -0
  194. package/python/foxclaw/pipeline/__init__.py +23 -0
  195. package/python/foxclaw/pipeline/cognitive_pipeline.py +429 -0
  196. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  197. package/python/foxclaw/pipeline/supervisor_stages.py +201 -0
  198. package/python/foxclaw/planning/__init__.py +17 -0
  199. package/python/foxclaw/planning/llm_planner.py +232 -0
  200. package/python/foxclaw/planning/planner.py +94 -0
  201. package/python/foxclaw/search/__init__.py +24 -0
  202. package/python/foxclaw/search/agent_port.py +190 -0
  203. package/python/foxclaw/search/evidence.py +109 -0
  204. package/python/foxclaw/search/external_search.py +50 -0
  205. package/python/foxclaw/search/hkc_read_port.py +60 -0
  206. package/python/foxclaw/search/internal_search.py +71 -0
  207. package/python/foxclaw/search/models.py +127 -0
  208. package/python/foxclaw/search/planner.py +44 -0
  209. package/python/foxclaw/search/retriever.py +39 -0
  210. package/python/foxclaw/search/runtime.py +127 -0
  211. package/python/foxclaw/search/strategy.py +44 -0
  212. package/python/foxclaw/skills/__init__.py +559 -0
  213. package/python/foxclaw/skills/archive.py +111 -0
  214. package/python/foxclaw/skills/fetch.py +226 -0
  215. package/python/foxclaw/skills/hooks_spec.py +69 -0
  216. package/python/foxclaw/skills/install_cmd.py +216 -0
  217. package/python/foxclaw/skills/lifecycle.py +252 -0
  218. package/python/foxclaw/skills/markdown_skill.py +175 -0
  219. package/python/foxclaw/skills/markdown_store.py +39 -0
  220. package/python/foxclaw/skills/mcp_client.py +184 -0
  221. package/python/foxclaw/skills/mcp_store.py +40 -0
  222. package/python/foxclaw/skills/permission.py +145 -0
  223. package/python/foxclaw/skills/pipeline.py +354 -0
  224. package/python/foxclaw/skills/policy.py +74 -0
  225. package/python/foxclaw/skills/review.py +72 -0
  226. package/python/foxclaw/skills/store.py +57 -0
  227. package/python/frontend/assets/core_bg.jpg +0 -0
  228. package/python/frontend/deck/auth.js +25 -0
  229. package/python/frontend/deck/chat_model_picker.js +483 -0
  230. package/python/frontend/deck/domain.js +276 -0
  231. package/python/frontend/deck/main.js +1806 -0
  232. package/python/frontend/deck/markdown.js +44 -0
  233. package/python/frontend/deck/settings.js +307 -0
  234. package/python/frontend/deck/state.js +5 -0
  235. package/python/frontend/deck/util.js +4 -0
  236. package/python/frontend/deck/visuals.js +190 -0
  237. package/python/frontend/deck.css +855 -0
  238. package/python/frontend/deck.html +686 -0
  239. package/python/frontend/deck_v2.html +341 -0
  240. package/python/frontend/planet/index.html +266 -0
  241. package/python/frontend/planet/planet.css +579 -0
  242. package/python/frontend/planet/planet.js +1436 -0
  243. package/python/frontend/planet/vendor/three.core.min.js +6 -0
  244. package/python/frontend/planet/vendor/three.module.min.js +6 -0
  245. package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
  246. package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
  247. package/python/frontend/v3/index.html +15 -0
  248. package/python/frontend/widget_runtime.js +252 -0
  249. package/python/frontend/widgets.js +639 -0
  250. package/python/frontend_src/index.html +14 -0
  251. package/python/frontend_src/package-lock.json +1376 -0
  252. package/python/frontend_src/package.json +19 -0
  253. package/python/frontend_src/src/App.vue +58 -0
  254. package/python/frontend_src/src/api.js +82 -0
  255. package/python/frontend_src/src/components/ChatDock.vue +365 -0
  256. package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
  257. package/python/frontend_src/src/components/DocViewer.vue +162 -0
  258. package/python/frontend_src/src/components/Inspector.vue +235 -0
  259. package/python/frontend_src/src/components/LeftRail.vue +415 -0
  260. package/python/frontend_src/src/components/MenuPopover.vue +128 -0
  261. package/python/frontend_src/src/components/MessageStream.vue +389 -0
  262. package/python/frontend_src/src/components/ModelPicker.vue +350 -0
  263. package/python/frontend_src/src/components/QuickIngest.vue +112 -0
  264. package/python/frontend_src/src/components/Stage.vue +272 -0
  265. package/python/frontend_src/src/components/ToolMenu.vue +141 -0
  266. package/python/frontend_src/src/components/TopBar.vue +118 -0
  267. package/python/frontend_src/src/components/panels/ActivityPanel.vue +82 -0
  268. package/python/frontend_src/src/components/panels/AgentsPanel.vue +83 -0
  269. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +425 -0
  270. package/python/frontend_src/src/components/panels/MemoryPanel.vue +117 -0
  271. package/python/frontend_src/src/components/panels/PanelBox.vue +50 -0
  272. package/python/frontend_src/src/components/panels/ReviewPanel.vue +292 -0
  273. package/python/frontend_src/src/components/panels/SettingsPanel.vue +358 -0
  274. package/python/frontend_src/src/components/panels/TasksPanel.vue +73 -0
  275. package/python/frontend_src/src/components/panels/ToolsPanel.vue +746 -0
  276. package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
  277. package/python/frontend_src/src/lib/fmt.js +44 -0
  278. package/python/frontend_src/src/lib/i18n.js +53 -0
  279. package/python/frontend_src/src/lib/locales/en.js +173 -0
  280. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  281. package/python/frontend_src/src/lib/md.js +190 -0
  282. package/python/frontend_src/src/main.js +9 -0
  283. package/python/frontend_src/src/stores/app.js +91 -0
  284. package/python/frontend_src/src/stores/chat.js +450 -0
  285. package/python/frontend_src/src/stores/model.js +96 -0
  286. package/python/frontend_src/src/stores/panels.js +289 -0
  287. package/python/frontend_src/src/stores/profile.js +31 -0
  288. package/python/frontend_src/src/stores/system.js +25 -0
  289. package/python/frontend_src/src/styles/base.css +138 -0
  290. package/python/frontend_src/src/styles/panel.css +117 -0
  291. package/python/frontend_src/src/styles/tokens.css +78 -0
  292. package/python/frontend_src/vite.config.js +39 -0
  293. package/python/pyproject.toml +268 -0
  294. package/python/requirements.txt +311 -0
  295. package/python/skills/README.md +123 -0
  296. package/python/skills/execution-contract/SKILL.md +30 -0
  297. package/python/skills/general-assistant/SKILL.md +34 -0
  298. package/python/skills/planner/SKILL.md +33 -0
  299. package/python/skills/schedule-itinerary/SKILL.md +35 -0
  300. package/python/skills/skill-composition/SKILL.md +31 -0
  301. package/python/skills/skill-guard/SKILL.md +67 -0
  302. package/python/skills/skill-memory/SKILL.md +50 -0
  303. package/python/skills/skill-router/SKILL.md +35 -0
  304. package/python/tests/conftest.py +54 -0
  305. package/python/tests/run_all.py +200 -0
  306. package/python/tests/test_agent_lifecycle.py +326 -0
  307. package/python/tests/test_ahs_p2.py +393 -0
  308. package/python/tests/test_anthropic_tools.py +192 -0
  309. package/python/tests/test_api.py +97 -0
  310. package/python/tests/test_api_auth.py +79 -0
  311. package/python/tests/test_api_host_guard.py +69 -0
  312. package/python/tests/test_attachments.py +151 -0
  313. package/python/tests/test_audit_chain.py +116 -0
  314. package/python/tests/test_audit_sink.py +130 -0
  315. package/python/tests/test_browser.py +149 -0
  316. package/python/tests/test_browser_request_guard.py +180 -0
  317. package/python/tests/test_capabilities.py +152 -0
  318. package/python/tests/test_channel_signing.py +185 -0
  319. package/python/tests/test_channel_trust.py +109 -0
  320. package/python/tests/test_channels.py +174 -0
  321. package/python/tests/test_cli.py +46 -0
  322. package/python/tests/test_compaction_toolpair.py +119 -0
  323. package/python/tests/test_config_layering.py +61 -0
  324. package/python/tests/test_confirm_nonce.py +186 -0
  325. package/python/tests/test_context_compaction_wiring.py +124 -0
  326. package/python/tests/test_dispatch.py +89 -0
  327. package/python/tests/test_domain_evals.py +334 -0
  328. package/python/tests/test_domain_import.py +72 -0
  329. package/python/tests/test_domain_reasoners.py +134 -0
  330. package/python/tests/test_e2e.py +161 -0
  331. package/python/tests/test_emb_cache.py +97 -0
  332. package/python/tests/test_embedding_singleton.py +87 -0
  333. package/python/tests/test_envfile.py +83 -0
  334. package/python/tests/test_evals_smoke.py +90 -0
  335. package/python/tests/test_event_spine.py +174 -0
  336. package/python/tests/test_evidence_semantic.py +88 -0
  337. package/python/tests/test_evidence_sse.py +94 -0
  338. package/python/tests/test_file_ingest.py +135 -0
  339. package/python/tests/test_finance.py +144 -0
  340. package/python/tests/test_finance_ledger.py +245 -0
  341. package/python/tests/test_finance_persist.py +174 -0
  342. package/python/tests/test_gateway.py +125 -0
  343. package/python/tests/test_governance.py +421 -0
  344. package/python/tests/test_governance_audit.py +324 -0
  345. package/python/tests/test_governance_middleware.py +137 -0
  346. package/python/tests/test_hkc.py +268 -0
  347. package/python/tests/test_hkc_index_build_manager.py +306 -0
  348. package/python/tests/test_hkc_manifest_reuse.py +294 -0
  349. package/python/tests/test_hkc_search_event_sync.py +482 -0
  350. package/python/tests/test_import_path_guard.py +155 -0
  351. package/python/tests/test_injection_guard.py +193 -0
  352. package/python/tests/test_integration.py +117 -0
  353. package/python/tests/test_itinerary.py +100 -0
  354. package/python/tests/test_kee_semantic.py +84 -0
  355. package/python/tests/test_kernel.py +212 -0
  356. package/python/tests/test_learning.py +101 -0
  357. package/python/tests/test_llm_admin.py +145 -0
  358. package/python/tests/test_llm_planner.py +344 -0
  359. package/python/tests/test_markdown_skills.py +62 -0
  360. package/python/tests/test_markdown_skills_openclaw.py +66 -0
  361. package/python/tests/test_mcp_connectors.py +97 -0
  362. package/python/tests/test_mcp_guardian_discovery.py +87 -0
  363. package/python/tests/test_mcp_no_memory.py +43 -0
  364. package/python/tests/test_mcp_policy_gate.py +76 -0
  365. package/python/tests/test_mcp_server.py +92 -0
  366. package/python/tests/test_memory_recall.py +92 -0
  367. package/python/tests/test_metrics.py +58 -0
  368. package/python/tests/test_multi_chain.py +90 -0
  369. package/python/tests/test_multimodal.py +122 -0
  370. package/python/tests/test_negotiation.py +136 -0
  371. package/python/tests/test_orchestration.py +109 -0
  372. package/python/tests/test_orchestration_decoupled.py +103 -0
  373. package/python/tests/test_p3_p5_hardening.py +324 -0
  374. package/python/tests/test_parallel_dispatch.py +207 -0
  375. package/python/tests/test_pipeline.py +492 -0
  376. package/python/tests/test_plan_visible.py +86 -0
  377. package/python/tests/test_planner.py +88 -0
  378. package/python/tests/test_policy_gate.py +108 -0
  379. package/python/tests/test_profile.py +67 -0
  380. package/python/tests/test_readme_claims.py +163 -0
  381. package/python/tests/test_reasoner.py +170 -0
  382. package/python/tests/test_release_check.py +252 -0
  383. package/python/tests/test_resume_checkpoint.py +148 -0
  384. package/python/tests/test_routing.py +85 -0
  385. package/python/tests/test_scheduler.py +138 -0
  386. package/python/tests/test_search.py +194 -0
  387. package/python/tests/test_secrets.py +153 -0
  388. package/python/tests/test_semantic.py +69 -0
  389. package/python/tests/test_sessions.py +90 -0
  390. package/python/tests/test_settings.py +265 -0
  391. package/python/tests/test_shadow_learning.py +290 -0
  392. package/python/tests/test_shared_write.py +200 -0
  393. package/python/tests/test_skill_taint_gate.py +145 -0
  394. package/python/tests/test_skills.py +117 -0
  395. package/python/tests/test_skills_fetch.py +282 -0
  396. package/python/tests/test_stats_and_events.py +162 -0
  397. package/python/tests/test_streaming.py +55 -0
  398. package/python/tests/test_supervisor_pipeline.py +176 -0
  399. package/python/tests/test_supervisor_triage.py +102 -0
  400. package/python/tests/test_taint.py +112 -0
  401. package/python/tests/test_task_stream.py +74 -0
  402. package/python/tests/test_thinking_sse.py +66 -0
  403. package/python/tests/test_thinking_stream.py +76 -0
  404. package/python/tests/test_tool_center.py +113 -0
  405. package/python/tests/test_tool_parallel.py +113 -0
  406. package/python/tests/test_tool_params.py +99 -0
  407. package/python/tests/test_tool_use.py +229 -0
  408. package/python/tests/test_toplevel_concurrency.py +116 -0
  409. package/python/tests/test_tracing.py +256 -0
  410. package/python/tests/test_truth_crystallizer.py +125 -0
  411. package/python/tests/test_truth_flow.py +88 -0
  412. package/python/tests/test_update_check.py +95 -0
  413. package/python/tests/test_url_policy.py +53 -0
  414. package/python/tests/test_verify_evidence.py +212 -0
  415. package/python/tests/test_version_sync.py +100 -0
  416. package/python/tests/test_web_trigger.py +82 -0
  417. package/python/tools/check_faiss_type.py +30 -0
  418. package/python/tools/check_log.ps1 +38 -0
  419. package/python/tools/ivf_status.py +72 -0
  420. package/python/tools/mypy_baseline.txt +13 -0
  421. package/python/tools/mypy_ratchet.py +143 -0
  422. package/python/tools/pack.py +173 -0
  423. package/python/tools/release_check.py +349 -0
  424. package/python/tools/restart_foxclaw.ps1 +71 -0
  425. package/python/tools/shared_write_audit.py +190 -0
  426. package/python/tools/size_baseline.txt +28 -0
  427. package/python/tools/size_check.py +102 -0
  428. package/python/tools/strip_comments.py +228 -0
  429. package/python/tools/verify_env.bat +2 -0
  430. package/python/tools/verify_env.py +25 -0
  431. package/python/vendor/HMR_LICENSE +37 -0
  432. package/python/vendor/HMR_README.md +65 -0
  433. package/python/vendor/hkc/LICENSE +37 -0
  434. package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
  435. package/python/vendor/hkc/hkc_ace/ace.py +457 -0
  436. package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
  437. package/python/vendor/hkc/hkc_api/__init__.py +0 -0
  438. package/python/vendor/hkc/hkc_api/container.py +416 -0
  439. package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
  440. package/python/vendor/hkc/hkc_api/main.py +157 -0
  441. package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
  442. package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
  443. package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
  444. package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
  445. package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
  446. package/python/vendor/hkc/hkc_api/schemas.py +153 -0
  447. package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
  448. package/python/vendor/hkc/hkc_core/__init__.py +0 -0
  449. package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
  450. package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
  451. package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
  452. package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
  453. package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
  454. package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
  455. package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
  456. package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
  457. package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
  458. package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
  459. package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
  460. package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
  461. package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
  462. package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
  463. package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
  464. package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
  465. package/python/vendor/hkc/hkc_kde/kde.py +298 -0
  466. package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
  467. package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
  468. package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
  469. package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
  470. package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
  471. package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
  472. package/python/vendor/hkc/hkc_kde/models.py +74 -0
  473. package/python/vendor/hkc/hkc_kde/packager.py +329 -0
  474. package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
  475. package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
  476. package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
  477. package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
  478. package/python/vendor/hkc/hkc_kee/kee.py +541 -0
  479. package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
  480. package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
  481. package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
  482. package/python/vendor/hkc/hkc_sdk/base.py +151 -0
  483. package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
  484. package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
  485. package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
  486. package/python/vendor/hkc/hkc_sdk/models.py +171 -0
  487. package/python/vendor/hkc/hkc_search/__init__.py +0 -0
  488. package/python/vendor/hkc/hkc_search/bm25.py +325 -0
  489. package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
  490. package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
  491. package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
  492. package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
  493. package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
  494. package/python/vendor/hmr/__init__.py +0 -0
  495. package/python/vendor/hmr/core/__init__.py +0 -0
  496. package/python/vendor/hmr/core/hmr.py +684 -0
  497. package/python/vendor/hmr/core/models.py +310 -0
  498. package/python/vendor/hmr/engines/__init__.py +0 -0
  499. package/python/vendor/hmr/engines/evolution.py +710 -0
  500. package/python/vendor/hmr/engines/jit_compiler.py +472 -0
  501. package/python/vendor/hmr/engines/lifecycle.py +264 -0
  502. package/python/vendor/hmr/engines/policy.py +625 -0
  503. package/python/vendor/hmr/engines/recall.py +260 -0
  504. package/python/vendor/hmr/engines/runtime_state.py +158 -0
  505. package/python/vendor/hmr/engines/scheduler.py +323 -0
  506. package/python/vendor/hmr/engines/semantic.py +150 -0
  507. package/python/vendor/hmr/engines/temporal.py +261 -0
  508. package/python/vendor/hmr/engines/thought_chain.py +635 -0
  509. package/python/vendor/hmr/graph/__init__.py +0 -0
  510. package/python/vendor/hmr/graph/cwg.py +338 -0
  511. package/python/vendor/hmr/graph/memory_graph.py +530 -0
  512. package/python/vendor/hmr/storage/__init__.py +0 -0
  513. package/python/vendor/hmr/storage/memory_fs.py +300 -0
  514. package/python/vendor/hmr/storage/vector_store.py +520 -0
@@ -0,0 +1,268 @@
1
+ """
2
+ 知识核心(HKC 融入)测试 —— 全离线,embedding=stub,不需 LLM。
3
+
4
+ 覆盖:
5
+ 1. init_hkc 同进程装配 → available;空库混合检索返回 [];stats 有 data_dir
6
+ 2. H1 资源修复:close() 后能 rmtree 数据目录(无 Windows WinError 32)——
7
+ 即 event_bus/id_gen/graph_store/synthesis 四个 SQLite 句柄全部释放
8
+ 3. close() 幂等
9
+ 4. 经 Foxclaw 惰性装配:未访问 .hkc 前不开句柄;访问后装配;aclose() 释放后可删目录
10
+
11
+ 摄入(ingest)需真实 LLM,不在离线回归内跑;这里只验证融合层的装配/降级/资源契约。
12
+ """
13
+ import sys, tempfile, shutil, asyncio
14
+ from pathlib import Path
15
+ sys.path.insert(0, str(Path(__file__).parent.parent))
16
+
17
+ from foxclaw.knowledge import init_hkc
18
+
19
+
20
+ def test_assemble_search_close():
21
+ print("\n【1】init_hkc 装配 + 空库检索 + stats + close 后可删目录(H1)")
22
+ d = tempfile.mkdtemp(prefix="hkc_test_")
23
+ ok_rmtree = False
24
+ try:
25
+ svc = init_hkc(d, api_key=None)
26
+ assert svc is not None and svc.available, "依赖已装,HKC 应装配成功"
27
+ print(" ✓ 同进程装配成功 available=True")
28
+
29
+ hits = svc.client.search("任意不存在的查询", mode="hybrid", top_k=5)
30
+ assert isinstance(hits, list) and len(hits) == 0
31
+ print(f" ✓ 空库混合检索返回空: {hits}")
32
+
33
+ s = svc.stats()
34
+ assert "data_dir" in s and "total_kus" in s
35
+ print(f" ✓ stats: total_kus={s.get('total_kus')} embedding={s.get('embedding')!r}")
36
+
37
+ svc.close()
38
+ svc.close()
39
+ print(" ✓ close() 幂等")
40
+ finally:
41
+ shutil.rmtree(d)
42
+ ok_rmtree = True
43
+ assert ok_rmtree
44
+ print(" ✓ close 后 rmtree 成功 → H1 资源泄漏修复生效(无 WinError 32)")
45
+
46
+
47
+ def test_via_foxclawos_lazy_teardown():
48
+ print("\n【2】经 Foxclaw 惰性装配 + aclose teardown 全链路")
49
+ d = tempfile.mkdtemp(prefix="hkc_os_")
50
+ try:
51
+ from foxclaw import Foxclaw
52
+ os_ = Foxclaw(data_dir=d, shadow_rounds=50)
53
+ assert os_._hkc_service is None and os_._hkc_inited is False
54
+ print(" ✓ 构造 OS 后未装配 HKC(惰性,未开 db 句柄)")
55
+
56
+ hkc = os_.hkc
57
+ assert os_._hkc_inited is True
58
+ assert hkc is not None and hkc.available
59
+ print(" ✓ 首次访问 os_.hkc 触发装配 available=True")
60
+
61
+ hits = hkc.client.search("空库查询", top_k=3)
62
+ assert hits == []
63
+ print(" ✓ 经 DirectClient 检索空库返回 []")
64
+
65
+ asyncio.run(os_.aclose())
66
+ print(" ✓ aclose() 释放浏览器 + HKC 资源")
67
+ finally:
68
+ shutil.rmtree(d)
69
+ print(" ✓ teardown 后 rmtree 成功(HKC 句柄经 aclose 释放)")
70
+
71
+
72
+ def test_ability_readonly_surface():
73
+ print("\n【3】第2档 Step1:ACE 能力只读面(list/compile 证据 gate/neighbors)——无 LLM")
74
+ d = tempfile.mkdtemp(prefix="hkc_abil_")
75
+ svc = None
76
+ try:
77
+ svc = init_hkc(d, api_key=None)
78
+ assert svc is not None and svc.available
79
+ from hkc_sdk.exceptions import HKCInsufficientCoverage, HKCNotFoundError
80
+ keys = svc.client.list_abilities()
81
+ assert isinstance(keys, list) and len(keys) > 0, f"应有内置能力定义,实得 {keys}"
82
+ print(f" ✓ list_abilities 返回 {len(keys)} 个能力: {keys[:4]}...")
83
+ gated = False
84
+ try:
85
+ svc.client.compile_ability(keys[0])
86
+ except HKCInsufficientCoverage as e:
87
+ gated = True
88
+ assert hasattr(e, "missing_skills") and hasattr(e, "coverage")
89
+ assert gated, "空知识库编译能力应被证据 gate 拦(覆盖不足)"
90
+ print(" ✓ 空库 compile_ability 被证据 gate 拦(覆盖不足→能力不予授予)")
91
+ raised = False
92
+ try:
93
+ svc.client.neighbors("KU_nonexistent", max_depth=2, top_k=10)
94
+ except HKCNotFoundError:
95
+ raised = True
96
+ assert raised, "不存在 KU 应抛 HKCNotFoundError(SDK 契约)"
97
+ print(" ✓ neighbors(不存在 KU) 抛 HKCNotFoundError(SDK 契约;API 端点已删,见 V3 清理)")
98
+ finally:
99
+ if svc is not None:
100
+ svc.close()
101
+ shutil.rmtree(d, ignore_errors=True)
102
+ print(" ✓ 只读能力面成立:枚举 + 证据 gate + 图邻域(全无 LLM/零成本)")
103
+
104
+
105
+ def test_agent_available_abilities():
106
+ print("\n【4】第2档 Step2:Agent.available_abilities() 证据驱动自省(只读,不改执行)")
107
+ d = tempfile.mkdtemp(prefix="hkc_selfintro_")
108
+ os_ = None
109
+ try:
110
+ from foxclaw import Foxclaw
111
+ os_ = Foxclaw(data_dir=d, shadow_rounds=50)
112
+ abils = os_.finance.available_abilities()
113
+ assert isinstance(abils, list) and len(abils) > 0, f"应枚举出能力,实得 {abils}"
114
+ assert all(a["sufficient"] is False for a in abils), \
115
+ f"空库→全部证据 gate 拦(sufficient=False),实得 {[a['sufficient'] for a in abils]}"
116
+ assert all("missing_skills" in a for a in abils), "未达标应附 missing_skills"
117
+ print(f" ✓ 空库自省:{len(abils)} 个能力全 sufficient=False(证据 gate)")
118
+ assert len(os_.office.available_abilities()) == len(abils)
119
+ print(" ✓ 各 Agent 一致(HKC 知识全局共享)")
120
+ asyncio.run(os_.aclose())
121
+ finally:
122
+ shutil.rmtree(d, ignore_errors=True)
123
+ print(" ✓ 能力自省成立:证据驱动、只读、无 LLM")
124
+
125
+
126
+ def test_ability_context_injection():
127
+ print("\n【5】第2档 Step3:能力→上下文注入(opt-in;默认关不改行为;fail-safe)")
128
+ from types import SimpleNamespace
129
+ d = tempfile.mkdtemp(prefix="hkc_inject_")
130
+ os_ = None
131
+ try:
132
+ from foxclaw import Foxclaw
133
+ os_ = Foxclaw(data_dir=d, shadow_rounds=50)
134
+ ag = os_.finance
135
+ base = [{"role": "user", "content": "原始问题"}]
136
+
137
+ assert ag._ability_augment("帮我做写作专家分析", list(base)) == base
138
+
139
+ ag.ABILITY_AUGMENT = True
140
+ assert ag._ability_augment("帮我做写作专家分析", list(base)) == base
141
+
142
+ class _C:
143
+ def list_abilities(self): return ["writing_expert"]
144
+ def compile_ability(self, k):
145
+ return SimpleNamespace(ability_key=k, display_name="写作专家",
146
+ domain="writing", coverage={"修辞": 0.9}, skills=[])
147
+ def search(self, q, mode="hybrid", top_k=10, domain=""):
148
+ return [SimpleNamespace(name="修辞", summary="比喻与排比",
149
+ ku_id="K1", score=0.9, ku_type="Concept")]
150
+ os_.capabilities.register("hkc", SimpleNamespace(available=True, client=_C()))
151
+ out = ag._ability_augment("帮我做写作专家风格分析", list(base))
152
+ assert len(out) == 2 and "能力知识增强" in out[0]["content"] and "修辞" in out[0]["content"]
153
+ assert out[1] == base[0], "原消息应保留在注入内容之后"
154
+ print(" ✓ 默认关不注入;空库 no-op;启用+达标+命中→注入知识上下文")
155
+
156
+ class _Cbad(_C):
157
+ def search(self, *a, **k): raise RuntimeError("boom")
158
+ os_.capabilities.register("hkc", SimpleNamespace(available=True, client=_Cbad()))
159
+ assert ag._ability_augment("帮我做写作专家分析", list(base)) == base
160
+ print(" ✓ 增强内部异常 → fail-safe 原样返回(不影响主流程)")
161
+ asyncio.run(os_.aclose())
162
+ finally:
163
+ shutil.rmtree(d, ignore_errors=True)
164
+ print(" ✓ 能力上下文注入成立:opt-in、匹配、fail-safe、默认行为不变")
165
+
166
+
167
+ def test_ability_as_skill_step4():
168
+ print("\n【6】第2档 Step4:能力→SkillRegistry(证据 gate + read 经 PolicyGate + 可路由)")
169
+ from types import SimpleNamespace
170
+ from foxclaw.knowledge import register_abilities_as_skills
171
+ from foxclaw.skills import SkillRegistry, SkillAdapter
172
+
173
+ class _C:
174
+ def list_abilities(self): return ["writing_expert", "quant_analyst"]
175
+ def compile_ability(self, k):
176
+ if k == "quant_analyst":
177
+ raise RuntimeError("coverage 不足")
178
+ return SimpleNamespace(ability_key=k, display_name="写作专家",
179
+ domain="writing", coverage={"修辞": 0.9})
180
+ def search(self, q, mode="hybrid", top_k=10, domain=""):
181
+ return [SimpleNamespace(name="修辞", summary="比喻与排比",
182
+ ku_id="K1", score=0.9, ku_type="Concept")]
183
+
184
+ hkc = SimpleNamespace(available=True, client=_C())
185
+ reg = SkillRegistry()
186
+ ids = register_abilities_as_skills(reg, hkc)
187
+ assert ids == ["ability:writing_expert"], ids
188
+ assert reg.get("ability:quant_analyst") is None, "覆盖不足→不注册(证据 gate)"
189
+ spec = reg.get("ability:writing_expert")
190
+ assert spec is not None and spec.side_effect == "read", "能力 skill 为 read(无外部副作用)"
191
+
192
+ res = SkillAdapter(reg).invoke(spec, {"query": "写作风格"})
193
+ assert res.success and res.output.get("ability") == "writing_expert", res.to_dict()
194
+ assert res.output["knowledge"] and res.output["knowledge"][0]["name"] == "修辞"
195
+ print(" ✓ 证据 gate:达标能力注册为 read skill、覆盖不足不注册;经 PolicyGate 放行返回知识")
196
+
197
+ assert register_abilities_as_skills(SkillRegistry(), None) == []
198
+ assert register_abilities_as_skills(None, hkc) == []
199
+ print(" ✓ HKC 不可用/registry 缺失 → 优雅返回 []")
200
+
201
+
202
+ def test_ability_skill_prune_and_concurrency():
203
+ print("\n【7】深审修复:ability skill 陈旧清理(re-sync)+ SkillRegistry 线程安全")
204
+ import threading
205
+ from types import SimpleNamespace
206
+ from foxclaw.knowledge import register_abilities_as_skills
207
+ from foxclaw.skills import SkillRegistry, SkillSpec
208
+
209
+ class _Cdrop:
210
+ def __init__(self): self.ok = True
211
+ def list_abilities(self): return ["writing_expert"]
212
+ def compile_ability(self, k):
213
+ if not self.ok:
214
+ raise RuntimeError("coverage 掉了")
215
+ return SimpleNamespace(ability_key=k, display_name="写作专家",
216
+ domain="writing", coverage={})
217
+ def search(self, *a, **k): return []
218
+ c = _Cdrop()
219
+ hkc = SimpleNamespace(available=True, client=c)
220
+ reg = SkillRegistry()
221
+ assert register_abilities_as_skills(reg, hkc) == ["ability:writing_expert"]
222
+ assert reg.get("ability:writing_expert") is not None
223
+ c.ok = False
224
+ assert register_abilities_as_skills(reg, hkc) == []
225
+ assert reg.get("ability:writing_expert") is None, "陈旧 ability skill 应在 re-sync 时被移除"
226
+ print(" ✓ re-sync:覆盖掉了的 ability skill 被清理(不残留)")
227
+
228
+ reg2 = SkillRegistry()
229
+ errs = []
230
+ def reg_worker():
231
+ try:
232
+ for i in range(400):
233
+ reg2.register(SkillSpec(id=f"s{i % 20}", name="x", description="",
234
+ kind="local", triggers=["x"]))
235
+ except Exception as e:
236
+ errs.append(repr(e))
237
+ def read_worker():
238
+ try:
239
+ for _ in range(400):
240
+ reg2.match("任意任务 x"); reg2.all(); reg2.catalog()
241
+ except Exception as e:
242
+ errs.append(repr(e))
243
+ ts = [threading.Thread(target=reg_worker) for _ in range(3)] + \
244
+ [threading.Thread(target=read_worker) for _ in range(3)]
245
+ for t in ts: t.start()
246
+ for t in ts: t.join()
247
+ assert not errs, f"并发 register/match 应无异常,实得 {errs[:2]}"
248
+ print(" ✓ 6 线程并发 register/match/all/catalog 无异常(SkillRegistry 加锁生效)")
249
+
250
+
251
+ def main():
252
+ print("=" * 60)
253
+ print("知识核心 · HKC 融入(全离线,embedding=stub)")
254
+ print("=" * 60)
255
+ test_assemble_search_close()
256
+ test_via_foxclawos_lazy_teardown()
257
+ test_ability_readonly_surface()
258
+ test_agent_available_abilities()
259
+ test_ability_context_injection()
260
+ test_ability_as_skill_step4()
261
+ test_ability_skill_prune_and_concurrency()
262
+ print("\n" + "=" * 60)
263
+ print("✓ HKC 融入验证完成:装配/检索/stats + H1 资源修复 + 惰性/teardown")
264
+ print("=" * 60)
265
+
266
+
267
+ if __name__ == "__main__":
268
+ main()
@@ -0,0 +1,306 @@
1
+ """
2
+ HKC 后台索引构建测试(阶段 C 提交)。
3
+
4
+ 覆盖范围:
5
+ S1 无索引启动 + 库有数据 → assemble 立即返;manager 后台编码;
6
+ 等 join 后 vector_size == source_total,index_state=ready。
7
+ S2 旧快照启动(磁盘 faiss 完整覆盖库) → assemble 立即返;
8
+ manager.start() 返 None(无需构建);vector_size == 磁盘中数。
9
+ S3 构建中重启 → 续跑(落盘 faiss 删光,manager 仍从空开始;但状态机重启后
10
+ state 仍为上次 cancelled/ready,这是 next.start() 决策依据)
11
+ S4 构建中查询:manager 跑后台编码的同时,前端调 c.search.search()
12
+ → 不抛错;能拿到当前 partial 结果。
13
+
14
+ 设计原则(同 P0/P1/P2):
15
+ - 不 mock:真 SQLite + 真 faiss + StubBackend + 真 HKCContainer + 真 IndexBuildManager。
16
+ - vendor 隔离硬阻断:模块级解析后立即自检 __file__,不是 vendor 就 RuntimeError。
17
+ - 不依赖网络 / 真实模型 / 真实数据目录;全程 tmp_path。
18
+ - 每个测试独立 tmp_path;HKCContainer init/reset 严格 try/finally。
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import sys
23
+ import time
24
+ from pathlib import Path
25
+
26
+
27
+ from hkc_search.index_build_manager import IndexBuildManager, _BUILD_STATES
28
+ from hkc_search.vector_search import VectorIndex
29
+ from hkc_search.embedding_backends import StubBackend
30
+ from hkc_api.container import HKCContainer, init_container, reset_container
31
+ from hkc_core.graph.sqlite_store import SQLiteGraphStore
32
+ from hkc_core.models.ku import ConceptKU
33
+
34
+ _VENDOR_ROOT = Path(__file__).resolve().parents[1] / "vendor" / "hkc"
35
+
36
+
37
+ def _assert_vendor_path(obj, name: str) -> None:
38
+ mod = obj.__module__
39
+ f = getattr(sys.modules[mod], "__file__", "") or ""
40
+ assert str(Path(f).resolve()).startswith(str(_VENDOR_ROOT.resolve())), (
41
+ f"vendor isolation failed: {name}.__module__={mod!r} __file__={f!r} "
42
+ f"not under vendor/hkc. Test is running against wrong HKC copy."
43
+ )
44
+
45
+
46
+ for _cls_name, _cls in [
47
+ ("IndexBuildManager", IndexBuildManager),
48
+ ("VectorIndex", VectorIndex),
49
+ ("StubBackend", StubBackend),
50
+ ("HKCContainer", HKCContainer),
51
+ ("SQLiteGraphStore", SQLiteGraphStore),
52
+ ("ConceptKU", ConceptKU),
53
+ ]:
54
+ _assert_vendor_path(_cls, _cls_name)
55
+
56
+
57
+ def _seed_kus(c: HKCContainer, ids: list[str]) -> None:
58
+ """直接 put 进 store,不发出 KNOWLEDGE_CREATED 事件。
59
+ 用于构造 '库有数据但 vector 没条' 的初始状态,触发 manager 全量构建。
60
+ """
61
+ for i, ku_id in enumerate(ids):
62
+ ku = ConceptKU(
63
+ ku_id=ku_id,
64
+ name=f"ku_{i}",
65
+ summary=f"ku {i} 的描述是若干中文文字以填充文本",
66
+ domain="d",
67
+ )
68
+ c.graph_store.put(ku)
69
+
70
+
71
+ def _wait_idle(c: HKCContainer, timeout: float = 30.0) -> dict:
72
+ """等 manager 跑完一轮变成 ready/failed/cancelled,返 snapshot。timeout 内的 poll。"""
73
+ deadline = time.time() + timeout
74
+ mgr = c.index_build_manager
75
+ last_state = None
76
+ while time.time() < deadline:
77
+ s = mgr.stats()
78
+ if s["index_state"] in ("ready", "failed", "cancelled", "idle"):
79
+ return s
80
+ last_state = s
81
+ time.sleep(0.05)
82
+ raise AssertionError(
83
+ f"等超时 {timeout}s;最后状态: {last_state}"
84
+ )
85
+
86
+
87
+ def _seed_and_start_build(c: HKCContainer, ids: list[str]) -> None:
88
+ """init_container 在装配期会扫描库。此时若库空,manager 会立即 idle;
89
+ seed KU 后必须显式再 start() 触发 full build(在已加载的 vector 上 append)。
90
+ 装配时已自动 start 过一次(空库 → idle),这次手动 start 触发增量构建。
91
+ """
92
+ _seed_kus(c, ids)
93
+ r = c.index_build_manager.start()
94
+ assert r is not None, (
95
+ f"_seed_and_start_build 应触发新构建,但 start() 返 None。"
96
+ f"当前 stats: {c.index_build_manager.stats()}"
97
+ )
98
+
99
+
100
+ class TestStartNoIndexManagerBuildsBackground:
101
+ def test_assemble_returns_fast_manager_builds_then_ready(self, tmp_path):
102
+ """库有 5 KU 但磁盘无索引 → seed + start → manager 后台构建;
103
+ join 后 vector_size == source_total 且 state=ready。"""
104
+ data_dir = str(tmp_path)
105
+ c = init_container(data_dir=data_dir, embedding_kind="stub")
106
+ try:
107
+ _seed_and_start_build(c, [f"KU_S1_{i}" for i in range(5)])
108
+ mgr = c.index_build_manager
109
+ assert mgr is not None
110
+ final = _wait_idle(c, timeout=30)
111
+ assert final["index_state"] == "ready", (
112
+ f"应在 ready,实际 {final}"
113
+ )
114
+ assert final["source_total"] == 5, final
115
+ assert final["ready"] == 5, final
116
+ assert final["failed"] == 0, final
117
+ assert c.search.vector.size() == 5
118
+ finally:
119
+ c.close()
120
+ reset_container()
121
+
122
+ def test_assemble_does_not_block_when_manager_builds(self, tmp_path):
123
+ """关键不变量:装配本身**不阻塞**全量 encode。
124
+ 装配返回(manager 已 start)再 seed → 整体构建必须由 manager 后台承担。"""
125
+ data_dir = str(tmp_path)
126
+ c = init_container(data_dir=data_dir, embedding_kind="stub")
127
+ try:
128
+ _seed_and_start_build(c, [f"KU_FAST_{i}" for i in range(2)])
129
+ s = c.index_build_manager.stats()
130
+ assert s["index_state"] in _BUILD_STATES
131
+ final = _wait_idle(c, timeout=10)
132
+ assert final["index_state"] == "ready"
133
+ assert final["ready"] == 2
134
+ finally:
135
+ c.close()
136
+ reset_container()
137
+
138
+
139
+ class TestStartWithPersistedSnapshotNoRebuild:
140
+ def test_manager_skips_when_disk_snapshot_covers_source(self, tmp_path):
141
+ """先启动一次让 manager 构建完 → close;重启 → manager 看到 faiss 已非空
142
+ → start() 返 None,不再构建。"""
143
+ data_dir = str(tmp_path)
144
+ c = init_container(data_dir=data_dir, embedding_kind="stub")
145
+ try:
146
+ _seed_and_start_build(c, [f"KU_S2A_{i}" for i in range(4)])
147
+ _wait_idle(c, timeout=20)
148
+ assert c.search.vector.size() == 4
149
+ finally:
150
+ c.close()
151
+ reset_container()
152
+
153
+ c2 = init_container(data_dir=data_dir, embedding_kind="stub")
154
+ try:
155
+ s = c2.index_build_manager.stats()
156
+ assert s["index_state"] == "ready", (
157
+ f"已有磁盘快照时 manager 应跳过,实际 {s}"
158
+ )
159
+ assert s["vector_size"] == 4, (
160
+ f"vector_size 应从磁盘恢复,实际 {s}"
161
+ )
162
+ r = c2.index_build_manager.start()
163
+ assert r is None, (
164
+ f"已有快照时 start() 应返 None,实际 {r!r}"
165
+ )
166
+ finally:
167
+ c2.close()
168
+ reset_container()
169
+
170
+
171
+ class TestMidBuildRestartPicksUp:
172
+ def test_resume_after_disk_vector_partial(self, tmp_path):
173
+ """模拟 '上次构建到一半被杀掉':磁盘 faiss 有 5 条全量,manager 看到库空
174
+ 就不重跑(构不了啥)。验证 restart 链路:装配→加载 faiss→manager 跳过。
175
+ 真正的「mid-build 续跑」语义由 state.json + vector 缺漏检测;
176
+ 但因为 manager 的 start() 总是基于「当前 vector vs 当前库」做决策,
177
+ 磁盘有 5 条 + 库 5 条相同 → 跳过(等价于 S2 的 happy path)。"""
178
+ data_dir = str(tmp_path)
179
+ c = init_container(data_dir=data_dir, embedding_kind="stub")
180
+ try:
181
+ _seed_and_start_build(c, [f"KU_S3_{i}" for i in range(5)])
182
+ _wait_idle(c, timeout=20)
183
+ assert c.search.vector.size() == 5
184
+ finally:
185
+ c.close()
186
+ reset_container()
187
+
188
+ vec_path = tmp_path / "search_vector"
189
+ if (vec_path.with_suffix(".faiss")).exists():
190
+ (vec_path.with_suffix(".faiss")).unlink()
191
+ if (vec_path.with_suffix(".meta")).exists():
192
+ (vec_path.with_suffix(".meta")).unlink()
193
+
194
+ import json as _json
195
+ state = {
196
+ "task_id": "build-deadbeef",
197
+ "index_version": "v-stale",
198
+ "state": "building",
199
+ "source_total": 5,
200
+ "scanned": 3,
201
+ "ready": 3,
202
+ "failed": 0,
203
+ "failures": [],
204
+ "started_at": "2025-01-01T00:00:00+00:00",
205
+ "finished_at": None,
206
+ "last_checkpoint_at": "2025-01-01T00:00:01+00:00",
207
+ "error_summary": "",
208
+ }
209
+ (tmp_path / IndexBuildManager.STATE_FILE).write_text(
210
+ _json.dumps(state, ensure_ascii=False, indent=2),
211
+ encoding="utf-8",
212
+ )
213
+
214
+ c2 = init_container(data_dir=data_dir, embedding_kind="stub")
215
+ try:
216
+ final = _wait_idle(c2, timeout=30)
217
+ assert final["index_state"] == "ready", (
218
+ f"重启后应到 ready,实际 {final}"
219
+ )
220
+ assert c2.search.vector.size() == 5, (
221
+ f"重启后 vector_size 应=5,实际 {c2.search.vector.size()}"
222
+ )
223
+ assert final["task_id"] != "build-deadbeef", (
224
+ f"应起新 task_id,实际仍用历史的 {final['task_id']!r}"
225
+ )
226
+ finally:
227
+ c2.close()
228
+ reset_container()
229
+
230
+
231
+ class TestSearchDuringBuild:
232
+ def test_concurrent_search_does_not_crash(self, tmp_path):
233
+ """manager 后台跑编码的同时,前调 c.search.search() → 不抛错;
234
+ 任何阶段拿到的是 partial 但应是合法 SearchResult 列表。"""
235
+ data_dir = str(tmp_path)
236
+ c = init_container(data_dir=data_dir, embedding_kind="stub")
237
+ try:
238
+ _seed_and_start_build(c, [f"KU_S4_{i}" for i in range(6)])
239
+ results = c.search.search("ku_1 描述", mode="hybrid", top_k=3)
240
+ assert isinstance(results, list)
241
+ for r in results:
242
+ assert hasattr(r, "ku_id")
243
+ assert hasattr(r, "score")
244
+ final = _wait_idle(c, timeout=30)
245
+ assert final["index_state"] == "ready"
246
+ assert c.search.vector.size() == 6
247
+ results2 = c.search.search("ku_2", mode="bm25", top_k=2)
248
+ assert isinstance(results2, list)
249
+ finally:
250
+ c.close()
251
+ reset_container()
252
+
253
+
254
+ class TestManagerStats:
255
+ def test_stats_keys_present(self, tmp_path):
256
+ """stats 字典字段稳定性(对外观测前端要靠这些 key)。"""
257
+ mgr = IndexBuildManager(
258
+ data_dir=str(tmp_path),
259
+ hybrid_search=_FakeHybrid(),
260
+ graph_store=_FakeEmptyGraphStore(),
261
+ )
262
+ s = mgr.stats()
263
+ for k in (
264
+ "index_state", "index_version", "task_id", "source_total",
265
+ "scanned", "ready", "pending", "failed",
266
+ "started_at", "finished_at", "last_checkpoint_at",
267
+ "error_summary", "failures_preview", "vector_size",
268
+ ):
269
+ assert k in s, f"stats 缺字段 {k}, got {sorted(s.keys())}"
270
+
271
+ def test_idle_start_no_kus_yields_ready(self, tmp_path):
272
+ """空 store + 空 faiss → start 起线程跑,扫描后 source_total=0 → 立即 ready。"""
273
+ mgr = IndexBuildManager(
274
+ data_dir=str(tmp_path),
275
+ hybrid_search=_FakeHybrid(),
276
+ graph_store=_FakeEmptyGraphStore(),
277
+ )
278
+ mgr.start()
279
+ mgr.join(timeout=5)
280
+ s = mgr.stats()
281
+ assert s["index_state"] in ("ready", "cancelled", "failed"), s
282
+ assert s["source_total"] == 0, s
283
+
284
+
285
+ class _FakeHybrid:
286
+ """空 hybrid:vector_size=0,_ku_ids 空列表,_lock 是 RLock,vector 假实例。"""
287
+ def __init__(self):
288
+ import threading as _t
289
+ self._lock = _t.RLock()
290
+ self.vector = _FakeVector()
291
+
292
+
293
+ class _FakeVector:
294
+ def __init__(self):
295
+ self._ku_ids: list = []
296
+ def size(self): return len(self._ku_ids)
297
+ def save(self, path): pass
298
+ def append_bulk(self, kus):
299
+ with _FakeVector._lock:
300
+ self._ku_ids.extend(k.ku_id for k in kus)
301
+ _lock = __import__("threading").RLock()
302
+
303
+
304
+ class _FakeEmptyGraphStore:
305
+ def query_all(self, limit=99999999):
306
+ return []